Skip to content

Commit e326ec6

Browse files
committed
feat: Spotify AccessToken 자동 재발급 로직 추가
1 parent 0cbe505 commit e326ec6

2 files changed

Lines changed: 143 additions & 8 deletions

File tree

src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/spotifyService/SpotifyRateLimitHandler.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package com.back.web7_9_codecrete_be.domain.artists.service.spotifyService;
22

3+
import com.back.web7_9_codecrete_be.global.spotify.SpotifyClient;
4+
import lombok.RequiredArgsConstructor;
35
import lombok.extern.slf4j.Slf4j;
46
import org.springframework.stereotype.Component;
57

68
// 429 에러 처리 및 전역 쿨다운 관리
9+
// 401 에러 처리 (토큰 만료 시 자동 재발급)
710

811
@Slf4j
912
@Component
13+
@RequiredArgsConstructor
1014
public class SpotifyRateLimitHandler {
1115

16+
private final SpotifyClient spotifyClient;
17+
1218
private static final long SPOTIFY_RATE_LIMIT_INTERVAL_MS = 500; // 초당 2회
1319
private static final long MUSICBRAINZ_RATE_LIMIT_INTERVAL_MS = 1000; // 초당 1회
1420
private static final long GLOBAL_COOLDOWN_DURATION_MS = 60_000; // 60초
@@ -65,6 +71,22 @@ public <T> T callWithRateLimitRetry(java.util.function.Supplier<T> supplier, Str
6571
globalConsecutive429Count = 0;
6672
return result;
6773
} catch (Exception e) {
74+
// 401 에러 확인 (Unauthorized - 토큰 만료)
75+
boolean is401 = is401Error(e);
76+
77+
if (is401) {
78+
log.warn("401 Unauthorized 에러 감지 - 토큰 재발급 후 재시도: attempt={}/{}", attempt, maxRetry);
79+
// 토큰 강제 재발급
80+
spotifyClient.forceRefreshToken();
81+
82+
if (attempt < maxRetry) {
83+
// 재시도
84+
continue;
85+
} else {
86+
log.error("401 에러 재시도 횟수 초과 ({}회)", maxRetry);
87+
throw new RuntimeException(context + ": 401 Unauthorized - 토큰 재발급 후에도 실패", e);
88+
}
89+
}
6890
// 원본 예외 확인 (래핑된 경우 cause 확인)
6991
Throwable originalException = e;
7092
Throwable current = e;
@@ -203,5 +225,28 @@ public <T> T callWithRateLimitRetry(java.util.function.Supplier<T> supplier, Str
203225

204226
throw new RuntimeException(context + ": rate limit retry exhausted");
205227
}
228+
229+
/**
230+
* 401 Unauthorized 에러인지 확인
231+
*/
232+
private boolean is401Error(Throwable e) {
233+
Throwable current = e;
234+
while (current != null) {
235+
String className = current.getClass().getSimpleName();
236+
String errorMsg = current.getMessage();
237+
238+
// 401 에러 확인
239+
if (className.contains("Unauthorized") ||
240+
className.contains("401") ||
241+
(errorMsg != null && (errorMsg.contains("401") ||
242+
errorMsg.contains("Unauthorized") ||
243+
errorMsg.contains("Invalid access token")))) {
244+
return true;
245+
}
246+
247+
current = current.getCause();
248+
}
249+
return false;
250+
}
206251
}
207252

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,122 @@
11
package com.back.web7_9_codecrete_be.global.spotify;
22

33
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
45
import org.springframework.stereotype.Component;
56
import se.michaelthelin.spotify.SpotifyApi;
67
import se.michaelthelin.spotify.model_objects.credentials.ClientCredentials;
78
import se.michaelthelin.spotify.requests.authorization.client_credentials.ClientCredentialsRequest;
89

10+
import java.util.concurrent.locks.ReentrantLock;
11+
12+
/**
13+
* Spotify AccessToken 중앙 관리 클래스
14+
*
15+
* - 토큰 만료 시간 관리 (1시간 유효)
16+
* - 만료 전 자동 재발급 (5분 여유)
17+
* - 401 에러 발생 시 자동 재발급 및 재요청 지원
18+
*/
19+
@Slf4j
920
@Component
1021
@RequiredArgsConstructor
1122
public class SpotifyClient {
1223

1324
private final SpotifyApi spotifyApi;
14-
15-
public String getAccessToken() {
25+
26+
// 토큰 관리 필드
27+
private volatile String cachedAccessToken;
28+
private volatile long tokenExpiresAt = 0; // 만료 시각 (밀리초)
29+
private final ReentrantLock tokenLock = new ReentrantLock();
30+
31+
// 토큰 만료 여유 시간 (5분 = 300초)
32+
private static final long TOKEN_BUFFER_SECONDS = 300;
33+
private static final long TOKEN_BUFFER_MS = TOKEN_BUFFER_SECONDS * 1000;
34+
35+
/**
36+
* AccessToken 발급 (내부 메서드)
37+
* 동시성 제어를 통해 중복 발급 방지
38+
*/
39+
private String refreshAccessToken() {
40+
tokenLock.lock();
1641
try {
42+
// Double-check: 다른 스레드가 이미 토큰을 발급했는지 확인
43+
if (cachedAccessToken != null && System.currentTimeMillis() < tokenExpiresAt) {
44+
return cachedAccessToken;
45+
}
46+
47+
log.info("Spotify AccessToken 발급 시작");
1748
ClientCredentialsRequest request = spotifyApi.clientCredentials().build();
1849
ClientCredentials credentials = request.execute();
19-
spotifyApi.setAccessToken(credentials.getAccessToken());
20-
return credentials.getAccessToken();
50+
51+
String newToken = credentials.getAccessToken();
52+
// Spotify 토큰은 3600초(1시간) 유효
53+
// 여유 시간을 빼서 실제 만료 시각 계산
54+
long expiresInSeconds = credentials.getExpiresIn();
55+
tokenExpiresAt = System.currentTimeMillis() + (expiresInSeconds * 1000) - TOKEN_BUFFER_MS;
56+
57+
cachedAccessToken = newToken;
58+
spotifyApi.setAccessToken(newToken);
59+
60+
log.info("Spotify AccessToken 발급 완료. 만료 시각: {} ({}초 후)",
61+
new java.util.Date(tokenExpiresAt), expiresInSeconds);
62+
63+
return newToken;
2164
} catch (Exception e) {
65+
log.error("Spotify 토큰 발급 실패", e);
66+
// 토큰 발급 실패 시 캐시 초기화
67+
cachedAccessToken = null;
68+
tokenExpiresAt = 0;
2269
throw new RuntimeException("Spotify 토큰 발급 실패", e);
70+
} finally {
71+
tokenLock.unlock();
2372
}
2473
}
25-
26-
public SpotifyApi getAuthorizedApi() {
27-
if (spotifyApi.getAccessToken() == null) {
28-
getAccessToken();
74+
75+
/**
76+
* AccessToken 조회 (만료 체크 포함)
77+
* 만료되었거나 곧 만료될 경우 자동 재발급
78+
*/
79+
public String getAccessToken() {
80+
long now = System.currentTimeMillis();
81+
82+
// 토큰이 없거나 만료되었거나 곧 만료될 경우 재발급
83+
if (cachedAccessToken == null || now >= tokenExpiresAt) {
84+
return refreshAccessToken();
2985
}
86+
87+
return cachedAccessToken;
88+
}
89+
90+
/**
91+
* 인증된 SpotifyApi 반환
92+
* 토큰이 없거나 만료된 경우 자동으로 재발급
93+
*/
94+
public SpotifyApi getAuthorizedApi() {
95+
getAccessToken(); // 만료 체크 및 필요시 재발급
3096
return spotifyApi;
3197
}
98+
99+
/**
100+
* 401 에러 발생 시 토큰 강제 재발급
101+
* 이 메서드를 호출한 후 API를 재요청해야 함
102+
*/
103+
public void forceRefreshToken() {
104+
log.warn("401 에러 감지 - Spotify AccessToken 강제 재발급");
105+
tokenLock.lock();
106+
try {
107+
// 캐시 초기화 후 재발급
108+
cachedAccessToken = null;
109+
tokenExpiresAt = 0;
110+
refreshAccessToken();
111+
} finally {
112+
tokenLock.unlock();
113+
}
114+
}
115+
116+
/**
117+
* 토큰 만료 여부 확인
118+
*/
119+
public boolean isTokenExpired() {
120+
return cachedAccessToken == null || System.currentTimeMillis() >= tokenExpiresAt;
121+
}
32122
}

0 commit comments

Comments
 (0)