작성자: 권지원

Lock과 Transaction 사이 Race Condition 을 구조 변경, AOP, 낙관락 도입으로 해결

<aside>


1. 문제 정의

분산 Lock을 도입할 때 흔히 발생하는 패턴이 있다.

@Transactional                     // ① 트랜잭션 시작
public void someMethod(Long id) {
    lock.acquire(id);              // ② Lock 획득
    // ... 비즈니스 로직 ...
    lock.release(id);              // ③ Lock 해제
}                                  // ④ 트랜잭션 커밋

⚠️ 문제: ③ Lock 해제와 ④ 커밋 사이에 공백이 생긴다. 이 짧은 순간 동안 다른 스레드가 Lock을 획득하여 아직 커밋되지 않은 데이터를 읽을 수 있다.


2. 왜 공백이 생기는가

@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은 이미 반환됐으므로, 다음 스레드가 즉시 진입할 수 있다.


3. Race Condition 시나리오

예약 만료(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 정합성이 깨진다.