• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    미해결

낙관적락 테스트 코드 작성시 질문입니다.

23.05.27 16:38 작성 조회수 536

1

안녕하세요. 항상 강의 잘 듣고 있습니다.

낙관적 락에 대해서 직접 테스트 코드를 통해 동작 방식을 살펴보고자 하는데 생각대로 동작하지 않아서 질문남깁니다.

 

우선 테스트 코드 시 스프링 IOC 컨테이너를 사용하기 위해 @SpringBootTest를 선언한 상황이고, 아래는 테스트 코드 내용입니다.

스레드 1(트랜잭션1)에서 낙관적 락을 사용한 조회를 하도록 했고 커밋 되기 전, 스레드 2(르랜잭션2)에서 해당 데이터를 변경하도록 했습니다. 예상 대로라면 예외가 발생해야되는 데 정상 종료가 되어서 질문드립니다. ㅠㅠ

로그를 찍어보았는데, 예상대로 각 스레드 별로 독립적인 트랜잭션이 실행되고, 낙관적 락을 통해 마지막에 version 확인 쿼리까지 발행하는데 왜 오류가 발생하지 않는지 궁금합니다. 잘못된 점이 있으면 알려주시면 감사하겠습니다!!

답변 3

·

답변을 작성해보세요.

1

안녕하세요. rnqhstlr2297님

보내주신 로그와 코드를 확인해보니 쓰레드2가 먼저 수행되었습니다^^;

이 테스트가 정상 수행 되려면 쓰레드1이 다음 로직을 먼저 수행하고,

memberLockService.findMemberByOPTIMISTIC(memberId);

그 다음에 바로 쓰레드2가 다음 로직을 수행해야 하는데요.

memberLockService.findMemberAndUpdate(memberId, changeMoney);

다음과 같이 sleep 코드를 넣어서 쓰레드2의 실행을 잠시 미루면 쓰레드1이 먼저 수행되기 때문에 로그에서 오류 메시지를 확인하실 수 있을거에요.

        //스래드 2(트랜잭션2) -> 데이터 수정
        executorService.execute(() -> {
            // === 추가 ===
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            memberLockService.findMemberAndUpdate(memberId, changeMoney);
            countDownLatch.countDown();
        });

오류 메시지

