"충돌은 드물다"고 보고 잠그지 않고 진행한 뒤, 저장 시점에 검증하는 전략.
방법 — 버전 컬럼
-- 읽을 때 version 을 함께 가져온다 (version = 3)
SELECT id, qty, version FROM stock WHERE id = 1;
-- 저장할 때 그 version 이 그대로인지 조건에 넣는다
UPDATE stock SET qty = 9, version = 4
WHERE id = 1 AND version = 3;
-- 영향받은 행이 0이면 → 그 사이 누가 바꿨다 → 충돌
핵심은 WHERE version = 3 이다. 내가 읽은 뒤 아무도 안 바꿨을 때만 성공한다.
JPA에서
@Entity
class Stock {
@Version
private Long version; // 이 필드만 두면
}
하이버네이트가 UPDATE에 AND version = ? 를 자동으로 붙이고, 0행이면
OptimisticLockException을 던진다.
언제 유리한가
충돌이 드물 때. 락 대기가 전혀 없어 처리량이 높다.
특히 락을 오래 쥘 수 없는 흐름에 필수적이다.
사용자가 편집 화면을 열고 → 10분간 작성 → 저장
↑ 이 10분 동안 DB 락을 쥘 수는 없다
→ 낙관적 락으로 "그 사이 남이 바꿨으면 알려 주기"
이것을 오프라인 낙관적 락이라 한다. 웹 애플리케이션의 전형적인 패턴이다.
대가
- 충돌 시 재시도 로직이 필요하다. 몇 번 시도할지, 실패하면 사용자에게 뭐라 할지.
- 경쟁이 심하면 재시도가 반복돼 오히려 느려진다(라이브 커머스 선착순 등).
재시도는 트랜잭션 밖에서
// ❌ 같은 트랜잭션 안에서 재시도하면 이미 오염된 영속성 컨텍스트를 재사용
@Transactional
void order() { for (…) { try { … } catch (…) { } } }
// ✅ 트랜잭션을 새로 열어 재시도
@Retryable(retryFor = OptimisticLockException.class, maxAttempts = 3)
@Transactional
void order() { … }
면접 꼬리질문
"낙관적 락은 DB 락을 전혀 안 쓰나?"
→ UPDATE ... WHERE version = ? 자체는 여전히 행 락을 잡는다.
다만 잠기는 시간이 UPDATE 실행 순간으로 극히 짧다는 점이 다르다.
"락을 안 쓴다"가 아니라 "락 구간이 짧다" 가 정확한 표현이다.