요즘 WMS를 손보면서 가장 오래 붙잡고 있는 문제는 재고의 정확성보다도 배타 락이 길어질 때 속도가 어디까지 무너지는가다. 입출고가 몰리는 시간에는 재고 한 행을 안전하게 지키려는 선택이, 다음 작업을 줄 세우는 병목이 되기 쉽다.
짧은 영상 하나가 이 고민을 잘 압축했다. 두 요청이 재고 1을 함께 읽고 둘 다 차감하면 음수 재고가 될 수 있다. 그렇다고 모든 재고 행을 먼저 잠그면 처리량이 떨어진다. 결국 비관적 락과 낙관적 락의 선택은 이름이 아니라 같은 재고를 동시에 건드리는 빈도에서 출발해야 한다.
연관 글
요약
비관적 락은 충돌이 날 것이라 보고 먼저 잠가 대기시키는 방식이고, 낙관적 락은 잠그지 않고 버전이 바뀌지 않았을 때만 갱신하는 방식이다. WMS에서 특정 SKU나 특정 로케이션에 작업이 몰리는 구조라면, 단순히 낙관적 락으로 바꾸는 것만으로는 빨라지지 않는다. 먼저 조건부 재고 차감으로 문제를 좁히고, 실제 충돌률·재시도율·락 대기 시간을 보고 경계를 나눠야 한다.
목차
이 글에서 다루는 내용
WMS에서 락이 갑자기 비싸지는 순간
재고 조회와 차감이 분리된 흐름은 위험하다. 같은 수량을 두 트랜잭션이 읽은 뒤 각각 업데이트하면 마지막 저장이 앞선 결과를 덮거나, 업무 규칙상 허용하면 안 되는 출고가 함께 확정될 수 있다. 영상에서 말한 음수 재고 사례가 바로 이것이다.
비관적 락은 보통 DB의 행 잠금을 이용해 이 구간을 직렬화한다. Hibernate의 PESSIMISTIC_WRITE는 명시적 락을 요청하고, DB에 따라 SELECT … FOR UPDATE 계열 SQL이 사용될 수 있다. 정확성은 분명하지만 트랜잭션 안에 외부 API, 긴 검증, 화면 입력 대기가 들어가면 락 보유 시간이 그대로 대기열이 된다.
비관적 락과 낙관적 락은 무엇이 다른가
| 비교 기준 | 비관적 락 | 낙관적 락 |
|---|---|---|
| 기본 가정 | 충돌이 발생할 수 있으니 먼저 막는다. | 충돌은 드물다고 보고 일단 진행한다. |
| 핵심 동작 | 읽거나 갱신할 때 행 잠금을 잡고 커밋까지 보유한다. | @Version 값을 조건에 넣어, 바뀌었으면 갱신 실패로 돌려준다. |
| 비용이 드러나는 곳 | 락 대기, 긴 트랜잭션, 교착 상태 가능성. | 충돌 후 재조회·재계산·재시도와 실패 응답 처리. |
| 잘 맞는 장면 | 같은 SKU·로케이션을 동시에 처리하는 피크 구간처럼 충돌이 잦을 때. | 서로 다른 품목을 주로 만지고 같은 행 경합이 드문 일반 작업. |
락 선택 차트: 충돌 빈도를 먼저 본다
정량 성능 그래프가 아니라, WMS 재고 갱신 경로를 나눠 볼 때의 판단 차트다.
낙관적 락
처음에는 잠그지 않는다.
버전 확인으로 충돌을 감지하고, 실패 시 최신 상태에서 다시 판단한다.
대기 비용보다 재시도 비용이 작은 경로에 맞는다.
비관적 락
먼저 행을 잠그고 순서를 보장한다.
락 보유 구간을 짧게 유지해 같은 재고의 경합을 직렬화한다.
재시도 폭주보다 대기가 예측 가능한 경로에 맞는다.
JPA에서 낙관적 락은 보통 엔티티에 @Version을 둔다. 갱신 SQL은 버전도 조건으로 삼기 때문에, 이미 누군가 바꿨다면 영향을 받은 행 수가 0이 되고 애플리케이션은 충돌을 알 수 있다.
@Version
private Long version;
// 개념적으로는 UPDATE stock
// SET quantity = ?, version = version + 1
// WHERE id = ? AND version = ?
재고 차감에서는 락보다 먼저 볼 것
수량만 줄이는 작업이라면 읽기 → 계산 → 쓰기를 꼭 애플리케이션에 나눠 둘 이유가 없는 경우가 많다. 아래처럼 조건부 갱신 하나로 “수량이 충분할 때만 차감”을 DB에 맡기면, 먼저 성공한 요청만 1행을 갱신하고 나머지는 0행 결과로 품절·재조회 흐름으로 보낼 수 있다.
UPDATE stock
SET quantity = quantity - :requestedQty
WHERE stock_id = :stockId
AND quantity >= :requestedQty;
이것은 모든 업무를 해결하는 만능 락이 아니다. 할당 규칙, 로트·유통기한 선택, 여러 재고 행의 일관성처럼 결정해야 할 것이 많아지면 트랜잭션 경계와 락 순서가 다시 중요해진다. 그래도 WMS의 단순 차감 경로에서는 긴 배타 락을 없애기 전에 원자적 SQL로 줄일 수 있는 왕복이 있는지부터 확인하는 편이 낫다.
실제로 막히는 지점: 낙관적 락의 재시도 폭주
낙관적 락은 충돌이 적을 때 대기 없이 지나가서 매력적이다. 하지만 한 인기 품목이나 한 출고 로케이션에 요청이 몰리면 실패한 요청들이 동시에 다시 읽고 다시 갱신한다. 영상의 “재시도가 재시도를 부른다”는 표현이 정확하다. 무제한 자동 재시도는 DB 쿼리와 애플리케이션 CPU만 더 쓰고, 사용자에게는 느린 성공이나 뒤늦은 실패를 남긴다.
- 단순 수량 차감은 짧은 횟수의 재시도나 조건부 UPDATE 결과 처리로 수습할 수 있다.
- 작업자 선택, 로트 배정, 피킹 우선순위가 들어간 명령은 최신 상태로 다시 판단해야 한다. 이전 결정을 그대로 재전송하면 안 된다.
- 동일 요청의 중복 전송은 락과 별개다. 출고 지시·확정 API에는 멱등 키와 상태 전이가 필요하다.
내가 먼저 볼 지표와 선택 기준
지금 겪는 속도 이슈를 “비관적 락이 느리다”로만 결론내리지는 않으려 한다. 우선 SKU·로케이션별 동시 갱신 수, DB의 락 대기 시간, 트랜잭션 평균/상위 보유 시간, 낙관적 락 충돌률과 재시도 횟수를 같이 보려 한다. 같은 품목에 몰린다면 비관적 락을 짧은 DB 작업 안에서 쓰거나 작업을 분산시키는 편이 낫고, 충돌이 드물다면 낙관적 락이 불필요한 대기를 없애준다.
중요한 것은 락을 잡은 채 외부 호출을 하지 않는 것이다. 피킹 검증, 라벨 발행, ERP 연동처럼 시간이 튈 수 있는 일은 재고 확정 트랜잭션 밖으로 빼고, 재고 행을 잠그는 순서도 여러 행에서 일관되게 정해야 교착 상태를 줄일 수 있다.
결론
비관적 락과 낙관적 락 중 무엇이 더 빠른지는 고정된 답이 아니다. 충돌이 드문 WMS 경로에는 낙관적 락이, 같은 재고에 작업이 집중되는 경로에는 짧고 명확한 비관적 락 또는 원자적 조건부 갱신이 더 현실적일 수 있다. 이번 개선에서는 락 종류를 일괄 교체하기보다, 재고 경합이 실제로 높은 경로부터 분리해 측정하는 것이 먼저다.