learning raphael draft 2026-05-22

Redis 분산락 + DB 트랜잭션 분리 시 정합성 문제

TL;DR — Redis lock과 DB 트랜잭션은 서로 다른 레이어다. Lock 해제 시점이 DB commit 시점보다 빠르면, 후속 트랜잭션이 롤백된 결과를 정상으로 가정하고 동작해서 정합성이 깨진다. 면접 정답 라인: “Lock은 DB commit 이후에 해제하거나, 애초에 DB 비관적 락(SELECT FOR UPDATE)을 쓴다.”

개념 설명

핵심 아이디어

면접관이 던진 질문 원문: > “Redis lock을 ’안전 장치’로 쓰신다고 하셨는데, DB 트랜잭션 커밋 실패 시 Redis lock이 이미 해제된 상태라면 데이터 정합성을 어떻게 보장하시나요?”

무슨 일이 일어나는가 (잘못된 흐름):

시간 →

T1: [Redis lock 획득] → [DB UPDATE 실행] → [Redis lock 해제] → [DB COMMIT 실패 → ROLLBACK]
                                            ↓
T2:                                         [Redis lock 획득] → [잔액 조회 = 차감 전 상태]
                                                                 → [의사결정] → [DB UPDATE] → [COMMIT]

왜 중요한가

두 메커니즘의 레이어가 다르다:

항목 DB 비관적 락 (SELECT FOR UPDATE) Redis 분산락
범위 트랜잭션 범위와 자동 일치 별도 해제 호출 필요
해제 시점 COMMIT/ROLLBACK 시 자동 개발자가 명시적 호출 또는 TTL
장애 시 DB 트랜잭션과 운명 공유 TTL 만료 시 멋대로 풀림
인프라 단일 DB 별도 Redis 클러스터

Redis lock의 장점은 분산 환경(여러 서비스/DB 간 동시성 제어)인데, 단일 DB 내 동시성 제어용으로 쓰면 트랜잭션 경계 불일치 문제가 생긴다.

실전 적용

해결 방안 4가지 (면접 답변 라인)

  1. Lock 해제를 DB Commit 이후로 (Spring 패턴)
@Service
public class PaymentService {
    @Transactional
    public void processPayment(Long userId, BigDecimal amount) {
        // 1. Redis lock 획득
        RLock lock = redisson.getLock("payment:" + userId);
        lock.tryLock(3, 10, TimeUnit.SECONDS);

        try {
            // 2. DB 작업
            updateBalance(userId, amount);

            // 3. Commit은 메서드 종료 시 자동
            // 4. Lock 해제도 commit 이후로 미룸
        } finally {
            // ❌ 여기서 해제하면 commit 전이라 위험
            // lock.unlock();
        }
    }
}

// ✅ Commit 이후 이벤트 리스너에서 해제
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void releaseLock(LockReleaseEvent event) {
    event.getLock().unlock();
}
트레이드오프: Lock 보유 시간 늘어남 → 동시성 ↓. 짧은 트랜잭션에만 권장.
  1. DB 비관적 락 사용 (SELECT FOR UPDATE)
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT a FROM Account a WHERE a.id = :id")
    Account findByIdForUpdate(@Param("id") Long id);
}
  • Lock 범위 = 트랜잭션 범위 → commit/rollback과 자동 동기화
  • 단일 DB 내라면 이게 가장 깔끔
트레이드오프: DB 부하 ↑, 분산 환경(여러 서비스)에는 부적합.
  1. Idempotency Key + DB Unique Constraint
@Entity
public class PaymentTransaction {
    @Column(unique = true)
    private String idempotencyKey;  // 클라이언트가 전달
    // ...
}
  • Lock 자체를 안 쓰고 멱등성으로 중복 방지
  • 결제 같은 케이스에 자주 쓰임 (Stripe, Toss 패턴)
트레이드오프: 클라이언트가 idempotency key 관리 필요. 락보다 깔끔하지만 책임이 클라이언트로 이동.
  1. Outbox Pattern + Retry
  • DB에 “이벤트 발행 큐” 테이블 만들고 트랜잭션 안에서 함께 INSERT
  • 별도 워커가 이벤트 폴링 → 후속 작업
  • Lock 없이 최종 일관성 보장
트레이드오프: 즉시 일관성 X, 구현 복잡도 ↑.

면접 답변 권장 라인

“Redis 분산락은 분산 환경 전제일 때 의미가 있고, 단일 DB 내라면 DB 비관적 락이 더 자연스럽습니다. Redis lock을 쓴다면 반드시 DB commit 이후에 해제해야 하는데, Spring에서는 @TransactionalEventListener(AFTER_COMMIT)로 처리합니다. 다만 결제 같은 케이스는 idempotency key + unique constraint 조합이 더 명시적이고 안전해서 실무에서는 그쪽을 선호했습니다.”

함정 / 주의점
  • Redis lock TTL 만료 = 무음의 해제. 작업 중 lock이 풀리는 경우 대응 어려움.
  • Redisson tryLock 자동 갱신(watchdog)도 모든 경우 보장 X — 네트워크 분리 시 split-brain.
  • “Redis lock으로 안전성 보장”이라는 표현은 면접관에게 레이어 차이 인식 부족 신호로 읽힐 수 있다.
  • RedLock 알고리즘(Martin Kleppmann 논쟁)도 알아두면 가산점.

복습 일정

단계 날짜 완료
Day 0 (초학습) 2026-05-22
Day 7 2026-05-29
Day 37 2026-06-28
Day 127 2026-09-26

참고