Skip to content

Commit 2a96c93

Browse files
committed
feat: Redis 테스트 코드
1 parent b6af475 commit 2a96c93

2 files changed

Lines changed: 499 additions & 62 deletions

File tree

src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/spotify/cache/SpotifyCacheService.java

Lines changed: 61 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import org.springframework.stereotype.Service;
99

1010
import java.util.concurrent.TimeUnit;
11-
11+
import java.util.function.Supplier;
1212

1313
@Slf4j
1414
@Service
@@ -22,34 +22,33 @@ public class SpotifyCacheService {
2222

2323
private static final String CACHE_KEY_PREFIX = "artist:detail:spotify:";
2424
private static final String LOCK_KEY_PREFIX = "artist:detail:spotify:lock:";
25-
private static final long CACHE_TTL_SECONDS = 3600; // 1시간 (기본값, 추후 3~6시간 조정 가능)
26-
private static final long LOCK_TTL_SECONDS = 30; // 락 TTL: 30초 (API 호출 완료 대기 시간)
25+
private static final long CACHE_TTL_SECONDS = 3600; // 1시간
26+
private static final long LOCK_TTL_SECONDS = 30; // 30초
27+
28+
private static final long RETRY_SLEEP_MS = 100;
29+
private static final int MAX_RETRIES = 30; // 100ms * 30 = 3초
2730

28-
// Redis 캐시에서 Spotify 상세 정보 조회
2931
public SpotifyArtistDetailCache getCached(String spotifyArtistId) {
3032
try {
3133
String cacheKey = getCacheKey(spotifyArtistId);
3234
Object cached = objectRedisTemplate.opsForValue().get(cacheKey);
3335

34-
if (cached == null) {
35-
return null;
36-
}
36+
if (cached == null) return null;
3737

38-
// Object를 SpotifyArtistDetailCache로 변환
3938
if (cached instanceof SpotifyArtistDetailCache) {
4039
return (SpotifyArtistDetailCache) cached;
4140
}
4241

43-
// LinkedHashMap 등으로 역직렬화된 경우 ObjectMapper로 변환
4442
return objectMapper.convertValue(cached, SpotifyArtistDetailCache.class);
4543
} catch (Exception e) {
4644
log.warn("Redis 캐시 조회 실패: spotifyArtistId={}", spotifyArtistId, e);
4745
return null;
4846
}
4947
}
5048

51-
// Redis 캐시에 Spotify 상세 정보 저장
5249
public void save(String spotifyArtistId, SpotifyArtistDetailCache data) {
50+
if (data == null) return; // (선택) supplier가 null 반환 시 방어
51+
5352
try {
5453
String cacheKey = getCacheKey(spotifyArtistId);
5554
objectRedisTemplate.opsForValue().set(
@@ -61,88 +60,89 @@ public void save(String spotifyArtistId, SpotifyArtistDetailCache data) {
6160
log.debug("Spotify 상세 정보 캐시 저장: spotifyArtistId={}, ttl={}초", spotifyArtistId, CACHE_TTL_SECONDS);
6261
} catch (Exception e) {
6362
log.warn("Redis 캐시 저장 실패: spotifyArtistId={}", spotifyArtistId, e);
64-
// 캐시 저장 실패해도 API 호출은 성공했으므로 계속 진행
6563
}
6664
}
6765

68-
// 캐시 스탬피드 방지: Redis 락을 사용하여 동시 API 호출 제한
6966
public SpotifyArtistDetailCache getOrFetchWithLock(
7067
String spotifyArtistId,
71-
java.util.function.Supplier<SpotifyArtistDetailCache> apiCallSupplier
68+
Supplier<SpotifyArtistDetailCache> apiCallSupplier
7269
) {
7370
String lockKey = getLockKey(spotifyArtistId);
7471

75-
// 락 획득 시도 (SETNX 방식)
76-
Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(
77-
lockKey,
78-
"locked",
79-
LOCK_TTL_SECONDS,
80-
TimeUnit.SECONDS
81-
);
72+
// ✅ 핵심 수정 1) 락 획득 자체가 예외면 fallback
73+
final Boolean lockAcquired;
74+
try {
75+
lockAcquired = redisTemplate.opsForValue().setIfAbsent(
76+
lockKey,
77+
"locked",
78+
LOCK_TTL_SECONDS,
79+
TimeUnit.SECONDS
80+
);
81+
} catch (Exception e) {
82+
log.warn("Redis 락 획득 실패(예외) → 직접 API 호출로 fallback: spotifyArtistId={}", spotifyArtistId, e);
83+
SpotifyArtistDetailCache spotifyData = apiCallSupplier.get();
84+
save(spotifyArtistId, spotifyData);
85+
return spotifyData;
86+
}
8287

8388
if (Boolean.TRUE.equals(lockAcquired)) {
84-
// 락 획득 성공: 이 스레드가 API 호출 담당
8589
try {
8690
log.debug("Spotify API 호출 락 획득: spotifyArtistId={}", spotifyArtistId);
8791

88-
// 다시 한 번 캐시 확인 (락 획득 대기 중 다른 스레드가 저장했을 수 있음)
92+
// Double-check
8993
SpotifyArtistDetailCache doubleCheck = getCached(spotifyArtistId);
9094
if (doubleCheck != null) {
9195
log.debug("락 획득 후 캐시 재조회 HIT: spotifyArtistId={}", spotifyArtistId);
9296
return doubleCheck;
9397
}
9498

95-
// Spotify API 호출
9699
SpotifyArtistDetailCache spotifyData = apiCallSupplier.get();
97-
98-
// 캐시에 저장
99100
save(spotifyArtistId, spotifyData);
100-
101101
return spotifyData;
102+
102103
} finally {
103-
// 락 해제
104-
redisTemplate.delete(lockKey);
104+
// ✅ 핵심 수정 2) 락 해제도 예외 방어
105+
try {
106+
redisTemplate.delete(lockKey);
107+
} catch (Exception e) {
108+
log.warn("Redis 락 해제 실패: lockKey={}", lockKey, e);
109+
}
105110
}
106-
} else {
107-
// 락 획득 실패: 다른 스레드가 API 호출 중
108-
log.debug("Spotify API 호출 락 획득 실패 (다른 스레드가 처리 중): spotifyArtistId={}", spotifyArtistId);
111+
}
109112

110-
// 짧은 대기 후 캐시 재조회 (다른 스레드가 저장 완료했을 수 있음)
111-
try {
112-
Thread.sleep(100); // 100ms 대기
113-
} catch (InterruptedException e) {
114-
Thread.currentThread().interrupt();
115-
}
113+
// 락 획득 실패: 다른 스레드가 API 호출 중
114+
log.debug("Spotify API 호출 락 획득 실패 (다른 스레드가 처리 중): spotifyArtistId={}", spotifyArtistId);
115+
116+
sleepQuietly(RETRY_SLEEP_MS);
117+
118+
SpotifyArtistDetailCache retryCache = getCached(spotifyArtistId);
119+
if (retryCache != null) {
120+
log.debug("락 대기 후 캐시 재조회 HIT: spotifyArtistId={}", spotifyArtistId);
121+
return retryCache;
122+
}
116123

117-
// 캐시 재조회
118-
SpotifyArtistDetailCache retryCache = getCached(spotifyArtistId);
124+
for (int i = 0; i < MAX_RETRIES; i++) {
125+
sleepQuietly(RETRY_SLEEP_MS);
126+
127+
retryCache = getCached(spotifyArtistId);
119128
if (retryCache != null) {
120-
log.debug("락 대기 캐시 재조회 HIT: spotifyArtistId={}", spotifyArtistId);
129+
log.debug("락 대기 캐시 재조회 HIT ({}ms 후): spotifyArtistId={}", (i + 1) * RETRY_SLEEP_MS, spotifyArtistId);
121130
return retryCache;
122131
}
132+
}
123133

124-
// 여전히 캐시가 없으면 최대 3초까지 대기하며 재시도
125-
int maxRetries = 30; // 100ms * 30 = 3초
126-
for (int i = 0; i < maxRetries; i++) {
127-
try {
128-
Thread.sleep(100);
129-
} catch (InterruptedException e) {
130-
Thread.currentThread().interrupt();
131-
break;
132-
}
133-
134-
retryCache = getCached(spotifyArtistId);
135-
if (retryCache != null) {
136-
log.debug("락 대기 중 캐시 재조회 HIT ({}ms 후): spotifyArtistId={}", (i + 1) * 100, spotifyArtistId);
137-
return retryCache;
138-
}
139-
}
134+
// 최종 fallback
135+
log.warn("락 대기 후에도 캐시 없음, 직접 API 호출: spotifyArtistId={}", spotifyArtistId);
136+
SpotifyArtistDetailCache spotifyData = apiCallSupplier.get();
137+
save(spotifyArtistId, spotifyData);
138+
return spotifyData;
139+
}
140140

141-
// 최종적으로도 캐시가 없으면 직접 API 호출 (락이 만료되었을 수 있음)
142-
log.warn("락 대기 후에도 캐시 없음, 직접 API 호출: spotifyArtistId={}", spotifyArtistId);
143-
SpotifyArtistDetailCache spotifyData = apiCallSupplier.get();
144-
save(spotifyArtistId, spotifyData);
145-
return spotifyData;
141+
private void sleepQuietly(long millis) {
142+
try {
143+
Thread.sleep(millis);
144+
} catch (InterruptedException e) {
145+
Thread.currentThread().interrupt();
146146
}
147147
}
148148

@@ -154,4 +154,3 @@ private String getLockKey(String spotifyArtistId) {
154154
return LOCK_KEY_PREFIX + spotifyArtistId;
155155
}
156156
}
157-

0 commit comments

Comments
 (0)