728x90

서론

비관적 락 데이터 충돌 자주 발생할 것으로 가정하고, 데이터에 접근할 때마다 잠금을 걸어 다른 트랜잭션 동시에 접근하지 못하게 합니다.

 

 

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

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

pixx.tistory.com

 

[JPA] @Lock 어노테이션이란 무엇일까❓: 다양한 LockModeType 잠금 모드

@Lock 어노테이션이란❓@Lock 어노테이션은 Spring Data JPA에서 제공하는 기능으로, JPA 리포지토리 메소드에 적용하여 특정 데이터베이스 쿼리에 대한 잠금 모드를 지정하는 데 사용됩니다. 이 어

pixx.tistory.com

 

본 글에서는 Spring bootJPA를 활용하여 비관적 락동작 방식을 확인해보겠습니다.

 

Entity & Repository

Item Entity

 

 

Item 엔티티는 id, name, quantity라는 세 가지 필드를 가진 간단한 JPA 엔티티입니다.

 

ItemRepository

 

ItemRepository 인터페이스에서는 @Lock 어노테이션을 사용하여 LockModeType.PESSIMISTIC_WRITE 모드를 지정하고, findByIdWithLock 메서드를 통해 데이터를 조회할 때 해당 데이터에 쓰기 잠금을 적용합니다.

 

이로 인해 다른 트랜잭션이 해당 데이터를 읽거나 수정할 수 없게 됩니다.

 

비관적 락이 적용된 서비스 메소드

ItemService

 

ItemServiceupdateItemQuantity 메소드findByIdWithLock 메소드를 통해 Item 엔티티를 조회할 때 비관적 락사용합니다.

비관적 락의 동작 방식

 

  • findByIdWithLock 메소드를 호출하면, 데이터베이스는 해당 Item 레코드를 읽으면서 쓰기 잠금을 설정합니다.(LockModeType.PESSIMISTIC_WRITE).
  • 이 잠금이 설정된 동안에는 다른 트랜잭션에서 해당 Item읽거나 쓰려고 하면, 해당 트랜잭션은 잠금이 해제될 때까지 대기해야 합니다.
  • 이렇게 해서 두 개 이상의 트랜잭션이 동시에 동일한 Item을 수정하려고 할 때 발생할 수 있는 충돌을 방지합니다.

 

 

Test에서 비관적 락 동작 확인하기

@Test
public void testPessimisticLocking() throws InterruptedException {
    // 초기 데이터 설정
    logger.info("초기 아이템 데이터를 설정합니다.");
    Item item = new Item();
    item.setName("Item 1");
    item.setQuantity(10);
    itemRepository.save(item);

    // 첫 번째 트랜잭션: 아이템 수량을 20으로 업데이트
    Thread thread1 = new Thread(() -> {
        logger.info("스레드 1: 아이템 수량 업데이트를 시도합니다.");
        itemService.updateItemQuantity(item.getId(), 20);
        logger.info("스레드 1: 아이템 수량 업데이트 완료.");
    });

    // 두 번째 트랜잭션: 아이템 수량을 30으로 업데이트
    Thread thread2 = new Thread(() -> {
        logger.info("스레드 2: 아이템 수량 업데이트를 시도합니다.");
        itemService.updateItemQuantity(item.getId(), 30);
        logger.info("스레드 2: 아이템 수량 업데이트 완료.");
    });

    // 두 스레드를 동시에 실행
    thread2.start();
    thread1.start();

    // 두 스레드가 종료될 때까지 대기
    thread1.join();
    thread2.join();

    // 최종 결과를 확인합니다.
    Item updatedItem = itemService.findItemById(item.getId());
    logger.info("최종 아이템 수량: {}", updatedItem.getQuantity());
}

 

 

Item 객체를 생성하고 name"Item 1", quantity10으로 설정하여 데이터베이스에 저장하여 초기 데이터를 저장합니다. 위 로그를 보면 정상적으로 insert 쿼리문과 로그가 찍혔습니다.

 

 

 

두 개의 Thread 객체를 생성하여, 각각의 스레드동시에 데이터의 수량을 변경하도록 합니다. 그리고 start()메서드를 사용하여 두 스레드를 동시에 실행하고, join()메서드를 사용하여 두 스레드가 종료될 때 까지 대기합니다.

  • Thread1: 수량을 10 ➡️ 20으로 업데이트합니다.
  • Thread2: 수량을 10 ➡️ 30으로 업데이트합니다.

위 로그를 보면 두 스레드동시에 실행했음에도 불구하고, Thread2수량 업데이트먼저 실행되고 나서 완료되어야 그제서야Thread1 업데이트가 실행되는 것은 비관적 락 정상적으로 동작하고 있음을 나타낼 수 있습니다.

 

 

또한 위 쿼리 실행 로그를 보면 FOR UPDATE가 있습니다. FOR UPDATE는 SQL에서 특정 데이터베이스 행에 대해 비관적 락을 설정하는 쿼리입니다. 따라서 정상적으로 비관적 락이 설정된 것을 확인할 수 있습니다.

 

 

그리고 최종 아이템 수량을 보면 20입니다. 최종 수량은 다음과 같은 과정을 지났습니다.

 

  • 초기 상태: Item의 수량이 10입니다.
  • Thread2: 수량을 30으로 업데이트합니다.
  • Thread1: 수량을 20으로 업데이트합니다.

즉, Lost Update(업데이트 손실)이 발생했습니다.

 

비관적 락트랜잭션 간의 충돌을 방지하기 위해 설정되지만, 락이 설정된 후의 트랜잭션이 완료될 때까지 대기하도록 하는 방식으로 동작합니다.

 

하지만 락의 순서트랜잭션의 처리 방식에 따라 여전히 업데이트 손실이 발생할 수 있습니다.

 

특히, 두 트랜잭션이 동시에 데이터의 상태를 읽고 수정하는 경우, 데이터의 일관성을 보장하는 방법이 필요합니다.