728x90

서론

낙관적 락 데이터 충돌 드물 것이라고 가정하고, 데이터를 수정할 때만 충돌을 검사합니다. 트랜잭션이 데이터를 수정하기 전까지는 락을 걸지 않고, 수정 시점에 데이터가 변경되지 않았는지 확인합니다.

 

 

[DB] DB Lock이란 무엇일까❓ : 데이터 무결성을 지키는 다양한 DB Lock알아보기

오늘날의 데이터베이스 시스템은 수많은 사용자와 프로세스가 동시에 데이터를 접근하고 수정하는 복잡한 환경에서 운영됩니다. 이러한 환경에서 데이터의 무결성과 일관성을 유지하는 것은

pixx.tistory.com

 

본 글에서는 JPA의 @Version 어노테이션을 사용하여 낙관적 락을 구현하고, 낙관적 락을 사용하여 동시성 문제를 해결하는 동작을 확인해보겠습니다.

 

@Version 어노테이션이란 ❓

@Version 어노테이션은 주로 JPA를 사용할 때 낙관적 락(Optimistic Locking)구현하기 위해 사용됩니다.

 

이 어노테이션은 엔티티(Entity)의 버전 관리에 사용되며, 데이터베이스 테이블의 특정 열매핑되어 엔티티의 수정 이력을 추적합니다.

주요 기능

 

  • 낙관적 락(Optimistic Locking)
    • @Version 어노테이션이 적용된 필드는 엔티티가 저장될 때마다 자동으로 증가합니다.
    • 이 필드는 주로 int, long, Timestamp와 같은 타입으로 선언됩니다.
  • 동시성 제어
    • 여러 트랜잭션동시에 같은 엔티티수정할 때, @Version 필드값이 변경되었는지 확인하여 동시성 문제를 방지합니다.
    • 만약 트랜잭션이 데이터를 수정하려고 할 때, 이 필드의 값이 기존과 다르다면, JPA는 OptimisticLockException 예외를 발생시켜 변경을 막습니다.

 

Entity 클래스

Product Entity

 

Product 엔티티 클래스상품 정보를 담고 있는 간단한 JPA 엔티티입니다.

 

  • @Version 어노테이션이 붙은 version 필드낙관적 락을 구현하기 위해 사용됩니다.
  • 이 version 필드는 JPA가 엔티티의 상태를 관리하고, 업데이트 시 현재 버전데이터베이스의 버전을 비교하여 충돌을 감지합니다. 만약 다른 트랜잭션이 데이터를 변경했다면, 버전 불일치로 인해 OptimisticLockException 예외이 발생합니다.

 

 

Repository Interface

ProductRepository

public interface ProductRepository extends JpaRepository<Product, Long> {
}

 

 

Service

ProductService

 

ProductServic 클래스는 비즈니스 로직을 처리하는 서비스 클래스입니다.

 

  • updateProductPrice 메서드는 특정 productId에 해당하는 상품의 가격새 값으로 업데이트합니다.
  • 이 메서드는 @Transactional 어노테이션으로 트랜잭션 내에서 실행되며, 동시성 문제를 처리하기 위해 ObjectOptimisticLockingFailureException캐치(catch)합니다.
  • 만약 두 개의 트랜잭션동일한 상품수정하려고 할 때, 첫 번째 트랜잭션이 완료된 후 두 번째 트랜잭션이 수행되면 version 불일치로 인해 예외가 발생하게 됩니다.

 

 

낙관적 락 동작 확인하기

@SpringBootTest
public class ProductServiceTests {

    @Autowired
    private ProductService productService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    public void testOptimisticLocking() throws InterruptedException {
        // 초기 데이터 생성
        Product product = new Product();
        product.setName("Product 1");
        product.setPrice(100.0);
        productRepository.save(product);

        Thread thread1 = new Thread(() -> {
            productService.updateProductPrice(product.getId(), 200.0);
        });

        Thread thread2 = new Thread(() -> {
            assertThrows(ObjectOptimisticLockingFailureException.class, () -> {
                productService.updateProductPrice(product.getId(), 300.0);
            });
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }
}

 

 

테스트 목표 🏁

두 번째 스레드
가 동작할 때 ObjectOptimisticLockingFailureException발생하는지assertThrows를 통해 확인하는 것입니다.

스레드 2번이 Product의 가격을 300으로 수정하려고 할 때, 이미 버전이 변경되었으므로 예외가 발생할 것으로 예상합니다.

 

테스트를 위해 Hibernate product 테이블을 생성합니다. 이 테이블에는 price, version, id, name 필드가 있습니다. version 필드낙관적 락을 관리하는 데 사용됩니다.

 

 

테스트를 시작하기 전에 Product 1이라는 이름으로 초기 데이터삽입합니다. version 필드초기값은 0입니다.

 

 

위 로그를 보면 스레드 1과 스레드 2가 동시에 실행되었습니다. 스레드 1스레드 2가 거의 동시에 product 데이터를 조회합니다. 두 스레드 모두 동일한 version 값(version = 0)을 가져옵니다.

 

 

이 단계에서는 아직 데이터베이스에 아무런 변화도 없습니다.

 


업데이트 시도

 

위 로그를 보면 스레드 1Product 엔티티가격을 200으로 업데이트합니다. 이때 version 값이 0에서 1로 증가합니다.

  • (binding parameter (3:INTEGER) <- [1]).

where id=? and version=? 조건을 통해 현재 버전0인지 확인한 후에 업데이트가 수행됩니다.

 

즉, 두 스레드가 동시에 UPDATE문을 실행합니다. 로그에 따르면, 두 스레드 모두 0인 버전을 사용하여 업데이트를 시도합니다. 하지만 스레드 1번이 먼저 성공적으로 업데이트를 완료하고, 버전이 1로 증가합니다.

 

위 로그를 보면 스레드 2업데이트 시도를 하다가 예외가 발생했습니다.

 

  • 스레드 2도 Product 엔티티의 가격을 300으로 업데이트하려고 시도하지만, where id=? and version=? 조건에서 version이 이미 1로 변경된 것을 확인합니다.

 

이후 스레드 2번수정된 엔티티다시 수정하려고 할 때, 버전이 맞지 않아 충돌이 발생하게 되고, 이로 인해 스레드 2업데이트에 실패하고, ObjectOptimisticLockingFailureException발생합니다. 이 예외는 assertThrows로 예상한 대로 발생하여 테스트가 정상적으로 통과합니다.

 

결과적으로, 예상한 예외가 발생했으므로 테스트가 정상적으로 종료되고 "Tests passed"가 출력된 것입니다.

 

이는 낙관적 락성공적으로 동작했음을 보여줍니다. 

 

 

 

"ObjectOptimisticLockingFailureException" 은 Spring Framework에서 제공하는 예외 중 하나로, JPA에서 낙관적 락(Optimistic Locking) 사용 시 발생할 수 있는 예외입니다.

이 예외는 주로 데이터베이스의 동시성 문제를 처리하는 과정에서 발생합니다.