DB Lock

 

DB 락

데이터베이스에서 여러 트랜잭션이 동시에 같은 데이터에 접근할 때, 데이터의 무결성(일관성)을 보장하기 위해 사용되는 메커니즘

 

필요성

Dirty Read 

 

트랜잭션 A가 데이터를 수정 중일 때 트랜잭션 B가 그 데이터를 읽는다. 하지만 트랜잭션 A가 롤백된다면 트랜잭션 B는 잘못된 데이터를 읽은 것이 된다.

 

초기 값: balance = 100

트랜잭션 A 시작
수정: balance = 100 → 80 (아직 커밋되지 않음)

트랜잭션 B 시작
읽기: balance = 80 ← Dirty Read 발생
작업 진행...

트랜잭션 A 롤백 → balance = 100으로 복원

트랜잭션 B는 잘못된 balance(80)를 바탕으로 로직을 수행하게 됨

 

Non-repeatable Read

 

트랜잭션 A가 데이터를 읽은 후, 트랜잭션 B가 그 데이터를 수정하고 커밋한다. 이 경우 트랜잭션 A가 동일한 데이터를 다시 읽을 때 값이 달라지게 된다. 같은 트랜잭션 내에서 동일한 데이터를 두 번 읽었지만, 그 값이 일관되지 않는 문제이다.

 

초기 값: balance = 100

트랜잭션 A 시작
읽기 1회: balance = 100

트랜잭션 B 시작
수정: balance = 100 → 80
커밋

트랜잭션 A 계속
읽기 2회: balance = 80 ← Non-repeatable Read

→ 트랜잭션 A는 같은 쿼리를 두 번 실행했지만, 결과는 서로 다름

 

Lost Update (업데이트 손실)

 

두 개 이상의 트랜잭션이 동시에 같은 데이터를 수정할 때, 한 트랜잭션의 변경 내용이 나중 트랜잭션의 변경에 의해 덮어써져 사라지는 현상을 말한다.

 

초기 값: balance = 100

트랜잭션 A 시작
읽기: balance = 100

트랜잭션 B 시작
읽기: balance = 100
수정: balance = 100 - 30 → balance = 70
커밋

트랜잭션 A 계속
수정: balance = 100 - 20 → balance = 80
커밋

 

 

 

현상 트랜잭션 간 순서/흐름 예시 문제
Dirty Read A 수정 → B 읽음 → A 롤백 존재하지 않는 데이터 기반 로직
Non-repeatable Read A 읽음 → B 수정 후 커밋 → A 재읽음 같은 데이터인데 값이 바뀜
Lost Update A 읽음 → B 읽음/수정/커밋 → A 수정/커밋 A가 무시됨 (충돌 감지 없음)

 

 

DB 락을 통해 데이터에 대한 접근을 제어하면, 위와 같은 상황에서 발생할 수 있는 데이터 무결성 문제를 예방할 수 있다.

 

 

 

DB 락의 종류

공유 락 (Shared Lock, S Lock)

  • 데이터베이스에서 데이터를 읽을 때 사용한다.
  • 여러 트랜잭션이 동시에 같은 데이터를 읽을 수 있지만, 공유 락이 걸린 동안에는 데이터를 수정할 수 없다.

 

배타 락 (Exclusive Lock,  X Lock)

  • 데이터를 수정할 때 사용한다.
  • 배타 락이 걸린 데이터는 다른 트랜잭션이 읽거나 수정할 수 없다.
  • 한 트랜잭션이 배타 락을 획득하면 다른 모든 트랜잭션은 해당 데이터에 접근할 수 없다.

 

비관적 락 (Pessimistic Locking)

  • 데이터를 읽는 시점부터 락을 걸어 다른 트랜잭션이 접근하지 못하게 함
  • 데이터 충돌 가능성이 높은 경우에 적합
  • DB 차원에서 락이 걸리므로, 잠금 범위가 강력하지만 성능 비용이 큼

은행 시스템에서 한 사용자가 특정 계좌의 잔액을 조회하고 수정하려는 시나리오에서, 다른 사용자가 이 계좌에 접근하지 못하게 해야할 때 비관적 락을 적용할 수 있다.

트랜잭션 A 시작
→ 계좌 balance에 락 설정
→ 다른 트랜잭션은 대기

트랜잭션 A 커밋
→ 락 해제
→ 다른 트랜잭션 진행 가능

 

