낙관적 락

dev_archive_v·2026년 4월 26일

스프링 부트

목록 보기
22/22

테스트 코드

@Test
    @DisplayName("낙관적 락 테스트")
    public void optimistic_test()throws InterruptedException{
        //given
        int threadCount = 20;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);
        System.out.println("초기 좋아요 수:"+playlistArticleRepository.findById(articleId).orElseThrow().getLikeCnt());
        for(int i=0;i<threadCount;i++){
            final Member member = members.get(i);
            executorService.submit(()->{

                try{
                    playListArticleService.decreaseLike(articleId,member.getEmail());
                    System.out.println("좋아요 취소: "+member.getEmail());
                } catch(Exception e){
                    System.out.println("감소 실패 에러: "+e.getMessage());

                }finally {
                    latch.countDown();
                }
            });
        }

        latch.await();


        //then
        PlayListArticle playListArticle = playlistArticleRepository.findById(articleId).orElseThrow();
        assertThat(playListArticle.getLikeCnt()).isEqualTo(0);
        assertThat(playListArticle.getLikeList()).isNull();

    }

결과

초기 좋아요 수:20
좋아요 취소: user11@gmail.com
좋아요 취소: user12@gmail.com
감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]
....
감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]
감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]

감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]
감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]

감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]
감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]

감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]
감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]

감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]
감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]

감소 실패 에러: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.FifthSpring.model.PlayListArticle#13]


org.opentest4j.AssertionFailedError: 
expected: 0
 but was: 18
Expected :0
Actual   :18
  1. 낙관적 락을 적용했을 때 왜 두명의 사람의 좋아요만 취소된걸까?
  2. 왜 감소 실패 에러가 18번이나 발생했을까?

좋아요 취소에 성공한 쿼리부터 살펴보자.

    update
        play_list_article 
    set
        address=?,
        created=?,
        latitude=?,
        like_cnt=?,
        longitude=?,
        member_id=?,
        updated=?,
        version=?,
        view_cnt=? 
    where
        id=? 
        and version=?
  • 버전이 포함되어 있어서 낙관적 락이 적용되었음을 알 수 있다.

좋아요 취소에 실패했을 때의 예외메시지를 다시 살펴보자면
Row was updated or deleted by another transaction 즉 또 다른 transaction의 변경 및 삭제로 인해 18개의 요청이 유실되었음을 알 수 있다.
낙관적 락은 이렇듯 예외를 catch해서 다시 retry를 할 수 있다.

낙관적 락에서의 예외 처리 적용(Retryable)

  1. spring-retry 의존성을 추가한다.
  2. config에 @EnableRetry 적용
  3. 특정 메소드에 @Retryable 어노테이션으로 자동으로 재시도하는 로직을 구현
    • 기존 코드
     @Transactional(propagation = Propagation.REQUIRES_NEW)
      @Retryable(value={ObjectOptimisticLockingFailureException.class}
    , maxAttempts = 100,backoff = @Backoff(delay=50))
    public PlayListDto decreaseLike(Long id,String email) {


        /* 낙관적 락 */
        PlayListArticle targetPost = playlistArticleRepository.findByIdWithOptimisticLock(id).orElseThrow();

        if (targetPost.getLikeCnt() <=0 )
        {
            throw new IllegalArgumentException("likeCnt는 0 이하가 될 수 없습니다");
        }
        targetPost.decreaseLike(); //엔티티 내부에서 likeCnt 처리
        Member member = memberRepository.findByEmail(email).orElseThrow();
        likeRepository.deleteByPlayListArticleIdAndMember(id,member.getId()); // member엔티티 변경


        return mapToPlayListDto(targetPost);

    }

결과

0개의 댓글