Skip to content

Commit 05750d4

Browse files
authored
Merge pull request #70 from prgrms-web-devcourse-final-project/feature/cluster-marker-lock-retry
지도 클러스터/마커형 조회 기능 Redis 분산락 & Retry 적용
2 parents d370f35 + f067d23 commit 05750d4

File tree

20 files changed

+376
-124
lines changed

20 files changed

+376
-124
lines changed

.github/workflows/dev-build.yml

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,29 +35,13 @@ jobs:
3535
key: ${{ runner.os }}-sonar
3636
restore-keys: ${{ runner.os }}-sonar
3737

38-
# - name: S3에서 init.sql 다운로드
39-
# env:
40-
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
41-
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
42-
# AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
43-
# run: |
44-
# mkdir -p docker
45-
# aws s3 cp s3://devcos4-team08-bucket/db/init.sql ./docker/init.sql
46-
47-
4838
- name: Docker Compose 설치
4939
run: |
5040
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
5141
sudo chmod +x /usr/local/bin/docker-compose
5242
53-
- name: CI용 빈 init.sql 생성
54-
run: |
55-
mkdir -p docker
56-
touch docker/init.sql
57-
5843
- name: Docker Compose 실행
5944
run: |
60-
chmod -R 755 ./docker
6145
docker-compose -f docker-compose.yml up -d
6246
6347
- name: 컨테이너 실행 대기
@@ -117,7 +101,7 @@ jobs:
117101
run: |
118102
chmod +x ./gradlew
119103
# 소나클라우드 임시 비활성화 ./gradlew build jacocoTestReport sonar --info -Pprofile=dev -Dsonar.branch.name=${{ github.ref_name }}
120-
./gradlew build -i jacocoTestReport -Pprofile=dev
104+
./gradlew build -i jacocoTestReport -Pprofile=test
121105
122106
123107
- name: Docker Compose 종료

docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ services:
1010
- "5432:5432"
1111
volumes:
1212
- pgdata:/var/lib/postgresql/data
13-
- ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql
1413
networks:
1514
- log4u-net
1615

