서론
비관적 락은 데이터 충돌이 자주 발생할 것으로 가정하고, 데이터에 접근할 때마다 잠금을 걸어 다른 트랜잭션이 동시에 접근하지 못하게 합니다.
본 글에서는 Spring boot와 JPA를 활용하여 비관적 락의 동작 방식을 확인해보겠습니다.
Entity & Repository
Item Entity
Item 엔티티는 id, name, quantity라는 세 가지 필드를 가진 간단한 JPA 엔티티입니다.
ItemRepository
ItemRepository 인터페이스에서는 @Lock 어노테이션을 사용하여 LockModeType.PESSIMISTIC_WRITE 모드를 지정하고, findByIdWithLock 메서드를 통해 데이터를 조회할 때 해당 데이터에 쓰기 잠금을 적용합니다.
이로 인해 다른 트랜잭션이 해당 데이터를 읽거나 수정할 수 없게 됩니다.
비관적 락이 적용된 서비스 메소드
ItemService
ItemService의 updateItemQuantity 메소드는 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", quantity를 10으로 설정하여 데이터베이스에 저장하여 초기 데이터를 저장합니다. 위 로그를 보면 정상적으로 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(업데이트 손실)이 발생했습니다.
비관적 락은 트랜잭션 간의 충돌을 방지하기 위해 설정되지만, 락이 설정된 후의 트랜잭션이 완료될 때까지 대기하도록 하는 방식으로 동작합니다.
하지만 락의 순서와 트랜잭션의 처리 방식에 따라 여전히 업데이트 손실이 발생할 수 있습니다.
특히, 두 트랜잭션이 동시에 데이터의 상태를 읽고 수정하는 경우, 데이터의 일관성을 보장하는 방법이 필요합니다.
'Framework > JPA' 카테고리의 다른 글
[JPA] JPA에서 단방향 및 양방향 관계 이해 (@ManyToOne, @OneToMany, @OneToOne, @ManyToMany) (1) | 2024.08.23 |
---|---|
[JPA] JPA에서 낙관적 락(Optimistic Locking)을 통한 동시성 제어하기 (@Version) (0) | 2024.08.22 |
[JPA] @Lock 어노테이션이란 무엇일까❓: 다양한 LockModeType 잠금 모드 (0) | 2024.08.21 |
[JPA] Query Method란 무엇일까 ❓ (0) | 2024.07.27 |
[JPA] JPA Auditing란 무엇일까❓(@EnableJpaAuditing, @MappedSuperclass, @EntityListeners) (0) | 2024.07.27 |