org.springframework.orm.ObjectOptimisticLockingFailureException: Newer version [2] of entity [[hello.jdbc.lock.MemberLock#memberA]] found in database

감사합니다.

영한님 감사합니다 !!!! ㅎㅎ
제가 제대로 확인하지 않았네요 ㅠㅠㅠ

항상 강의 잘 듣고 있고 새로운 도전 응원합니다~~~~~

계속 귀찮게 해서 죄송합니다..
영한님이 알려준 코드로 실행해보았는데, 스레드 1이 먼저 시작된 것을 로그로 확인되었는데
마찬가지로 정상 종료가 됩니다...ㅠㅠ

실행된 로그를 첨부해드리겠습니다.

시간을 좀 더 늘려보시겠어요?

참고로 저는 H2 데이터베이스를 사용해서 테스트 했습니다.

변경한 전체 코드를 보여드릴게요.

    @Test
    void JPA_낙관적락_Test() throws InterruptedException {

        final int numberOfThreads = 2;
        CountDownLatch countDownLatch = new CountDownLatch(numberOfThreads);
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        final String memberId = "memberA";
        final int changeMoney = 80;

        //스래드 1(트랜잭션1) -> 낙관적 락 조회
        executorService.execute(() -> {
            try {
                log.info("thread1");
               memberLockService.findMemberByOPTIMISTIC(memberId);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                countDownLatch.countDown();
            }
        });

        //스래드 2(트랜잭션2) -> 데이터 수정
        executorService.execute(() -> {
            // === 추가 ===
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            memberLockService.findMemberAndUpdate(memberId, changeMoney);
            countDownLatch.countDown();
        });
        countDownLatch.await();
    }

 

전체 실행로그

2023-06-08T21:51:31.385+09:00 DEBUG 42009 --- [pool-2-thread-1] o.h.e.t.internal.TransactionImpl         : begin
Hibernate: 
    select
        m1_0.member_id,
        m1_0.money,
        m1_0.version 
    from
        member_lock m1_0 
    where
        m1_0.member_id=?
2023-06-08T21:51:31.489+09:00 DEBUG 42009 --- [pool-2-thread-2] o.h.e.t.internal.TransactionImpl         : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2023-06-08T21:51:31.489+09:00 DEBUG 42009 --- [pool-2-thread-2] o.h.e.t.internal.TransactionImpl         : begin
Hibernate: 
    select
        m1_0.member_id,
        m1_0.money,
        m1_0.version 
    from
        member_lock m1_0 
    where
        m1_0.member_id=?
2023-06-08T21:51:31.491+09:00 DEBUG 42009 --- [pool-2-thread-2] o.h.e.t.internal.TransactionImpl         : committing
Hibernate: 
    update
        member_lock 
    set
        money=?,
        version=? 
    where
        member_id=? 
        and version=?
2023-06-08T21:51:36.424+09:00 DEBUG 42009 --- [pool-2-thread-1] o.h.e.t.internal.TransactionImpl         : committing
Hibernate: 
    select
        version as version_ 
    from
        member_lock 
    where
        member_id=?
2023-06-08T21:51:36.428+09:00 DEBUG 42009 --- [pool-2-thread-1] o.h.e.t.internal.TransactionImpl         : rollback() called on an inactive transaction
Exception in thread "pool-2-thread-1" org.springframework.orm.ObjectOptimisticLockingFailureException: Newer version [2] of entity [[hello.jdbc.lock.MemberLock#memberA]] found in database
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:310)

감사합니다.

1

안녕하세요. rnqhstlr2297님

도움을 드리고 싶지만 질문 내용만으로는 답변을 드리기 어렵습니다.

실제 동작하는 전체 프로젝트를 압축해서 구글 드라이브로 공유해서 링크를 남겨주세요.

구글 드라이브 업로드 방법은 다음을 참고해주세요.

https://bit.ly/3fX6ygx

주의: 업로드시 링크에 있는 권한 문제 꼭 확인해주세요

추가로 다음 내용도 코멘트 부탁드립니다.

1. 문제 영역을 실행할 수 있는 방법

2. 문제가 어떻게 나타나는지에 대한 상세한 설명

감사합니다.

영한님 항상 강의 잘보고 있습니다~~~ 아래는 구글 드라이브 링크입니다!!

https://drive.google.com/file/d/1edMlUD4V_jdtgESSW4tETlZzDuu-QwPp/view?usp=sharing

 

  1. test.java.hello.jdbc.lock 경로의 DBLockTest 클래스내에 "JPA_낙관적락_test()메소드" 가 문제의 테스트 영역입니다.

  2. 낙관적 락(Optimistic 설정)을 테스트 하려고 합니다. 스레드 1 트랜잭션 커밋 전 스레드 2를 통해 데이터 변경을 진행하였고 트랜잭션 커밋도 진행되었습니다. 그리고 스레드 1의 트랜잭션 커밋전 예외를 기대했지만, 정상 종료가 됩니다!

1

인프런 AI 인턴님의 프로필

인프런 AI 인턴

2023.05.29

안녕하세요, 인프런 AI 인턴이에요.

테스트 코드를 보면 정상적으로 낙관적 락이 적용되어있고, 스레드 2에서 해당 트랜잭션을 변경하면 스레드 1에서 예외가 발생해야합니다. 하지만 예외가 발생하지 않는 것은 다음과 같은 이유로 인해 발생할 수 있습니다.

① 스프링에서 제공하는 캐쉬 때문으로 인해 캐쉬에서 데이터를 가져왔기 때문에 예외가 발생하지 않았을 가능성이 있습니다.
② 스레드 1과 2가 동시에 트랜잭션을 수행하지 않았을 가능성이 있습니다.

이 부분을 다시 한번 체크해보시고 문제가 있으면 자세한 코드와 로그 내용을 함께 첨부해주시면 더욱 도움이 될 것 같아요.

스프링의 기본 캐쉬를 종속성에 추가하지 않았으면, 기본 캐쉬가 적용되지 않는 것 아닌가요?!

혹시, 스레드 1,2 가 동시에 트랜잭션을 수행하지 않았다는 말씀이 어떤 의미인지 상세히 알 수 있을까요?
스레드 1의 트랜잭션을 시작하고 단순 조회 작업을 실행하고 커밋되기 전, 스레드 2의 트랜잭션이 시작되고 데이터를 변경 후 커밋이 완료되면, 스레드 1의 예외가 발생하는 흐름아닌가요?
트랜잭션이 동시에 수행하지 않는다는 말의 의미를 잘 모르겠습니다.

소스 코드는 위에 코드가 끝이고 로그 내용을 모두 첨부하겠습니다~~
imageimageimage
정성스러운 답변 항상 감사드립니다~~