DB Lock(Optimistic, Pessimistic)

낙관적, 비관적락의 설명과 JPA 진영에서의 동시성 제어, 충돌 해결 방법
정찬's avatar
May 11, 2025
DB Lock(Optimistic, Pessimistic)
 

Lock 이란?

 
Database에서 데이터를 수정할 때 동시성 문제가 발생할 수 있다.
Lock은 데이터베이스에서 발생하는 동시성을 제어하기 위해 사용되는 개념으로, 여러 트랜잭션이 공유 자원에 동시에 접근 하는 것을 조절하기 위한 잠금 장치이다.
 

Lock의 종류

 
락 종류
설명
주요 사용 상황
분산 락 (Distributed Lock)
여러 서버/인스턴스 간 공유 자원에 대한 락을 외부 시스템을 통해 구현 (ex. Redis, ZooKeeper)
멀티 인스턴스 환경에서의 동시성 제어
낙관적 락 (Optimistic Lock)
락을 걸지 않고 작업 후 버전 체크로 충돌 감지
충돌 가능성이 낮은 경우
비관적 락 (Pessimistic Lock)
자원에 접근할 때 즉시 락을 걸고, 다른 접근을 차단
충돌 가능성이 높은 경우
 
동시성 제어 방법은 여러 가지가 있으며, 각 환경에 맞는 락 기법을 선택하는 것이 중요하다. 예를 들어, 낙관적 락이나 비관적 락 기법을 사용한 동시성 제어는 단일 시스템에서는 유용할 수 있지만, 분산 환경에서는 제대로 동시성 제어가 되지 않을 수 있다.
소개한 네 가지 동시성 제어 방법 중에서, 저수준 락자원 낭비성능 저하를 초래할 수 있어, 특히 고성능 시스템에서는 잘 사용되지 않는다. 코드 레벨에서 동작하는 synchronized 방식은 단일 JVM 내에서만 작동하며, 블로킹을 유발할 수 있다.
따라서, 분산 환경에서 효과적인 동시성 제어를 위해서는 분산 락낙관적 락과 같은 기법을 고려해야 한다.
 
이 글에서는 낙관적 락과 비관적 락을 중심으로, 각 기법이 어떻게 동시성을 제어할 수 있는지에 대해 설명하고자 한다.
 

낙관적 락

 
 
낙관적 락(Optimistic Lock)은 락을 미리 걸지 않고, 작업 후 충돌을 감지하는 방식으로 동시성 제어를 수행하는 기법이다. 이 방법은 충돌이 발생할 확률이 낮을 때 유효하게 사용할 수 있다. 보통 버전 관리타임스탬프를 활용하여 충돌 여부를 확인한다.

🔑 낙관적 락의 동작 원리

낙관적 락은 기본적으로 락을 미리 걸지 않는 대신, 작업을 마친 후에 충돌 여부를 감지하는 방식이다. 이 때 버전 정보타임스탬프와 같은 메타데이터를 사용하여, 다른 트랜잭션이 해당 데이터에 영향을 미쳤는지 확인한다.

1. 작업 전 (Optimistic)

  • 트랜잭션이 시작될 때, 락을 걸지 않고 자원에 접근하여 작업을 시작한다.
  • 이때 자원에 대한 충돌 가능성을 낙관적으로 보고, 작업을 진행한다.

2. 작업 후 (Conflict Detection)

  • 작업이 완료되면 충돌 검사를 한다. 보통 버전 정보타임스탬프를 이용해서, 해당 데이터가 다른 트랜잭션에 의해 수정되지 않았는지 확인한다.
    • 예: 버전 번호가 변경되었으면 다른 트랜잭션에서 이미 수정되었음을 의미한다.

3. 충돌 발생 시 처리

  • 만약 충돌이 발생했다면, 트랜잭션을 롤백하거나, 사용자가 선택한 방법으로 재시도하거나 에러를 반환할 수 있다.
 
