Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.integration.region

import app.IntegrationTestSupport
import app.bottlenote.alcohols.domain.RegionRepository
import app.bottlenote.alcohols.dto.request.AdminRegionCreateRequest
import app.bottlenote.alcohols.dto.request.AdminRegionSortOrderRequest
import app.bottlenote.alcohols.dto.request.AdminRegionUpdateRequest
Expand All @@ -21,6 +22,9 @@ class AdminRegionIntegrationTest : IntegrationTestSupport() {
@Autowired
private lateinit var regionTestFactory: RegionTestFactory

@Autowired
private lateinit var regionRepository: RegionRepository

private lateinit var accessToken: String

@BeforeEach
Expand Down Expand Up @@ -133,6 +137,33 @@ class AdminRegionIntegrationTest : IntegrationTestSupport() {
.bodyJson()
.extractingPath("$.data.code").isEqualTo("REGION_UPDATED")
}

@Test
@DisplayName("지역 수정 시 lastModifyAt이 갱신된다")
fun updateRefreshesLastModifyAt() {
val region = regionTestFactory.persistRoot("스코트랜드", "ScotlandTypo", 10)
val before = regionRepository.findById(region.id!!).orElseThrow().lastModifyAt
Thread.sleep(1100)

val request = AdminRegionUpdateRequest.builder()
.korName("스코틀랜드")
.engName("Scotland")
.description("정정")
.sortOrder(10)
.build()

assertThat(
mockMvcTester
.put()
.uri("/v1/regions/${region.id}")
.header("Authorization", "Bearer $accessToken")
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request))
).hasStatusOk()

val after = regionRepository.findById(region.id!!).orElseThrow().lastModifyAt
assertThat(after).isAfter(before)
}
}

@Nested
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package app.bottlenote.alcohols.dto.response;

import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;

@Getter
@EqualsAndHashCode
@AllArgsConstructor(staticName = "of")
public class RegionsItem {
private final Long regionId;
Expand Down
1 change: 1 addition & 0 deletions bottlenote-product-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
// ===== Spring Boot Web =====
implementation libs.spring.boot.starter.web
implementation libs.spring.boot.starter.validation
implementation libs.spring.boot.starter.data.jpa

// OpenAPI 스펙 생성 (swagger-ui는 설정으로 비활성)
implementation libs.springdoc.openapi.starter.webmvc.ui
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package app.bottlenote.alcohols.service;

import app.bottlenote.alcohols.domain.RegionRepository;
import app.bottlenote.alcohols.dto.response.RegionsItem;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class RegionCacheRefreshService {

private static final String REGION_CACHE_NAME = "local_cache_alcohol_region_information";

private final RegionRepository regionRepository;
private final CacheManager cacheManager;
private List<RegionsItem> lastKnownRegions;

@EventListener(ApplicationReadyEvent.class)
@Transactional(readOnly = true)
public void initializeRevision() {
refresh();
}

@Scheduled(cron = "${schedules.region.cache.refresh.cron:0 */5 * * * *}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking — rule-tests 실패

@Service의 public 메서드는 @Transactional을 가져야 한다는 ArchUnit 규칙 때문에 initializeRevision()refresh()가 실패합니다. DB 조회 경계인 refresh()에는 @Transactional(readOnly = true)를 적용하고, initializeRevision()의 public 메서드 규칙도 프로젝트 방식에 맞게 충족시켜 주세요. 현재 CI rule-tests는 이 위반 2건으로 실패 중입니다.

@Transactional(readOnly = true)
public synchronized void refresh() {
List<RegionsItem> currentRegions = regionRepository.findAllRegionsResponse();
if (lastKnownRegions != null && !lastKnownRegions.equals(currentRegions)) {
Cache cache = cacheManager.getCache(REGION_CACHE_NAME);
if (cache != null) {
cache.clear();
log.info("지역 캐시를 갱신했습니다.");
}
}
lastKnownRegions = currentRegions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package app.bottlenote.alcohols.service;

import static org.assertj.core.api.Assertions.assertThat;

import app.bottlenote.alcohols.domain.Region;
import app.bottlenote.alcohols.fixture.InMemoryRegionRepository;
import java.time.LocalDateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.test.util.ReflectionTestUtils;

@Tag("unit")
@DisplayName("[unit] RegionCacheRefreshService")
class RegionCacheRefreshServiceTest {

private static final String REGION_CACHE_NAME = "local_cache_alcohol_region_information";

private final InMemoryRegionRepository regionRepository = new InMemoryRegionRepository();
private final CacheManager cacheManager = new ConcurrentMapCacheManager(REGION_CACHE_NAME);
private RegionCacheRefreshService service;

@BeforeEach
void setUp() {
service = new RegionCacheRefreshService(regionRepository, cacheManager);
}

@Test
@DisplayName("지역이 추가되면 Product 지역 캐시를 비운다")
void refresh_whenRegionAdded_clearsRegionCache() {
service.refresh();

Cache cache = cacheManager.getCache(REGION_CACHE_NAME);
cache.put(SimpleKey.EMPTY, "cached-regions");
regionRepository.save(Region.builder().korName("스코틀랜드").engName("Scotland").build());

service.refresh();

assertThat(cache.get(SimpleKey.EMPTY)).isNull();
}

@Test
@DisplayName("같은 최종 수정 시각에 지역 내용이 변경되면 Product 지역 캐시를 비운다")
void refresh_whenRegionContentChangedWithSameLastModifyAt_clearsRegionCache() {
Region region = Region.builder().korName("스코틀랜드").engName("Scotland").build();
regionRepository.save(region);
ReflectionTestUtils.setField(region, "lastModifyAt", LocalDateTime.of(2026, 7, 29, 12, 5));
service.refresh();

Cache cache = cacheManager.getCache(REGION_CACHE_NAME);
cache.put(SimpleKey.EMPTY, "cached-regions");
region.update("스코틀랜드", "Scotland", null, "변경된 설명", null, null, null);

service.refresh();

assertThat(cache.get(SimpleKey.EMPTY)).isNull();
}

@Test
@DisplayName("지역이 삭제되면 Product 지역 캐시를 비운다")
void refresh_whenRegionDeleted_clearsRegionCache() {
Region region = Region.builder().korName("스코틀랜드").engName("Scotland").build();
regionRepository.save(region);
service.refresh();

Cache cache = cacheManager.getCache(REGION_CACHE_NAME);
cache.put(SimpleKey.EMPTY, "cached-regions");
regionRepository.delete(region);

service.refresh();

assertThat(cache.get(SimpleKey.EMPTY)).isNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public List<RegionsItem> findAllRegionsResponse() {
.toList();
}


@Override
public Page<AdminRegionItem> findAllRegions(String keyword, Pageable pageable) {
List<AdminRegionItem> filtered =
Expand Down
Loading