22
33import com .fasterxml .jackson .databind .ObjectMapper ;
44import jakarta .mail .MessagingException ;
5+ import jakarta .persistence .OptimisticLockException ;
56import jakarta .servlet .http .HttpServletRequest ;
67import jakarta .servlet .http .HttpServletResponse ;
78import jakarta .transaction .Transactional ;
89import lombok .RequiredArgsConstructor ;
910import lombok .extern .slf4j .Slf4j ;
1011import org .dfbf .soundlink .domain .alert .service .AlertService ;
1112import org .dfbf .soundlink .domain .blocklist .repository .BlockListRepository ;
13+ import org .dfbf .soundlink .domain .emotionRecord .dto .request .EmotionRecordUpdateRequestDTO ;
1214import org .dfbf .soundlink .domain .emotionRecord .entity .SpotifyMusic ;
1315import org .dfbf .soundlink .domain .emotionRecord .repository .EmotionRecordRepository ;
1416import org .dfbf .soundlink .domain .emotionRecord .repository .SpotifyMusicRepository ;
2931import org .springframework .beans .factory .annotation .Value ;
3032import org .springframework .data .redis .core .RedisTemplate ;
3133import org .springframework .http .ResponseCookie ;
34+ import org .springframework .retry .annotation .Backoff ;
35+ import org .springframework .retry .annotation .Recover ;
36+ import org .springframework .retry .annotation .Retryable ;
3237import org .springframework .security .crypto .bcrypt .BCryptPasswordEncoder ;
3338import org .springframework .stereotype .Service ;
3439
3540import javax .naming .AuthenticationException ;
3641import java .util .HashMap ;
42+ import java .util .List ;
3743import java .util .Map ;
3844import java .util .concurrent .TimeUnit ;
3945
@@ -95,6 +101,11 @@ public ResponseResult getUser(Long userId) {
95101
96102 // 회원정보 수정
97103 @ Transactional
104+ @ Retryable (
105+ retryFor = OptimisticLockException .class , // 낙관적 락 충돌 시 재시도
106+ maxAttempts = 3 , // 최대 3번 재시도
107+ backoff = @ Backoff (delay = 100 ) // 100ms(0.1초) 대기 후 재시도
108+ )
98109 public ResponseResult updateUser (Long userId , UserUpdateDto userUpdateDto ) {
99110 /**
100111 * orElse -> 일단 함수는 실행, 그러나 값이 null이면 orElse의 값으로 대체 (함수O, 람다x)
@@ -107,14 +118,17 @@ public ResponseResult updateUser(Long userId, UserUpdateDto userUpdateDto) {
107118 String spotifyId = userUpdateDto .spotifyId ().orElse (null );
108119
109120 if (spotifyId != null && !spotifyId .equals ("-1" )) {
110- // SpotifyMusic 객체 찾기 (없으면 새로 생성 & 저장)
111- SpotifyMusic spotifyMusic = spotifyMusicRepository .findBySpotifyId (spotifyId )
112- .orElseGet (() -> {
113- SpotifyMusic sm = new SpotifyMusic (userUpdateDto );
114- spotifyMusicRepository .save (sm );
115- return sm ;
116- });
117- user .update (userUpdateDto , passwordEncoder , spotifyMusic );
121+ // SpotifyMusic 객체 찾기
122+ List <SpotifyMusic > spotifyMusicList = spotifyMusicRepository .findListBySpotifyId (spotifyId );
123+
124+ // (없으면 새로 생성 & 저장)
125+ if (spotifyMusicList .isEmpty ()) {
126+ SpotifyMusic sm = new SpotifyMusic (userUpdateDto );
127+ spotifyMusicRepository .save (sm );
128+ user .update (userUpdateDto , passwordEncoder , sm );
129+ }
130+
131+ user .update (userUpdateDto , passwordEncoder , spotifyMusicList .get (0 ));
118132 } else {
119133 if ("-1" .equals (spotifyId )) { user .getProfileMusic ().deleteSpotifyId (); }
120134 user .update (userUpdateDto , passwordEncoder );
@@ -131,6 +145,15 @@ public ResponseResult updateUser(Long userId, UserUpdateDto userUpdateDto) {
131145 }
132146 }
133147
148+ // 낙관적 락 재시도 실패 시 실행되는 메서드
149+ // Recover 어노테이션에 의해 실패 시, 자동 호출됨
150+ @ Recover
151+ public ResponseResult recoverFromOptimisticLock (OptimisticLockException e , Long userId , UserUpdateDto userUpdateDto ) {
152+ log .error ("감정 기록 업데이트 중 동시성 충돌 발생. spotifyId: {}, error: {}" , userUpdateDto .spotifyId (), e .getMessage ());
153+
154+ return new ResponseResult (ErrorCode .CONCURRENCY_ERROR , e .getMessage ());
155+ }
156+
134157 // 회원정보 삭제
135158 @ Transactional
136159 public ResponseResult deleteUser (Long userId ) {
0 commit comments