낙관적 락비관적 락충돌 예방 방식에 비해 성능상의 장점이 있다. 비관적 락은 트랜잭션 시작 시 즉시 락을 걸고, 다른 트랜잭션이 자원에 접근하지 못하도록 차단하여 충돌 가능성을 미리 방지한다.
그러나 이는 락 경합을 유발하고, 성능 저하를 초래할 수 있다. 반면, 낙관적 락은 락을 걸지 않고 작업을 진행하다가 충돌이 발생한 경우에만 트랜잭션을 롤백하거나 재시도하는 방식으로, 락을 걸지 않기 때문에 성능 상 이점을 제공한다.
 
또한 낙관적 락은 Application 레벨에서 작동하는 Lock이다. 낙관적 락은 version, timestamp와 같은 속성값을 바탕으로 변경을 감지한다. 만약 동시성 이슈가 발생했다면 오류를 발생시킨다. 개발자는 오류를 제어하여 롤백, 업데이트를 결정해아한다.
충돌이 발생했을 때 처리하는 추가적인 작업이 필수적이다. 이는 개발 편의성(추가적인 코드 작성)과 성능(추가적인 쿼리) 관점에서, 충돌이 발생할수록 비효율적이다.
따라서 낙관적 락은 기본적으로 lock을 설정하는 작업이 필요없기 때문에, 비관적 락보다 대부분의 상황에서 성능이 좋다. 하지만 충돌 발생 시 많은 비용이 발생하는 상황에서는 오히려 비효율적이기 때문에, 판단에 따라 적합한 동시성 제어 전략을 사용해야 한다.
 

JPA에서 낙관적 락을 사용하는 방법

 
낙관적 락은, 락을 걸지 않고 DB에 attribute로 변경을 감지하여 동시성을 제어한다고 앞에서 설명했다.
 
따라서 Entity에 version과 같은 속성을 추가해줘야 한다.
@Entity public class Student { @Id private Long id; private String name; private String lastName; @Version private Integer version; // getters and setters }
 
version 속성의 값은 엔터티를 통해 가져올 수 있지만, 업데이트하거나 증가시켜서는 안된다. Persistence Provider(Hibernate)만 해당 작업을 수행할 수 있고, 이에 따라 데이터의 일관성이 유지된다.
 

Lock Modes

 
JPA는 2개의 낙관적 락 모드를 제공한다.
  • OPTIMISTIC(READ)
  • OPSITMISTIC_FORCE_INCREMENT(WRITE)
 
이는 LockModeType 에서 찾을 수 있으며, 사용 예제는 다음과 같다.
 
em.find(Product.class, 1L, LockModeType.OPTIMISTIC); em.find(Product.class, 1L, LockModeType.OPTIMISTIC_FORCE_INCREMENT); em.find(Product.class, 1L, LockModeType.READ); em.find(Product.class, 1L, LockModeType.WRITE);
 
앞서 설명한것과 같이 OPTIMISTIC == READ, OPSITMISTIC_FORCE_INCREMENT == WRITE 이다.
 
OPTIMISTIC 잠금 모드를 요청할 때마다 Persistence Provider는 DIRTY READ 뿐만 아니라 NON REPEATABLE READ 도 방지한다.
 
READ락의 존재 이유는?
 
READ락을 사용하는 이유는, 조회하는 순간부터 충돌을 방지하기 위함이라고 한다. 예제 코드를 보면
 
@Transactional public void reduceStock(Long productId, int quantity) { Product product = productRepository.findById(productId) .orElseThrow(); if (product.getStock() < quantity) { throw new IllegalArgumentException("재고 부족"); } product.setStock(product.getStock() - quantity); productRepository.save(product); }
 
위 상황에서는 동시에 여러 트랜잭션이 들어오면 재고가 마이너스가 될 수 있다. 따라서 처음에 조회할 때 READ Lock을 설정하여, 다른 트랜잭션을 격리하여 동시성을 회피하는 방법이다.
충돌이 발생하면 해결하는 WRITE와 다르게 처음부터 충돌이 발생하는 상황을 방지하려는 목적에서 사용한다.
 

OptimisticLockException

 
Persistence Provider가 엔터티에서 낙관적 잠금 충돌을 발견하면 OptimisticLockException 을 발생시킨다.  이 예외로 인해 트랜잭션은 롤백 상태로 된다.
 
OptimisticLockException이 발생하면 로직에 맞게 처리해야 한다. 예를 들면, 새로운 환경에서 변경된 엔티티를 조회하여 알맞게 처리해서 커밋하거나, 롤백할 수 있을것이다.
 

비관적 락

 
비관적 락(Pessimistic Lock)은 데이터에 동시에 접근하는 상황에서 충돌(동시성 문제)을 미리 방지하기 위해 트랜잭션이 데이터를 읽거나 쓰기 전에 락(Lock)을 걸어 다른 트랜잭션이 해당 데이터에 접근하지 못하도록 막는 방식이다.
 
이름에서 유추할 수 있듯이, 비관적 락은 낙관적 락과 달리 충돌에 있어서 비관적인(?) 특징을 가진다.
충돌이 발생할것이라 생각하고, 미리 Lock을 걸어 동시성 문제를 회피하는 방식이다. 따라서 다른 트랜잭션의 접근을 허용하는 낙관적 락에 비해 성능적으로 비효율적일 수 있다. 하지만 미리 예방하는 비관적락의 특성 때문에 동시성 충돌이 자주 발생하는 환경에 적합하다.
 

🔑 비관적 락의 동작 원리

비관적 락은 데이터를 조작하기 전에 락(Lock)을 걸어 다른 트랜잭션의 접근을 차단한다. 이로 인해 데이터 정합성은 보장되지만, 락 경합으로 인한 성능 저하가 발생할 수 있다. 주로 DB 레벨에서 동작하며, 락 모드에 따라 읽기/쓰기 제한 수준이 달라진다.

1. 작업 전 (락 획득)

