들어가며
JPA 낙관락 vs 비관락 무엇을 사용해야할까? "낙관락 VS 비관락" 검색시 Google에 노출되어 있는 어느 블로그 글

"충돌이 많으면 비관락, 적으면 낙관락", "정합성 보장은 비관락, 그렇지 않으면 낙관락" 그래서 정합성을 반드시 보장해야하는 돈관련 데이터는 비관락을 사용한다라는 말도 들어봤다.
근데 락 자체가 정합성을 보장하는데, 락이 정합성을 보장 못하는 상황이 존재하나? 보장 못하면 그건 락이라고 말할 수 없지 않나..
대체 무엇을 기준으로 선택해야하는걸까. AI에게 물어봐도 충돌이 많으면 비관락을 사용하라고하는데, 오히려 데드락 때문에 낙관락이 낫지 않나? 몇시간 동안 찾아보고나서 이거는 사람마다 선택이 다다르고 진짜 답이 없다. 나의 기준을 만들어야겠다고 생각했다.
이 글은 흔히 들어본 락 선택 '기준'을 꺼내보고 왜 이게 기준이 되는지 생각해보며 기준을 버려가고 남는 기준들을 가지고 나의 기준을 만들어간 글이다.
시작 전에.. 둘이 왜 다른지부터
아래 글에서 두 락에 대한 정리를 잘해놔서 남겨놨다.
https://github.com/2024-woowacourse-study/level-interview/discussions/234
선택 기준을 논하기 전에 메커니즘부터 이야기해보자면
비관락은 트랜잭션이 시작되는 순간 DB가 해당 row에 물리적 Lock을 건다. 다른 트랜잭션은 이 Lock이 풀릴 때까지 대기한다. 충돌을 사전에 막는 방식이다. 이 락은 커넥션을 반납하면서 락도 같이 반납한다.
SELECT * FROM coupon WHERE id = 1 FOR UPDATE;
대기 중인 트랜잭션은 idle 상태다. CPU를 쓰지 않는다. 그냥 줄을 서 있다. 대신 줄을 서 있는 동안에도 DB 커넥션을 점유한다.
낙관락은 DB Lock을 걸지 않는다. 대신 커밋 시점에 version을 비교한다.
-- 읽을 때: version을 같이 가져온다
SELECT id, stock, version FROM coupon WHERE id = 1;
-- → version = 3 이라고 가정
-- 쓸 때: 내가 읽었던 version과 지금 version이 같을 때만 UPDATE
UPDATE coupon
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 3;
-- 영향받은 row가 0이면 그 사이 누군가 먼저 수정한 것 → 충돌
내가 읽은 시점의 version과 커밋 시점의 version이 다르면, 그 사이 누군가 먼저 수정했다는 뜻이다. UPDATE 결과가 0 rows affected로 돌아오고, 이걸 감지해서 예외를 던진다. 충돌을 사후에 감지하는 방식이다.
충돌한 TX의 처리방법은?
낙관락은 재시도가 재시도를 낳고 랜덤으로 락을 획득하는반면, 비관락은 충돌 없이 순차적으로 대기하고 락을 획득한다.
여기서 비관락일 때 다른 DB 커넥션들의 대기 순서를 보장할 수 있었던 이유는 HikariCP 코드를 잠깐 까보면
SynchronousQueue(true)라는 큐 구조와 함께 fair = true를 하여 FIFO 구조를 보장한다.
그리고 타임아웃 시간동안 블락킹 상태 - 대기상태로 만들고 락이 회수되면 대기 쓰레드를 깨우는 방식이다.
이 순서를 보장하는 건 비관락 자체가 아니라, HikariCP가 커넥션을 분배하는 방식이다. ConcurrentBag을 잠깐 들여다보면 이렇게 돼 있다.
// ConcurrentBag.java
private final SynchronousQueue<T> handoffQueue = new SynchronousQueue<>(true);
// true → FIFO 보장
SynchronousQueue에 fair = true를 주면 대기 중인 스레드가 FIFO 순서로 서비스된다. 커넥션이 반환되는 순간, 가장 먼저 기다리기 시작한 스레드에게 전달된다. 타임아웃 시간 동안 스레드는 블락킹 상태로 대기하고, Lock이 해제되면 그 스레드를 깨운다.
낙관락 충돌 시: 재시도 → 재경쟁 → 재시도 → 재경쟁 (순서 없음)
비관락 충돌 시: 대기 큐에 순번 등록 → FIFO로 획득 (순서 있음)
이제 기준들을 하나씩 꺼내보자 — 그리고 버리자
첫 번째 후보 - 정합성
강한 정합성을 보장하는게 비관락이라고..? 이게 맞는 기준인가? 락이란 뭐지? 낙관락이 정합성을 못 지키나?
락을 거는건 내 사용시점에 다른사람들이 내 데이터에 접근하지 못하게 막는거다. 따라서 락이 걸면 잠겨있는동안 자기 트랜잭션 세션 외에 다른 데이터가 접근하지 못한다. 즉 데이터가 꼬이지 않는 다는 이야기이고 여기서 낙관락일때 누가 데이터를 수정하려고하면 충돌이 발생하고 예외를 던진다. 다른 블로그들에서 말하는 강한 정합성 약한 정합성이란 뭘까.. 정합성이라는것에 강도가 있는걸까? 진짜 몰라서 궁금하다. 정리하면 예외가 터지는 것은 정합성이 약하다라고 볼수 없다.
정합성은 둘 다 보장한다. 정합성이 어떤 락을 선택하는 기준이 될 수 없다고 판단했다.
두 번째 후보 - 데이터 성격
"돈 관련 데이터면 비관락 써야 하는 거 아닌가? 중요한 데이터니까."
이 말을 들었을 때 직관적으로는 어 그런가 했는데 근데 왜 그런가를 물어보면 막힌 내용이다.
비관락이 낙관락보다 더 정확한 데이터를 만드나? 아니다. 정합성은 이미 첫 번째에서 지웠다.
단, 외부 결제 API 호출처럼 멱등성이 없는 작업이 트랜잭션 안에 섞여 있으면 재시도 자체가 위험할 수 있다. 그런데 이건 데이터 성격 때문이 아니라, 재시도 구조가 설계상 불가능한 케이스다. 이 경우엔 비관락도 타임아웃 시 동일하게 실패한다. 락 선택보다 트랜잭션 설계를 먼저 봐야 할 문제다.
정합성과 같은 맥락으로 데이터 성격은 기준이 아니다. 지운다.
세 번째 후보 - 100% 요청 보장
이거는 AI랑 이야기하다가 비관락은 요청을 보장하는 것에 차이가 있다고 해서 언급해봤다.
그런데 비관락은 Lock wait timeout이 있고 대기 시간이 임계치를 넘으면 실패하고 예외를 던진다
마찬가지로 낙관락도 재시도 횟수나 시간 간격이 임계치를 넘으면 실패하고 예외를 던진다.
그래서 둘의 타임아웃과 재시도 한도를 동일 조건으로 맞추면, "응답이 느려지면 실패한다"는 본질은 둘 다 똑같다.
재시도 가능 여부는 설계로 조절하는 영역이다. 기준에서 지운다.
네 번째 후보 - 충돌 빈도
충돌이 나면 두 락은 어떻게 다르게 행동하나
낙관락은 재시도가 재시도를 낳고 랜덤으로 락을 획득하는반면, 비관락은 충돌 없이 순차적으로 대기하고 락을 획득한다.
여기서 비관락일 때 다른 DB 커넥션들의 대기 순서를 보장할 수 있었던 이유는 HikariCP 코드를 잠깐 까보면
SynchronousQueue(true)라는 큐 구조와 함께 fair = true를 하여 FIFO 구조를 보장한다.
그리고 타임아웃 시간동안 블락킹 상태 - 대기상태로 만들고 락이 회수되면 대기 쓰레드를 깨우는 방식이다.
이 순서를 보장하는 건 비관락 자체가 아니라, HikariCP가 커넥션을 분배하는 방식이다. ConcurrentBag을 잠깐 들여다보면 이렇게 돼 있다.
// ConcurrentBag.java
private final SynchronousQueue<T> handoffQueue = new SynchronousQueue<>(true);
// true → FIFO 보장
SynchronousQueue에 fair = true를 주면 대기 중인 스레드가 FIFO 순서로 서비스된다. 커넥션이 반환되는 순간, 가장 먼저 기다리기 시작한 스레드에게 전달된다. 타임아웃 시간 동안 스레드는 블락킹 상태로 대기하고, Lock이 해제되면 그 스레드를 깨운다.
낙관락 충돌 시: 재시도 → 재경쟁 → 재시도 → 재경쟁 (순서 없음)
비관락 충돌 시: 대기 큐에 순번 등록 → FIFO로 획득 (순서 있음)
그렇다면 여기서 질문이 생긴다. 비관락처럼 순서 있게 대기하는 게 낙관락의 랜덤 재경쟁보다 항상 나은가?
"충돌 많으면 비관락"이라는 말은 어디서 나왔나
낙관락은 충돌이 많으면 계속 재시도가 일어나고 재시도가 재시도를 낳는다.
[트랜잭션 A] [트랜잭션 B]
SELECT (version=1) SELECT (version=1)
... 작업 ... ... 작업 ...
UPDATE SET version=2 ✅ UPDATE SET version=2 ❌ (이미 2)
→ OptimisticLockException
→ SELECT (version=2) ← 재시도 시작
... 작업 ...
UPDATE SET version=3 ❌ (또 다른 트랜잭션)
→ 또 재시도 ...
충돌이 많으니까 재시도 없는 비관락을 사용하자는 의견인 것 같다.
그렇다면 충돌이 많을 때 비관락이 진짜 낫나
충돌이 많아지면 비관락이 낫다는 게 정확히 무슨 의미인가.
비관락의 대기는 idle이다. CPU를 쓰지 않는다. 대신 대기하는 동안 DB 커넥션을 점유한다.
반면 낙관락의 재시도는 실제 연산이다. CPU와 DB I/O를 쓴다. 대신 커넥션은 재시도 시점에만 잠깐 사용한다.
충돌이 많아진다는 건, 이 두 비용이 각각 더 많이 쌓인다는 뜻이다. 비관락 쪽엔 커넥션 점유가 쌓이고, 낙관락 쪽엔 CPU/IO 연산이 쌓인다.
| 비관락 | 낙관락 | |
| 처리량 감소 원인 | 순차 대기 — 한 일은 버려지지 않음 | 재시도마다 헛된 연산 누적 — 버려지는 작업이 쌓임 |
| 자원 소모 방식 | idle 대기 → 커넥션 점유 | 재시도 연산 → CPU/IO 소모 |
충돌이 높아졌을 때 비관락의 커넥션 누적이 더 견딜만한지, 낙관락의 CPU/IO 누적이 더 견딜만한지?
충돌 빈도는 "어떤 자원이 더 비싸게 쌓이느냐"를 결정하는 변수다. 어떤 자원이 더 위험한지는 서버마다 다르다. 충돌 빈도 하나만으로 선택을 정하기 어렵다.
정리하면, 충돌 빈도는 비용 계산의 변수다. 결정적 기준이 아니다.
그래서 뭐가 남나
이쯤 와서 보면, 비관락이 소모하는 자원과 낙관락이 소모하는 자원은 종류가 다르다는 것을 알수 있다.
비관락이 소모하는 자원:
- DB 커넥션 (대기 중에도 점유)
- WAS 스레드 (block 상태)
낙관락이 소모하는 자원:
- CPU (재시도 연산)
- DB I/O (재시도 시에만)
- 커넥션은 재시도 시점에만 잠깐 사용
비관락의 대기는 DB 커넥션을 계속 점유한다. 충돌이 잦아질수록 커넥션이 쌓아고 연쇄 장애로 커넥션 풀 고갈로 갈 수 있다.
커넥션 풀 10개
→ 10개 트랜잭션 전부 Lock 대기 중
→ 11번째 요청 → 커넥션 없음 → 타임아웃
→ WAS 스레드 풀로 전파 → 시스템 전체 장애
낙관락은 재시도 시점에만 커넥션을 잠깐 쓴다. 커넥션을 붙잡지 않는다. 대신 재시도마다 CPU와 DB I/O를 쓴다.
나의 비관락 vs 낙관락 선택 기준을 세워봤다.
락 선택 기준1 - 내 서버에서 어떤 자원이 여유 있는가?
결론은 더 여유 있는 자원 쪽으로 부하를 밀어넣는 것이다.
커넥션 풀이 병목인 서버 → 낙관락
CPU / IO가 병목인 서버 → 비관락
대부분의 서비스에서 DB 커넥션은 CPU보다 훨씬 비싸다고한다. 고갈 시 파급 효과도 크다.
그래서 나는 낙관락을 기본값으로 선택했다. 충돌이 적을때는 DB락을 사용안해서 좋고, 충돌이 많을때는 DB커넥션이 쌓여서 생기는 문제를 애플리케이션단에서 해결할 수 있고, 단, 모니터링을 해봤을 때 커넥션풀이 남아돌고 CPU 사용량이 높다면 이때 비관락으로 내릴 것 같다.
락 선택 기준2 - 운영 요구사항
만약 락 잡힌 한번의 상황에서 반드시 처리해야한다? 그러면 비관락을 사용한다.
만약 한건 처리 완료되고 전부 튕겨내야 하는 요구사항이 있다면 낙관락을 사용할 것 같다.
만약 선착순 쿠폰 발급을 구현해야하고 정말 클릭 순서대로 보장을 해야한다면, 만약 단일서버에서는 낙관락으로 재시도루프 돌려서(유저 입장에서는 내부 동작모르고, 선착순처럼 느껴지니까) 해결할것같고, 그런데 무조건 선착순이여야한다면 그러면 비관락인데 단일서버에서 커넥션 고갈가능성이 높으니 별도 선착순 서버로 분리하거나 이거는 락을 해결하기 어렵고 대기열 시스템을 추가해야하지 않을까
마치며
이 글에서 하나씩 지워온 기준들을 정리하면 아래와 같다.
| 기준 | 판정 | 이유 |
| 정합성 | ❌ | 둘 다 보장 |
| 데이터 성격 | ❌ | 관계없음. 트랜잭션 설계 문제 |
| 충돌 빈도 | △ | 비용 계산의 변수일 뿐, 결정적 기준 아님 |
| 재시도 가능 여부 | ❌ | 설계로 조절 가능 |
| 타임아웃 | ❌ | 둘 다 있음 |
| 어떤 자원을 소모하며 대기하는가 | ✅ | 설계로 바꿀 수 없는 유일한 차이 |