SpringBoot를 기준으로, 다음과 같은 어노테이션을 Repository Query 메서드에 붙여줌으로써 비관적 락을 적용할 수 있다.

  • @Lock(LockModeType.PESSIMISTIC_READ) : 읽기는 허용, 쓰기 금지
  • @Lock(LockModeType.PESSIMISTIC_WRITE) : 읽기, 쓰기 모두 금지

 

비관적 락은 데이터베이스 레벨에서 락을 적용하여 다른 트랜잭션의 접근을 차단한다. 따라서 락 유지 시간이 길어질 경우 전체 시스템의 응답 속도가 떨어질 수 있어, 신중하게 사용해야 한다.

 

 

낙관적 락 (Optimistic Locking)

 

  • 데이터를 읽을 때는 락을 걸지 않고, 수정 시점에 충돌을 감지
  • 데이터 충돌 가능성이 낮은 경우에 적합
  • 버전 필드(@Version)를 활용하여 충돌을 감지하고 예외 처리 또는 재시도

 

 

여러 사용자가 동시에 상품 정보를 수정할 수 있는 상황에서 낙관적 락을 적용해볼 수 있다. 마지막 저장 시점에만 충돌 여부를 확인한다.

트랜잭션 A: 상품 버전 1 → 가격 5만원 수정 → 저장 → 버전 2로 증가

트랜잭션 B: 상품 버전 1 → 가격 6만원 수정 → 저장 시 버전 불일치 예외 발생

 

SpringBoot를 기준으로, 버전 필드는 엔티티 필드에 @Version을 적어줌으로써 적용할 수 있다. 해당 엔티티의 수정 횟수를 추적하는 역할이고, 버전 필드로 명시하게 되면 트랜잭션이 엔티티를 수정하고 저장하려고할 때마다 자동으로 현재 데이터베이스에 저장된 버전과 트랜잭션이 처음 읽어온 버전 번호를 비교한다.

 

버전 번호가 다르면 다른 트랜잭션이 데이터를 수정한 것으로 간주하며 OptimisticLockException 예외가 발생한다. 이 예외 상황을 catch해서 현재 트랜잭션을 롤백하거나 재시도하는 로직이 추가적으로 구현되어야 한다.

 

 

명명된 락 (Named Lock)

  • 데이터베이스에서 특정 이름으로 락을 설정하여, 한번에 하나의 프로세스만 특정 리소스에 접근
  • 주로 특정 리소스나 작업에 대한 접근을 직관적으로 제어하기 위해 사용

 

분산 락 (Distributed Lock)

  • 여러 시스템(서버 인스턴스)에서 공유 자원에 동시에 접근할 수 없도록 제어하는 락
  • 보통 Redis, ZooKeeper, etcd 등을 활용해 구현
    • 일반적인 자바의 synchronized, DB의 SELECT ... FOR UPDATE는 단일 인스턴스 내에서만 유효
    • 분산 환경에서는 인스턴스 간 상태를 공유하지 않기 때문에, 외부 저장소(공통 자원)를 활용해야 함

 

비관 락 vs 낙관 락

 

항목 비관적 락 (Pessimistic Lock) 낙관적 락 (Optimistic Lock)
락 시점 데이터를 읽는 시점부터 락을 건다 데이터를 수정 시점에 충돌 여부를 확인
동시 접근 차단 다른 트랜잭션의 읽기/수정 모두 차단 가능 다른 트랜잭션의 접근은 허용, 단 저장 시 충돌 감지
충돌 발생 시 락 보유 중이므로 충돌 자체가 차단 충돌 시 예외 발생, 트랜잭션 롤백 또는 재시도 필요
성능 측면 병행 처리 적고 락 대기로 인해 성능 저하 가능 병행 처리에 유리, 충돌이 없을수록 성능 우수
재시도 필요 여부 거의 없음 충돌 시 재시도 로직 필요
적합한 상황 데이터 변경이 자주 발생하고 충돌 가능성 높을 때 병행 처리 많고 충돌 가능성 낮을 때
구현 방식 (JPA 기준) @Lock(PESSIMISTIC_WRITE) / SELECT FOR UPDATE @Version 필드 사용
주의 사항 데드락 가능성, 트랜잭션 지연 충돌 감지를 위한 버전 필드 필요, 재시도 설계 필요