|
1 | 1 | package com.back.web7_9_codecrete_be.global.spotify; |
2 | 2 |
|
3 | 3 | import lombok.RequiredArgsConstructor; |
| 4 | +import lombok.extern.slf4j.Slf4j; |
4 | 5 | import org.springframework.stereotype.Component; |
5 | 6 | import se.michaelthelin.spotify.SpotifyApi; |
6 | 7 | import se.michaelthelin.spotify.model_objects.credentials.ClientCredentials; |
7 | 8 | import se.michaelthelin.spotify.requests.authorization.client_credentials.ClientCredentialsRequest; |
8 | 9 |
|
| 10 | +import java.util.concurrent.locks.ReentrantLock; |
| 11 | + |
| 12 | +/** |
| 13 | + * Spotify AccessToken 중앙 관리 클래스 |
| 14 | + * |
| 15 | + * - 토큰 만료 시간 관리 (1시간 유효) |
| 16 | + * - 만료 전 자동 재발급 (5분 여유) |
| 17 | + * - 401 에러 발생 시 자동 재발급 및 재요청 지원 |
| 18 | + */ |
| 19 | +@Slf4j |
9 | 20 | @Component |
10 | 21 | @RequiredArgsConstructor |
11 | 22 | public class SpotifyClient { |
12 | 23 |
|
13 | 24 | 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(); |
16 | 41 | try { |
| 42 | + // Double-check: 다른 스레드가 이미 토큰을 발급했는지 확인 |
| 43 | + if (cachedAccessToken != null && System.currentTimeMillis() < tokenExpiresAt) { |
| 44 | + return cachedAccessToken; |
| 45 | + } |
| 46 | + |
| 47 | + log.info("Spotify AccessToken 발급 시작"); |
17 | 48 | ClientCredentialsRequest request = spotifyApi.clientCredentials().build(); |
18 | 49 | 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; |
21 | 64 | } catch (Exception e) { |
| 65 | + log.error("Spotify 토큰 발급 실패", e); |
| 66 | + // 토큰 발급 실패 시 캐시 초기화 |
| 67 | + cachedAccessToken = null; |
| 68 | + tokenExpiresAt = 0; |
22 | 69 | throw new RuntimeException("Spotify 토큰 발급 실패", e); |
| 70 | + } finally { |
| 71 | + tokenLock.unlock(); |
23 | 72 | } |
24 | 73 | } |
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(); |
29 | 85 | } |
| 86 | + |
| 87 | + return cachedAccessToken; |
| 88 | + } |
| 89 | + |
| 90 | + /** |
| 91 | + * 인증된 SpotifyApi 반환 |
| 92 | + * 토큰이 없거나 만료된 경우 자동으로 재발급 |
| 93 | + */ |
| 94 | + public SpotifyApi getAuthorizedApi() { |
| 95 | + getAccessToken(); // 만료 체크 및 필요시 재발급 |
30 | 96 | return spotifyApi; |
31 | 97 | } |
| 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 | + } |
32 | 122 | } |
0 commit comments