  • 트랜잭션 시작 시, 데이터에 락을 걸고 다른 트랜잭션의 접근을 차단한다.
  • 다른 트랜잭션은 락이 해제되기 전까지 해당 데이터에 접근하거나 수정할 수 없다.

2. 작업 중 (락 유지)

  • 해당 트랜잭션은 단독으로 데이터에 접근하여 작업을 수행한다.
  • 이 동안 다른 트랜잭션은 대기하거나 오류를 발생시킨다.

3. 작업 완료 (락 해제)

  • 트랜잭션이 커밋되거나 롤백되면, 락이 해제된다.
  • 이후 대기 중이던 트랜잭션들이 순차적으로 실행된다.

 

JPA에서 비관적 락을 사용하는 방법

 
비관적 락은 JPA의 @Lock 어노테이션과 함께 사용되며, SQL의 FOR UPDATE 구문을 기반으로 작동한다.

Entity Repository에서 사용 예:

java @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT p FROM Product p WHERE p.id = :id") Product findByIdForUpdate(@Param("id") Long id);

주요 LockModeType

LockModeType
설명
PESSIMISTIC_READ
다른 트랜잭션의 쓰기를 막고, 읽기는 허용 (공유 락, Shared Lock)
PESSIMISTIC_WRITE
다른 트랜잭션의 읽기/쓰기를 모두 막음 (배타 락, Exclusive Lock)
PESSIMISTIC_FORCE_INCREMENT
버전 정보를 증가시키며 배타 락을 건다 (낙관적 락 + 쓰기 의도 포함)

비관적 락의 장단점

✅ 장점

  • 데이터 정합성이 강력히 보장된다.
  • 충돌이 예상되는 경우에도 안정적인 처리가 가능하다.
  • 재고 감소, 좌석 예약 등 동시성 문제가 치명적인 도메인에 적합하다.

❌ 단점

  • 락 경합으로 인한 대기 시간이 발생할 수 있다.
  • 동시 요청이 많은 시스템에서는 성능 저하가 심각할 수 있다.
  • 트랜잭션이 오래 지속되면 데드락(교착상태) 이 발생할 가능성도 있다.
 

PessimisticLockException

 
비관적 락은 기본적으로 DB 수준에서 충돌을 막지만, 락 대기 시간이 지나면 LockTimeoutException 과 같은 예외가 발생할 수 있다.
try { productRepository.findByIdWithLock(productId); } catch (PessimisticLockException e) { // 예외 처리 로직: 재시도 또는 에러 반환 }
이 경우에는 재시도 로직을 구현하거나, 사용자에게 에러를 알리고 다시 시도하도록 유도할 수 있다.
 
 
Share article

lushlife99