강의

멘토링

로드맵

Inflearn brand logo image

인프런 커뮤니티 질문&답변

안_님의 프로필 이미지
안_

작성한 질문수

재고시스템으로 알아보는 동시성이슈 해결방법

@Lock(OPTIMISTIC)이 필요한 이유

작성

·

31

0

안녕하세요!

강의를 복습하던 중 의문이 생겼습니다.

 

JPA에서 @Version을 사용하면 자동으로 낙관적락이 적용되는 걸로 알고 있는데 @Lock(OPTIMISTIC)이 필요한 이유가 궁금했습니다.

 

그래서 알아보니 @Version의 경우는 엔티티에 수정/삭제가 될 경우에만 version을 체크하기 때문에 읽기만 존재할 때도 다른 곳에서 변경이 됐는지 감지하기 위해서는 @Lock(OPTIMISTIC)을 사용하라고 하던데 이게 맞을가요?

 

혹시 맞다면 @Lock(OPTIMISTIC)을 사용해서 OptimisticLockException이 발생하는 예시코드 부탁드립니다.

 

아래와 같이 테스트코드를 작성해봤는데 테스트가 성공하지 않아서요

    @Test
    void optimistic_lock_on_read_conflict() throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Callable<Void> task1 = () -> {
            service.readWithOptimisticLock(productId);
            return null;
        };

        Callable<Void> task2 = () -> {
            service.updatePrice(productId, 4000);
            return null;
        };

        Future<Void> f1 = executor.submit(task1);
        Future<Void> f2 = executor.submit(task2);

        f2.get();
        assertThatThrownBy(f1::get) // 읽기만 했던 쪽도 커밋 시점에서 충돌 감지
            .hasCauseInstanceOf(OptimisticLockingFailureException.class);

        executor.shutdown();
    }


//service.readWithOptimisticLock(productId);
    @Transactional(readOnly = true)
    public void readWithOptimisticLock(Long id) {
        productRepository.findByIdWithOptimisticLock(id);

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        productRepository.flush();
    }

//service.updatePrice(productId, 4000);
    @Transactional
    public void updatePrice(Long id, int newPrice) {
        Product product = productRepository.findById(id).orElseThrow();
        product.setPrice(newPrice);
        productRepository.flush();
        // flush 시점에 @Version 체크됨
    }

답변 2

0

최상용님의 프로필 이미지
최상용
지식공유자

안_님 안녕하세요.
말씀하신것처럼 업데이트시 감지될때만 필요하다면 @Version 만 붙여도 무방할것같습니다.
강의에서 Lock 을 사용한것은 별다른 이유는 없고 현재 OPTIMISTIC Lock 실습중인것을 나타내고싶었습니다.

OptimisticLockException 이 발생하지 않는것은 self-invocation 문제로 transactional 이 정상적으로 동작하지 않는듯합니다.

readWithOptimisticLock 메소드를 외부 클래스로 구현하신 후에 그 클래스를 이용하도록 변경해보시면 될것 같아요.

그래도 안되시면 전체소스를 깃헙에 올려주신 후에 주소를 알려주시면 다운받아서 확인해보도록 하겠습니다.

감사합니다.

0

안녕하세요, 인프런 AI 인턴입니다. 안_님!

질문 주신 내용에 대해 설명해 드리겠습니다. JPA에서 @Version을 통해 낙관적 락(Optimistic Lock)을 구현할 수 있습니다. 이것은 엔티티가 수정/삭제될 때 version을 체크하여 동시성 문제를 감지합니다. 그러나 @Lock(OPTIMISTIC) 어노테이션은 데이터 조회 시점에도 락을 잡을 수 있도록 하여, 데이터 읽기 때 발생할 수 있는 동시성 문제를 감지하고자 할 때 사용됩니다.

테스트 코드에서 OptimisticLockException이 발생하지 않는 이유는 @Lock(OPTIMISTIC)을 통해 읽기 시점에 락을 걸지 않았기 때문일 수 있습니다. @Lock(OPTIMISTIC)을 사용하지 않으면 읽기 시점에는 버전 체크가 이루어지지 않기 때문에 데이터를 읽기만 하는 트랜잭션에서는 버전 불일치로 인한 예외가 발생하지 않습니다.

이를 해결하기 위해서는 productRepository.findByIdWithOptimisticLock(id); 메서드에서 @Lock(OPTIMISTIC)을 적용하여 읽기 시점에도 락을 잡도록 설정해야 합니다. 샘플 코드에서는 읽기 트랜잭션에서 명시적으로 락을 잡는 로직이 빠져 있어, OptimisticLockException이 트리거되지 않은 것 같습니다.

예시 코드

먼저, @Lock(OPTIMISTIC)을 적용한 메서드를 작성합니다.

public interface ProductRepository extends JpaRepository<Product, Long> {
    
    @Lock(LockModeType.OPTIMISTIC)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Product findByIdWithOptimisticLock(Long id);
}

이 인터페이스에 @Lock(OPTIMISTIC)을 적용하면, findByIdWithOptimisticLock 메서드를 호출할 때 조회 시점에서 낙관적 락을 걸고, 이후 데이터 변경 시점에서 버전 충돌 여부를 확인합니다.

이렇게 설정 후 테스트 코드를 다시 실행했을 경우, 데이터 업데이트와 동시에 읽기가 이루어질 때 버전 충돌에 따른 OptimisticLockingFailureException이 발생할 수 있습니다.

낙관적 락을 활용할 때는 데이터 일관성과 동시성 문제를 잘 이해하고, 적절한 트랜잭션 설계와 테스트를 통해 문제를 방지할 수 있습니다.

추가적으로 관련된 질문 및 사례를 확인하여 문제가 발생한 다른 사례와 비교해 보시기를 추천드립니다:
- 낙관적 락 테스트 실패

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

안_님의 프로필 이미지
안_

작성한 질문수

질문하기