작성자: 권지원
<aside>
@Transactional 내부에서 Lock 획득 시 Lock 해제 시점과 Tx 커밋 시점 사이에 공백발생
→ 다른 스레드가 Lock을 획득해 커밋 이전 데이터에 접근 시 DB 정합성 보장 불가분산 Lock을 도입할 때 흔히 발생하는 패턴이 있다.
@Transactional // ① 트랜잭션 시작
public void someMethod(Long id) {
lock.acquire(id); // ② Lock 획득
// ... 비즈니스 로직 ...
lock.release(id); // ③ Lock 해제
} // ④ 트랜잭션 커밋
⚠️ 문제: ③ Lock 해제와 ④ 커밋 사이에 공백이 생긴다. 이 짧은 순간 동안 다른 스레드가 Lock을 획득하여 아직 커밋되지 않은 데이터를 읽을 수 있다.
@Transactional은 Spring AOP 프록시를 통해 동작한다. 실제 실행 순서는 다음과 같다.
// Spring이 내부적으로 처리하는 순서
TransactionManager.begin(); // 트랜잭션 시작
try {
target.someMethod(); // 실제 메서드 실행
// └─ lock.acquire()
// └─ 비즈니스 로직
// └─ lock.release() ← 여기서 Lock 풀림
TransactionManager.commit(); // 그 다음에 커밋
} catch (Exception e) {
TransactionManager.rollback();
}
lock.release() 시점에는 아직 커밋이 완료되지 않은 상태다.
DB에는 변경사항이 반영되지 않았지만 Lock은 이미 반환됐으므로, 다음 스레드가 즉시 진입할 수 있다.
예약 만료(Expire) 스케줄러와 결제 확정(PaymentConfirm)이 동시에 실행되는 경우를 예시로 본다.
| 시간 | 스레드 A (Expire) | 스레드 B (PaymentConfirm) |
|---|---|---|
| T1 | Lock 획득 (reservation:1) | Lock 대기 |
| T2 | EXPIRED 상태로 변경 | Lock 대기 |
| T3 | Lock 해제 ← 여기가 문제 | Lock 획득 성공 |
| T4 | (커밋 전) | DB 조회 → 아직 PENDING으로 읽힘 |
| T5 | 트랜잭션 커밋 → EXPIRED | CONFIRMED 처리 진행 |
| T6 | - | 트랜잭션 커밋 → CONFIRMED |
❌ 결과: 예약 상태가 EXPIRED인데 결제는 CONFIRMED. DB 정합성이 깨진다.