diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 0329f1c..9c0ba16 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -35,29 +35,13 @@ jobs: key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar -# - name: S3에서 init.sql 다운로드 -# env: -# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} -# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} -# AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} -# run: | -# mkdir -p docker -# aws s3 cp s3://devcos4-team08-bucket/db/init.sql ./docker/init.sql - - - name: Docker Compose 설치 run: | sudo curl -L "https://github.com/docker/compose/releases/download/v2.17.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose - - name: CI용 빈 init.sql 생성 - run: | - mkdir -p docker - touch docker/init.sql - - name: Docker Compose 실행 run: | - chmod -R 755 ./docker docker-compose -f docker-compose.yml up -d - name: 컨테이너 실행 대기 @@ -117,7 +101,7 @@ jobs: run: | chmod +x ./gradlew # 소나클라우드 임시 비활성화 ./gradlew build jacocoTestReport sonar --info -Pprofile=dev -Dsonar.branch.name=${{ github.ref_name }} - ./gradlew build -i jacocoTestReport -Pprofile=dev + ./gradlew build -i jacocoTestReport -Pprofile=test - name: Docker Compose 종료 diff --git a/docker-compose.yml b/docker-compose.yml index 7b93d5e..0696c8c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,6 @@ services: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data - - ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql networks: - log4u-net diff --git a/src/main/java/com/example/log4u/common/config/PostgreSqlConfig.java b/src/main/java/com/example/log4u/common/config/PostgreSqlConfig.java index 890459a..17e9e44 100644 --- a/src/main/java/com/example/log4u/common/config/PostgreSqlConfig.java +++ b/src/main/java/com/example/log4u/common/config/PostgreSqlConfig.java @@ -44,7 +44,7 @@ public LocalContainerEntityManagerFactoryBean postgresqlEntityManagerFactory() { Map properties = new HashMap<>(); properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"); - properties.put("hibernate.hbm2ddl.auto", "none"); + properties.put("hibernate.hbm2ddl.auto", "update"); properties.put("hibernate.format_sql", true); em.setJpaPropertyMap(properties); diff --git a/src/main/java/com/example/log4u/common/config/S3Config.java b/src/main/java/com/example/log4u/common/config/S3Config.java index c7ebb1a..d3f6fa9 100644 --- a/src/main/java/com/example/log4u/common/config/S3Config.java +++ b/src/main/java/com/example/log4u/common/config/S3Config.java @@ -13,13 +13,13 @@ @Configuration public class S3Config { - @Value("${AWS_ACCESS_KEY_ID}") + @Value("${spring.aws.access-key-id}") private String accessKey; - @Value("${AWS_SECRET_ACCESS_KEY}") + @Value("${spring.aws.secret-access-key}") private String secretKey; - @Value("${AWS_REGION:ap-northeast-2}") + @Value("${spring.aws.region:ap-northeast-2}") private String region; @Bean diff --git a/src/main/java/com/example/log4u/common/executor/DistributedLockExecutor.java b/src/main/java/com/example/log4u/common/executor/DistributedLockExecutor.java new file mode 100644 index 0000000..f32dc1d --- /dev/null +++ b/src/main/java/com/example/log4u/common/executor/DistributedLockExecutor.java @@ -0,0 +1,31 @@ +package com.example.log4u.common.executor; + +import java.util.function.Supplier; + +import org.springframework.stereotype.Component; + +import com.example.log4u.common.infra.cache.CacheManager; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DistributedLockExecutor { + + private final CacheManager cacheManager; + + public void runWithLock(String lockKey, Runnable task) { + runWithLock(lockKey, () -> task); + } + + public T runWithLock(String lockKey, Supplier task) { + if (!cacheManager.tryLock(lockKey)) { + return null; + } + try { + return task.get(); + } finally { + cacheManager.releaseLock(lockKey); + } + } +} diff --git a/src/main/java/com/example/log4u/common/executor/RetryExecutor.java b/src/main/java/com/example/log4u/common/executor/RetryExecutor.java new file mode 100644 index 0000000..024b29c --- /dev/null +++ b/src/main/java/com/example/log4u/common/executor/RetryExecutor.java @@ -0,0 +1,39 @@ +package com.example.log4u.common.executor; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.springframework.stereotype.Component; + +import com.example.log4u.domain.map.exception.MaxRetryExceededException; + +@Component +public class RetryExecutor { + + int MAX_RETRY_CNT = 10; + int INITIAL_DELAY_MSEC = 1_000; + int MAX_DELAY_MSEC = 100_000; + + public T runWithRetry(Supplier task) { + for (int attempt = 0; attempt < MAX_RETRY_CNT; attempt++) { + T result = task.get(); + if (result != null) { + return result; + } + backoff(attempt); + } + throw new MaxRetryExceededException(); + } + + private void backoff(int attempt) { + try { + int delay = Math.min( + INITIAL_DELAY_MSEC * (int) Math.pow(2, attempt - 1), + MAX_DELAY_MSEC); + + TimeUnit.MILLISECONDS.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/com/example/log4u/common/infra/cache/CacheManager.java b/src/main/java/com/example/log4u/common/infra/cache/CacheManager.java index 305d04b..17fc96e 100644 --- a/src/main/java/com/example/log4u/common/infra/cache/CacheManager.java +++ b/src/main/java/com/example/log4u/common/infra/cache/CacheManager.java @@ -4,9 +4,15 @@ public interface CacheManager { - void cache(String key, String value, Duration ttl); + void init(); + + void cache(String key, String value, Duration ttl); String get(String key); void evict(String key); + + Boolean tryLock(String key); + + void releaseLock(String key); } diff --git a/src/main/java/com/example/log4u/common/infra/cache/RedisCacheManagerImpl.java b/src/main/java/com/example/log4u/common/infra/cache/RedisCacheManagerImpl.java index bdde4e9..ad48808 100644 --- a/src/main/java/com/example/log4u/common/infra/cache/RedisCacheManagerImpl.java +++ b/src/main/java/com/example/log4u/common/infra/cache/RedisCacheManagerImpl.java @@ -14,7 +14,14 @@ @RequiredArgsConstructor public class RedisCacheManagerImpl implements CacheManager { - private final RedisTemplate redisTemplate; + private static final String LOCK_VALUE = "locked"; + + private final RedisTemplate redisTemplate; + + public void init() { + Objects.requireNonNull(redisTemplate.getConnectionFactory()) + .getConnection().flushAll(); + } public void cache(String key, String value, Duration ttl) { redisTemplate.opsForValue().set(key, value, ttl); @@ -27,4 +34,12 @@ public String get(String key) { public void evict(String key) { redisTemplate.delete(key); } + + public Boolean tryLock(String key) { + return redisTemplate.opsForValue().setIfAbsent(key, LOCK_VALUE); + } + + public void releaseLock(String key) { + redisTemplate.delete(key); + } } diff --git a/src/main/java/com/example/log4u/domain/map/cache/CacheKeyGenerator.java b/src/main/java/com/example/log4u/domain/map/cache/CacheKeyGenerator.java deleted file mode 100644 index fd5137d..0000000 --- a/src/main/java/com/example/log4u/domain/map/cache/CacheKeyGenerator.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.log4u.domain.map.cache; - -public class CacheKeyGenerator { - - private static final String CLUSTER_CACHE_KEY = "cluster:geohash:%s:level:%d"; - private static final String MARKER_CACHE_KEY = "marker:geohash:"; - - public static String clusterCacheKey(String geohash, int level) { - return String.format(CLUSTER_CACHE_KEY, geohash, level); - } - - public static String markerCacheKey(String geohash) { - return MARKER_CACHE_KEY + geohash; - } -} diff --git a/src/main/java/com/example/log4u/domain/map/cache/dao/ClusterCacheDao.java b/src/main/java/com/example/log4u/domain/map/cache/dao/ClusterCacheDao.java index fb82464..d9bbc58 100644 --- a/src/main/java/com/example/log4u/domain/map/cache/dao/ClusterCacheDao.java +++ b/src/main/java/com/example/log4u/domain/map/cache/dao/ClusterCacheDao.java @@ -6,8 +6,8 @@ import org.springframework.stereotype.Component; +import com.example.log4u.common.executor.DistributedLockExecutor; import com.example.log4u.common.infra.cache.CacheManager; -import com.example.log4u.domain.map.cache.CacheKeyGenerator; import com.example.log4u.domain.map.cache.RedisTTLPolicy; import com.example.log4u.domain.map.dto.response.DiaryClusterResponseDto; import com.example.log4u.domain.map.exception.InvalidMapLevelException; @@ -23,13 +23,19 @@ @Slf4j public class ClusterCacheDao { + private static final String CLUSTER_CACHE_KEY = "cluster:geohash:%s:level:%d"; + public static final String CLUSTER_LOCK_KEY = "cluster-lock"; + private final CacheManager cacheManager; + private final DistributedLockExecutor distributedLockExecutor; + private final SidoAreasRepository sidoAreasRepository; private final SiggAreasRepository siggAreasRepository; public List load(String geohash, int level) { - String value = cacheManager.get(CacheKeyGenerator.clusterCacheKey(geohash, level)); + String key = String.format(CLUSTER_CACHE_KEY, geohash, level); + String value = cacheManager.get(key); if (value == null) { return null; } @@ -42,9 +48,11 @@ private List convertToClusters(String value) { } public List loadAndCache(String geohash, int level) { - List clusters = loadClustersFromDb(geohash, level); - cache(clusters, geohash, level); - return clusters; + return distributedLockExecutor.runWithLock(CLUSTER_LOCK_KEY, () -> { + List clusters = loadClustersFromDb(geohash, level); + cache(clusters, geohash, level); + return clusters; + }); } private List loadClustersFromDb(String geohash, int level) { @@ -56,17 +64,17 @@ private List loadClustersFromDb(String geohash, int lev } private void cache(List clusters, String geohash, int level) { - String key = CacheKeyGenerator.clusterCacheKey(geohash, level); + String key = String.format(CLUSTER_CACHE_KEY, geohash, level); cacheManager.cache(key, writeValueAsString(clusters), RedisTTLPolicy.CLUSTER_TTL); } public void evictSido(String geohash) { - String key = CacheKeyGenerator.clusterCacheKey(geohash, 1); + String key = String.format(CLUSTER_CACHE_KEY, geohash, 1); cacheManager.evict(key); } public void evictSigg(String geohash) { - String key = CacheKeyGenerator.clusterCacheKey(geohash, 2); + String key = String.format(CLUSTER_CACHE_KEY, geohash, 2); cacheManager.evict(key); } diff --git a/src/main/java/com/example/log4u/domain/map/cache/dao/MarkerCacheDao.java b/src/main/java/com/example/log4u/domain/map/cache/dao/MarkerCacheDao.java index c73c9f3..6647e9b 100644 --- a/src/main/java/com/example/log4u/domain/map/cache/dao/MarkerCacheDao.java +++ b/src/main/java/com/example/log4u/domain/map/cache/dao/MarkerCacheDao.java @@ -7,11 +7,11 @@ import org.springframework.stereotype.Component; +import com.example.log4u.common.executor.DistributedLockExecutor; import com.example.log4u.common.infra.cache.CacheManager; import com.example.log4u.domain.diary.entity.Diary; import com.example.log4u.domain.diary.repository.DiaryGeoHashRepository; import com.example.log4u.domain.diary.repository.DiaryRepository; -import com.example.log4u.domain.map.cache.CacheKeyGenerator; import com.example.log4u.domain.map.cache.RedisTTLPolicy; import com.example.log4u.domain.map.dto.response.DiaryMarkerResponseDto; import com.fasterxml.jackson.core.type.TypeReference; @@ -24,13 +24,19 @@ @Slf4j public class MarkerCacheDao { + private static final String MARKER_CACHE_KEY = "marker:geohash:%s"; + public static final String MARKER_LOCK_KEY = "marker-lock"; + private final CacheManager cacheManager; + private final DistributedLockExecutor distributedLockExecutor; + private final DiaryRepository diaryRepository; private final DiaryGeoHashRepository diaryGeoHashRepository; public List load(String geohash) { - String value = cacheManager.get(CacheKeyGenerator.markerCacheKey(geohash)); + String key = String.format(MARKER_CACHE_KEY, geohash); + String value = cacheManager.get(key); if (value == null) { return null; } @@ -43,9 +49,11 @@ private List convertToMarkers(String value) { } public List loadAndCache(String geohash) { - List markers = loadMarkersFromDb(geohash); - cache(markers, geohash); - return markers; + return distributedLockExecutor.runWithLock(MARKER_LOCK_KEY, () -> { + List markers = loadMarkersFromDb(geohash); + cache(markers, geohash); + return markers; + }); } private List loadMarkersFromDb(String geohash) { @@ -60,11 +68,12 @@ private List loadMarkersFromDb(String geohash) { } private void cache(List markers, String geohash) { - String key = CacheKeyGenerator.markerCacheKey(geohash); + String key = String.format(MARKER_CACHE_KEY, geohash); cacheManager.cache(key, writeValueAsString(markers), RedisTTLPolicy.MARKER_TTL); } public void evict(String geohash) { - cacheManager.evict(CacheKeyGenerator.markerCacheKey(geohash)); + String key = String.format(MARKER_CACHE_KEY, geohash); + cacheManager.evict(key); } } diff --git a/src/main/java/com/example/log4u/domain/map/exception/MaxRetryExceededException.java b/src/main/java/com/example/log4u/domain/map/exception/MaxRetryExceededException.java new file mode 100644 index 0000000..0b3ccff --- /dev/null +++ b/src/main/java/com/example/log4u/domain/map/exception/MaxRetryExceededException.java @@ -0,0 +1,7 @@ +package com.example.log4u.domain.map.exception; + +import lombok.Getter; + +@Getter +public class MaxRetryExceededException extends RuntimeException { +} diff --git a/src/main/java/com/example/log4u/domain/map/service/MapService.java b/src/main/java/com/example/log4u/domain/map/service/MapService.java index 9a13f76..ded59ce 100644 --- a/src/main/java/com/example/log4u/domain/map/service/MapService.java +++ b/src/main/java/com/example/log4u/domain/map/service/MapService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.example.log4u.common.executor.RetryExecutor; import com.example.log4u.domain.diary.entity.DiaryGeoHash; import com.example.log4u.domain.diary.service.DiaryGeohashService; import com.example.log4u.domain.map.cache.dao.ClusterCacheDao; @@ -35,37 +36,30 @@ public class MapService { private final ClusterCacheDao clusterCacheDao; private final DiaryGeohashService diaryGeohashService; - /** - * 캐싱 전략: Look-Aside + Write-Around - * [HIT] geohash -> Redis에 저장된 클러스터 배열(JSON) 읽어 반환 - * [MISS] DB에서 geohash 셀 내 시/군/구 조회하여 Redis에 배열로 저장 후 반환 - * level 기준: - * level 1: 시/도 단위 클러스터 (sido) - * level 2: 시/군/구 단위 클러스터 (sigg) - */ + private final RetryExecutor retryExecutor; + @Transactional(readOnly = true) public List getDiaryClusters(String geohash, int level) { validateGeohashLength(geohash, 3); - List clusters = clusterCacheDao.load(geohash, level); - if (clusters == null) { - clusters = clusterCacheDao.loadAndCache(geohash, level); - } - return clusters; + return retryExecutor.runWithRetry(() -> { + List clusters = clusterCacheDao.load(geohash, level); + if (clusters == null) { + clusters = clusterCacheDao.loadAndCache(geohash, level); + } + return clusters; + }); } - /** - * 캐싱 전략: Look-Aside + Write-Around - * [HIT] geohash -> Redis에 저장된 클러스터 배열(JSON) 읽어 반환 - * [MISS] DB에서 geohash 셀 내 다이어리 조회하여 Redis에 배열로 저장 후 반환 - */ @Transactional(readOnly = true) public List getDiaryMarkers(String geohash) { validateGeohashLength(geohash, 5); - List markers = markerCacheDao.load(geohash); - if (markers == null) { - markers = markerCacheDao.loadAndCache(geohash); - } - return markers; + return retryExecutor.runWithRetry(() -> { + List markers = markerCacheDao.load(geohash); + if (markers == null) { + markers = markerCacheDao.loadAndCache(geohash); + } + return markers; + }); } @Transactional diff --git a/src/main/java/com/example/log4u/domain/media/service/S3Service.java b/src/main/java/com/example/log4u/domain/media/service/S3Service.java index a1faf6d..b6d671d 100644 --- a/src/main/java/com/example/log4u/domain/media/service/S3Service.java +++ b/src/main/java/com/example/log4u/domain/media/service/S3Service.java @@ -33,10 +33,10 @@ public class S3Service { private final S3Client s3Client; private final MediaRepository mediaRepository; - @Value("${S3_BUCKET_NAME}") + @Value("${spring.aws.s3.bucket}") private String bucketName; - @Value("${AWS_REGION}") + @Value("${spring.aws.region:ap-northeast-2}") private String s3Region; /** diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 0171f62..1bd37fd 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,42 +1,44 @@ -jwt: - secret: ${JWT_SECRET:${jwt.secret}} - access-token-expire-time-seconds: ${JWT_ACCESS_TOKEN_EXPIRE_TIME_SECONDS:${jwt.access-token-expire-time-seconds}} - refresh-token-expire-time-seconds: ${JWT_REFRESH_TOKEN_EXPIRE_TIME_SECONDS:${jwt.refresh-token-expire-time-seconds}} - - -logging: - level: - org: - springframework: debug +management: + endpoints: + web: + exposure: + include: health, metrics + endpoint: + metrics: + enabled: true + metrics: + tags: + application: log4u spring: - jpa: - database-platform: org.hibernate.dialect.MySQLDialect - hibernate: - ddl-auto: update - - - properties: - hibernate: - format_sql: true - show_sql: true - datasource: - jdbc-url: ${DB_URL:jdbc:mysql://localhost:3307/log4u} - username: ${DB_USERNAME:dev} - password: ${DB_PASSWORD:devcos4-team08} + jdbc-url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver second-datasource: + jdbc-url: ${POSTGRES_DB_URL} + username: ${POSTGRES_USERNAME} + password: ${POSTGRES_PASSWORD} driver-class-name: org.postgresql.Driver - username: postgres - password: 1234 - jdbc-url: jdbc:postgresql://localhost:5432/gis_db aws: + region: ${AWS_REGION} + access-key-id: ${AWS_ACCESS_KEY_ID} + secret-access-key: ${AWS_SECRET_ACCESS_KEY} s3: - bucket: ${S3_BUCKET_NAME} # S3 버킷 이름을 GitHub Secrets에서 참조 - region: ${AWS_REGION:ap-northeast-2} # 기본값 서울 리전(ap-northeast-2) + bucket: ${S3_BUCKET_NAME} + +jwt: + secret: ${JWT_SECRET} + access-token-expire-time-seconds: ${JWT_ACCESS_TOKEN_EXPIRE_TIME_SECONDS} + refresh-token-expire-time-seconds: ${JWT_REFRESH_TOKEN_EXPIRE_TIME_SECONDS} + +logging: + level: + org: + springframework: debug security: oauth2: diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 1701600..f6ab996 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -1,15 +1,29 @@ spring: datasource: - url: jdbc:h2:mem:testdb;MODE=MySQL - driver-class-name: org.h2.Driver - username: sa - password: - - jpa: - hibernate: - ddl-auto: create - show-sql: true - properties: - hibernate: - format_sql: true - dialect: org.hibernate.dialect.H2Dialect \ No newline at end of file + jdbc-url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + second-datasource: + jdbc-url: ${POSTGRES_DB_URL} + username: ${POSTGRES_USERNAME} + password: ${POSTGRES_PASSWORD} + driver-class-name: org.postgresql.Driver + + aws: + region: ${AWS_REGION} + access-key-id: ${AWS_ACCESS_KEY_ID} + secret-access-key: ${AWS_SECRET_ACCESS_KEY} + s3: + bucket: ${S3_BUCKET_NAME} + +logging: + level: + org: + springframework: debug + +jwt: + secret: ${JWT_SECRET} + access-token-expire-time-seconds: ${JWT_ACCESS_TOKEN_EXPIRE_TIME_SECONDS} + refresh-token-expire-time-seconds: ${JWT_REFRESH_TOKEN_EXPIRE_TIME_SECONDS} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a27c8da..a79d4ee 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,5 +3,7 @@ spring: group: prod: "prod, prod-secret" dev: "dev, dev-secret" + test: "test, test-secret" + # 프로필 변경 시 사용 active: dev \ No newline at end of file diff --git a/src/test/java/com/example/log4u/domain/Map/executor/DistributedLockExecutorTest.java b/src/test/java/com/example/log4u/domain/Map/executor/DistributedLockExecutorTest.java new file mode 100644 index 0000000..a754d29 --- /dev/null +++ b/src/test/java/com/example/log4u/domain/Map/executor/DistributedLockExecutorTest.java @@ -0,0 +1,82 @@ +package com.example.log4u.domain.Map.executor; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import com.example.log4u.common.executor.DistributedLockExecutor; +import com.example.log4u.common.infra.cache.CacheManager; +import com.example.log4u.domain.map.service.MapService; + +@ActiveProfiles("test") +@SpringBootTest +class DistributedLockExecutorTest { + + @MockitoSpyBean + private DistributedLockExecutor distributedLockExecutor; + + @MockitoSpyBean + private CacheManager cacheManager; + + @Autowired + private MapService mapService; + + @BeforeEach + void setUp() { + cacheManager.init(); + when(cacheManager.tryLock(anyString())).thenReturn(true); + } + + @DisplayName("다이어리 클러스터 캐시를 로드/갱신하는 경우, DistributedLockExecutor가 호출된다.") + @Test + void distributedLockExecutorShouldBeAppliedForClusters() { + // given + String geohash = "wyd"; + int level = 1; + + // when + mapService.getDiaryClusters(geohash, level); + + // then + verify(distributedLockExecutor, atLeastOnce()) + .runWithLock(anyString(), (Supplier) any()); + } + + @DisplayName("다이어리 마커 캐시를 로드/갱신하는 경우, DistributedLockExecutor가 호출된다.") + @Test + void distributedLockExecutorShouldBeAppliedForMarkers() { + // given + String geohash = "wyd4k"; + + // when + mapService.getDiaryMarkers(geohash); + + // then + verify(distributedLockExecutor, atLeastOnce()) + .runWithLock(anyString(), (Supplier) any()); + } + + @DisplayName("DistributedLockExecutor 실행 시 분산락을 획득하고 해제한다.") + @Test + void distributedLockExecutorShouldTryLockAndRelease() { + // given + String lockKey = "lockKey"; + + // when + distributedLockExecutor.runWithLock(lockKey, () -> { + }); + + // then + verify(cacheManager).tryLock(eq(lockKey)); + verify(cacheManager).releaseLock(eq(lockKey)); + } +} diff --git a/src/test/java/com/example/log4u/domain/Map/executor/RetryExecutorTest.java b/src/test/java/com/example/log4u/domain/Map/executor/RetryExecutorTest.java new file mode 100644 index 0000000..e17ff9d --- /dev/null +++ b/src/test/java/com/example/log4u/domain/Map/executor/RetryExecutorTest.java @@ -0,0 +1,54 @@ +package com.example.log4u.domain.Map.executor; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import com.example.log4u.common.executor.RetryExecutor; +import com.example.log4u.domain.map.service.MapService; + +@ActiveProfiles("test") +@SpringBootTest +public class RetryExecutorTest { + + @MockitoSpyBean + private RetryExecutor retryExecutor; + + @Autowired + private MapService mapService; + + @DisplayName("다이어리 클러스터 조회 시 RetryExecutor가 호출된다.") + @Test + void retryShouldBeAppliedOnGetDiaryClusters() { + // given + String geohash = "wyd"; + int level = 1; + + // when + mapService.getDiaryClusters(geohash, level); + + // then + verify(retryExecutor, atLeastOnce()).runWithRetry(any()); + } + + @DisplayName("다이어리 마커 조회 시 RetryExecutor가 호출된다.") + @Test + void retryShouldBeAppliedOnGetDiaryMarkers() { + // given + String geohash = "wyd4k"; + + // when + mapService.getDiaryMarkers(geohash); + + // then + verify(retryExecutor, atLeastOnce()).runWithRetry(any()); + } +} diff --git a/src/test/java/com/example/log4u/domain/Map/service/MapServiceTest.java b/src/test/java/com/example/log4u/domain/Map/service/MapServiceTest.java index 7f335ba..1e472ba 100644 --- a/src/test/java/com/example/log4u/domain/Map/service/MapServiceTest.java +++ b/src/test/java/com/example/log4u/domain/Map/service/MapServiceTest.java @@ -8,14 +8,19 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import com.example.log4u.common.executor.RetryExecutor; import com.example.log4u.domain.diary.entity.Diary; import com.example.log4u.domain.diary.repository.DiaryRepository; import com.example.log4u.domain.diary.service.DiaryGeohashService; @@ -26,7 +31,9 @@ import com.example.log4u.domain.map.dto.response.DiaryMarkerResponseDto; import com.example.log4u.domain.map.exception.InvalidGeohashException; import com.example.log4u.domain.map.exception.InvalidMapLevelException; +import com.example.log4u.domain.map.repository.sido.SidoAreasDiaryCountRepository; import com.example.log4u.domain.map.repository.sido.SidoAreasRepository; +import com.example.log4u.domain.map.repository.sigg.SiggAreasDiaryCountRepository; import com.example.log4u.domain.map.repository.sigg.SiggAreasRepository; import com.example.log4u.domain.map.service.MapService; import com.example.log4u.fixture.DiaryFixture; @@ -34,6 +41,7 @@ @DisplayName("지도 API 단위 테스트") @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) class MapServiceTest { @InjectMocks @@ -42,23 +50,26 @@ class MapServiceTest { @Mock private SidoAreasRepository sidoAreasRepository; + @Mock + private SidoAreasDiaryCountRepository sidoAreasDiaryCountRepository; + @Mock private SiggAreasRepository siggAreasRepository; @Mock - private DiaryRepository diaryRepository; + private SiggAreasDiaryCountRepository siggAreasDiaryCountRepository; @Mock private MarkerCacheDao markerCacheDao; @Mock - private DiaryService diaryService; + private ClusterCacheDao clusterCacheDao; @Mock private DiaryGeohashService diaryGeohashService; @Mock - private ClusterCacheDao clusterCacheDao; + private RetryExecutor retryExecutor; private static final String GEOHASH_L3 = "abc"; private static final String GEOHASH_L5 = "abcde"; @@ -76,6 +87,16 @@ class MapServiceTest { ) ); + @BeforeEach + void setUp() { + given(retryExecutor.runWithRetry(any())) + .willAnswer(invocation -> { + @SuppressWarnings("unchecked") + Supplier supplier = invocation.getArgument(0); + return supplier.get(); + }); + } + @DisplayName("성공: 클러스터 캐시 HIT") @Test void getDiaryClusters_cacheHit() {