락 - 낙관적 락과 비관적 락
왜 락이 필요한가?
재고 1개에 두 차감 요청이 동시에 읽고, 각자 갱산하다면?
-> 초과 판매가 될 수 있다. (Lost Update)
두 가지 락이 존재.
- 낙관적 락 : 충돌이 드물때 사용, 일단 진행하고 커밋때 검증
사고가 나면 그때 처리하는 것이다.
- 비관적 락 : 충돌이 자주날 때 사용, 미리 잠그고 시작
아예 접근을 불가하게 막는것이다.
=> 충돌 빈도에 따라 다르다.
낙관적 락
데이터를 읽을 때 버전과 같이 읽는다.
갱신 요청을 보낼 때 버전을 확인해서 그대로인지 확인한다.
- 버전이 같으면? : 성공 + 버전을 올린다.
- 버전이 다르면? : 누가 먼저 수정을 했다는 의미! -> 충돌 예외 발생
실제 락을 걸지않고, 락 효과를 기대하는 것
비관적 락
트랜잭션이 데이터를 읽는 즉시 행에 락을 건다.
인덱스가 없으면, 전체 LOCK 을 걸 가능성도 존재한다.
B는 A 가 락을 쥐는동안 접근 못하고 대기한다.
A가 커밋할 때 락을 풀면, B가 락을 얻어 로직을 진행한다.
충돌 자체를 원천 차단
락 in JPA
스프링 코드에선 어떻게 제공해주는지 살펴보자.
낙관적 락
@Version
private long version;JPA 가 어노테이션 기반으로 갱신시 버전 조건을 자동으로 추가해준다.
버전이 바뀌어 있으면, OptimisitcLockException 발생
충돌시 예외를 던지므로, 재시도 전략이 필수적이다.
(@Retryable 로 짧은 간격을 두고 재시도)
단, 무한 재시도는 절대적 금지다! 절대 해결되지 못하는데 부하만 유발할 수 있기 때문이다.
비관적 락
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from Product p where p.id = :id")
public findByIdLokc(long id);PESSIMISTIC_WRITE 어노테이션을 명시하면 ... for update 구문이 붙는다. (X 락 획득)
조회 순간부터 락을 쥐므로, 다른 트랜잭션은 행을 건드리지 못하고 대기한다.
비관적 락 종류
- READ LOCK : 공유 락(S LOCK), 다른 트랜잭션은 읽기만 가능 + 쓰기는 대기
- WRITE LOCK : 베타 락(X LOCK), 다른 트랜잭션은 읽기/쓰기 모두 대기
낙관적 락 vs 비관적 락 비교
| 비교 항목 | 낙관적 락 (Optimistic Lock) | 비관적 락 (Pessimistic Lock) |
|---|---|---|
| 락 적용 시점 | 데이터 수정 후 커밋(Commit)하는 시점에 충돌 검증 | 데이터를 조회(Select)하는 즉시 락을 획득 |
| 처리 성능 | 충돌이 적으면 락을 걸지 않으므로 매우 빠름 | 락 대기 상태로 인해 전체적인 처리량(Throughput) 저하 |
| 충돌 발생 시 | 예외(Exception)가 발생하며, 개발자가 재시도 로직을 구현해야 함 | 선행 작업이 끝날 때까지 대기 후 순차적으로 처리됨 |
| 데드락 위험 | 낮음 (충돌 시 트랜잭션이 취소/재시도되므로 자연스레 해결) | 상대적으로 높음 (락을 획득하는 순서에 따라 교착 상태 가능) |
| 적합한 케이스 | • 읽기(조회) 요청이 대부분인 시스템• 업데이트 충돌 가능성이 낮은 경우 | • 데이터 정합성(쓰기 보장)이 극도로 중요한 시스템• 업데이트 충돌이 빈번하게 발생하는 경우 |
- 충돌이 잦을 경우 재시도 횟수 폭증으로 오버헤드 발생 가능
- 충돌이 잦아도 안정적이나, 대기 지연 및 데드락 위험 감수 필요
선택기준
충돌 빈도와 비용을 고민하자.
(트래픽 패턴에 따라 달라질 수도 있다.)
- 게시글 수정(충돌 가능성이 낮음) : 낙관적 락
- 인기 상품 수량 차감 : 비관적 락
- 충돌 시 재시도 부담이 큰 경우 : 비관적 락
- 처리량이 매우 중요한 경우 : 낙관적 락을 우선적 검토
비관적 락 주의
비관적 락은 결국 행을 잠구고, 행 관련 모든 요청을 대기시키는 것이다.
외부 API 를 호출한다면, 모든 요청이 대기한다.
-> 처리량이 일렬로 직렬화되며 무너진다 (하나 둘 모두 대기)
=> 락 구간은 짧게 + 락 타임아웃을 통한 무한 대기 방지
재고 차감 로직은 LOCK 이 아닌, DB 원자적 갱신도 고려할 수 있다!
락이 항상 정답은 아닐수도 있다.