src/main/java/com/example/log4u/common/config/PostgreSqlConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public LocalContainerEntityManagerFactoryBean postgresqlEntityManagerFactory() {
4444

4545
Map<String, Object> properties = new HashMap<>();
4646
properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
47-
properties.put("hibernate.hbm2ddl.auto", "none");
47+
properties.put("hibernate.hbm2ddl.auto", "update");
4848
properties.put("hibernate.format_sql", true);
4949
em.setJpaPropertyMap(properties);
5050

src/main/java/com/example/log4u/common/config/S3Config.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
@Configuration
1414
public class S3Config {
1515

16-
@Value("${AWS_ACCESS_KEY_ID}")
16+
@Value("${spring.aws.access-key-id}")
1717
private String accessKey;
1818

19-
@Value("${AWS_SECRET_ACCESS_KEY}")
19+
@Value("${spring.aws.secret-access-key}")
2020
private String secretKey;
2121

22-
@Value("${AWS_REGION:ap-northeast-2}")
22+
@Value("${spring.aws.region:ap-northeast-2}")
2323
private String region;
2424

2525
@Bean
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.example.log4u.common.executor;
2+
3+
import java.util.function.Supplier;
4+
5+
import org.springframework.stereotype.Component;
6+
7+
import com.example.log4u.common.infra.cache.CacheManager;
8+
9+
import lombok.RequiredArgsConstructor;
10+
11+
@Component
12+
@RequiredArgsConstructor
13+
public class DistributedLockExecutor {
14+
15+
private final CacheManager cacheManager;
16+
17+
public void runWithLock(String lockKey, Runnable task) {
18+
runWithLock(lockKey, () -> task);
19+
}
20+
21+
public <T> T runWithLock(String lockKey, Supplier<T> task) {
22+
if (!cacheManager.tryLock(lockKey)) {
23+
return null;
24+
}
25+
try {
26+
return task.get();
27+
} finally {
28+
cacheManager.releaseLock(lockKey);
29+
}
30+
}
31+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.example.log4u.common.executor;
2+
3+
import java.util.concurrent.TimeUnit;
4+
import java.util.function.Supplier;
5+
6+
import org.springframework.stereotype.Component;
7+
8+
import com.example.log4u.domain.map.exception.MaxRetryExceededException;
9+
10+
@Component
11+
public class RetryExecutor {
12+
13+
int MAX_RETRY_CNT = 10;
14+
int INITIAL_DELAY_MSEC = 1_000;
15+
int MAX_DELAY_MSEC = 100_000;
16+
17+
public <T> T runWithRetry(Supplier<T> task) {
18+
for (int attempt = 0; attempt < MAX_RETRY_CNT; attempt++) {
19+
T result = task.get();
20+
if (result != null) {
21+
return result;
22+
}
23+
backoff(attempt);
24+
}
25+
throw new MaxRetryExceededException();
26+
}
27+
28+
private void backoff(int attempt) {
29+
try {
30+
int delay = Math.min(
31+
INITIAL_DELAY_MSEC * (int) Math.pow(2, attempt - 1),
32+
MAX_DELAY_MSEC);
33+
34+
TimeUnit.MILLISECONDS.sleep(delay);
35+
} catch (InterruptedException e) {
36+
Thread.currentThread().interrupt();
37+
}
38+
}
39+
}

src/main/java/com/example/log4u/common/infra/cache/CacheManager.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44

55
public interface CacheManager {
66

7-
void cache(String key, String value, Duration ttl);
7+
void init();
8+
9+
void cache(String key, String value, Duration ttl);
810

911
String get(String key);
1012

1113
void evict(String key);
14+
15+
Boolean tryLock(String key);
16+
17+
void releaseLock(String key);
1218
}

src/main/java/com/example/log4u/common/infra/cache/RedisCacheManagerImpl.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@
1414
@RequiredArgsConstructor
1515
public class RedisCacheManagerImpl implements CacheManager {
1616

17-
private final RedisTemplate<String, String> redisTemplate;
17+
private static final String LOCK_VALUE = "locked";
18+
19+
private final RedisTemplate<String, String> redisTemplate;
20+
21+
public void init() {
22+
Objects.requireNonNull(redisTemplate.getConnectionFactory())
23+
.getConnection().flushAll();
24+
}
1825

1926
public void cache(String key, String value, Duration ttl) {
2027
redisTemplate.opsForValue().set(key, value, ttl);
@@ -27,4 +34,12 @@ public String get(String key) {
2734
public void evict(String key) {
2835
redisTemplate.delete(key);
2936
}
37+
38+
public Boolean tryLock(String key) {
39+
return redisTemplate.opsForValue().setIfAbsent(key, LOCK_VALUE);
40+
}
41+
42+
public void releaseLock(String key) {
43+
redisTemplate.delete(key);
44+
}
3045
}

src/main/java/com/example/log4u/domain/map/cache/CacheKeyGenerator.java

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/main/java/com/example/log4u/domain/map/cache/dao/ClusterCacheDao.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
import org.springframework.stereotype.Component;
88

9+
import com.example.log4u.common.executor.DistributedLockExecutor;
910
import com.example.log4u.common.infra.cache.CacheManager;
10-
import com.example.log4u.domain.map.cache.CacheKeyGenerator;
1111
import com.example.log4u.domain.map.cache.RedisTTLPolicy;
1212
import com.example.log4u.domain.map.dto.response.DiaryClusterResponseDto;
1313
import com.example.log4u.domain.map.exception.InvalidMapLevelException;
@@ -23,13 +23,19 @@
2323
@Slf4j
2424
public class ClusterCacheDao {
2525

26+
private static final String CLUSTER_CACHE_KEY = "cluster:geohash:%s:level:%d";
27+
public static final String CLUSTER_LOCK_KEY = "cluster-lock";
28+
2629
private final CacheManager cacheManager;
2730

31+
private final DistributedLockExecutor distributedLockExecutor;
32+
2833
private final SidoAreasRepository sidoAreasRepository;
2934
private final SiggAreasRepository siggAreasRepository;
3035

3136
public List<DiaryClusterResponseDto> load(String geohash, int level) {
32-
String value = cacheManager.get(CacheKeyGenerator.clusterCacheKey(geohash, level));
37+
String key = String.format(CLUSTER_CACHE_KEY, geohash, level);
38+
String value = cacheManager.get(key);
3339
if (value == null) {
3440
return null;
3541
}
@@ -42,9 +48,11 @@ private List<DiaryClusterResponseDto> convertToClusters(String value) {
4248
}
4349

4450
public List<DiaryClusterResponseDto> loadAndCache(String geohash, int level) {
45-
List<DiaryClusterResponseDto> clusters = loadClustersFromDb(geohash, level);
46-
cache(clusters, geohash, level);
47-
return clusters;
51+
return distributedLockExecutor.runWithLock(CLUSTER_LOCK_KEY, () -> {
52+
List<DiaryClusterResponseDto> clusters = loadClustersFromDb(geohash, level);
53+
cache(clusters, geohash, level);
54+
return clusters;
55+
});
4856
}
4957

5058
private List<DiaryClusterResponseDto> loadClustersFromDb(String geohash, int level) {
@@ -56,17 +64,17 @@ private List<DiaryClusterResponseDto> loadClustersFromDb(String geohash, int lev
5664
}
5765

5866
private void cache(List<DiaryClusterResponseDto> clusters, String geohash, int level) {
59-
String key = CacheKeyGenerator.clusterCacheKey(geohash, level);
67+
String key = String.format(CLUSTER_CACHE_KEY, geohash, level);
6068
cacheManager.cache(key, writeValueAsString(clusters), RedisTTLPolicy.CLUSTER_TTL);
6169
}
6270

6371
public void evictSido(String geohash) {
64-
String key = CacheKeyGenerator.clusterCacheKey(geohash, 1);
72+
String key = String.format(CLUSTER_CACHE_KEY, geohash, 1);
6573
cacheManager.evict(key);
6674
}
6775

6876
public void evictSigg(String geohash) {
69-
String key = CacheKeyGenerator.clusterCacheKey(geohash, 2);
77+
String key = String.format(CLUSTER_CACHE_KEY, geohash, 2);
7078
cacheManager.evict(key);
7179
}
7280

0 commit comments

Comments
 (0)