Redis 분산락 + DB 트랜잭션 분리 시 정합성 문제
개념 설명
핵심 아이디어
면접관이 던진 질문 원문:
> "Redis lock을 '안전 장치'로 쓰신다고 하셨는데, DB 트랜잭션 커밋 실패 시 Redis lock이 이미 해제된 상태라면 데이터 정합성을 어떻게 보장하시나요?"
무슨 일이 일어나는가 (잘못된 흐름):
```
시간 →
T1: [Redis lock 획득] → [DB UPDATE 실행] → [Redis lock 해제] → [DB COMMIT 실패 → ROLLBACK]
↓
T2: [Redis lock 획득] → [잔액 조회 = 차감 전 상태]
→ [의사결정] → [DB UPDATE] → [COMMIT]
```
- T1이 DB 작업을 했지만 **commit 전에 lock 해제**
- T2가 lock 획득 → "T1이 작업 완료한 후"라고 가정하고 동작
- T1이 commit 실패 → 롤백
- 결과: T2는 잘못된 가정으로 동작, 정합성 깨짐
왜 중요한가
두 메커니즘의 레이어가 다르다:
| 항목 | DB 비관적 락 (`SELECT FOR UPDATE`) | Redis 분산락 |
|---|---|---|
| 범위 | 트랜잭션 범위와 **자동 일치** | 별도 해제 호출 필요 |
| 해제 시점 | COMMIT/ROLLBACK 시 자동 | 개발자가 명시적 호출 또는 TTL |
| 장애 시 | DB 트랜잭션과 운명 공유 | TTL 만료 시 멋대로 풀림 |
| 인프라 | 단일 DB | 별도 Redis 클러스터 |
Redis lock의 장점은 분산 환경(여러 서비스/DB 간 동시성 제어)인데, 단일 DB 내 동시성 제어용으로 쓰면 트랜잭션 경계 불일치 문제가 생긴다.
실전 적용
해결 방안 4가지 (면접 답변 라인)
1. Lock 해제를 DB Commit 이후로 (Spring 패턴)
```java
@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 보유 시간 늘어남 → 동시성 ↓. 짧은 트랜잭션에만 권장.
2. DB 비관적 락 사용 (`SELECT FOR UPDATE`)
```java
@Repository
public interface AccountRepository extends JpaRepository
@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 부하 ↑, 분산 환경(여러 서비스)에는 부적합.
3. Idempotency Key + DB Unique Constraint
```java
@Entity
public class PaymentTransaction {
@Column(unique = true)
private String idempotencyKey; // 클라이언트가 전달
// ...
}
```
- Lock 자체를 안 쓰고 **멱등성으로 중복 방지**
- 결제 같은 케이스에 자주 쓰임 (Stripe, Toss 패턴)
트레이드오프: 클라이언트가 idempotency key 관리 필요. 락보다 깔끔하지만 책임이 클라이언트로 이동.
4. 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 | ☐ |
참고
- 2026-05-22 Raphael 모의 면접 — 본인 답변 "Redis lock으로 안전 장치" 후 면접관 질문 미해결
- Martin Kleppmann, "How to do distributed locking" — RedLock 논쟁 원문
- Spring TransactionPhase 공식 문서