이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 비관적 락, 낙관적 락, Unique 제약, 원자적 UPDATE는 언제 선택하는가?
- 동시성 제어를 애플리케이션 코드만으로 하면 왜 위험한가?
- 재고, 중복 요청, 상태 전이에서 어떤 패턴을 써야 하는가?
개요
동시성 제어는 격리 수준 하나로 끝나지 않는다. 실무에서는 비관적 락, 낙관적 락, 원자적 update, Unique 제약, 상태 전이 조건, 멱등 키, 큐잉을 조합한다.
좋은 선택은 충돌 빈도, 데이터 중요도, 사용자 경험, 재시도 가능성에 따라 달라진다.
원리
동시성 제어 패턴의 원리는 “락을 걸 것인가 말 것인가”가 아니라 어떤 불변식을 어느 계층에서 가장 짧고 강하게 지킬지 선택하는 것이다. 중복 방지는 Unique 제약이 강하고, 단일 row 수량 차감은 조건부 update가 단순하며, 충돌이 잦은 순차 처리는 비관적 락이나 큐잉이 필요할 수 있다.
백엔드 개발자는 패턴을 이름으로 외우기보다 충돌 빈도, 실패 시 사용자 경험, 재시도 가능성, 감사/복구 필요성, hot row 여부를 기준으로 고른다. 같은 숫자 변경이라도 재고, 포인트, 조회수는 요구사항이 다르다.
Unique 제약
중복을 막는 가장 강한 기본 패턴이다.
CREATE TABLE post_likes (
post_id bigint NOT NULL,
member_id bigint NOT NULL,
created_at timestamp NOT NULL DEFAULT now(),
PRIMARY KEY (post_id, member_id)
);좋아요 중복은 애플리케이션에서 exists로 검사해도 race condition이 생긴다. DB Unique 제약이 최종 방어선이어야 한다.
원자적 UPDATE
재고 차감에는 원자적 조건 update가 단순하고 강력하다.
UPDATE products
SET stock = stock - 1
WHERE id = :productId
AND stock > 0;영향 받은 row 수가 1이면 성공, 0이면 재고 부족이다. 별도 select 후 update보다 짧고 안전하다.
int updated = productRepository.decreaseStock(productId);
if (updated == 0) {
throw new SoldOutException();
}비관적 락
충돌 가능성이 높고 반드시 순차 처리해야 하면 비관적 락을 쓴다.
SELECT id, balance
FROM accounts
WHERE id = :accountId
FOR UPDATE;다른 트랜잭션의 변경을 기다리게 하므로 정합성은 강하지만 지연과 deadlock 가능성이 있다. 트랜잭션을 짧게 유지해야 한다.
낙관적 락
충돌이 드물고 재시도 가능한 경우에는 version 컬럼을 둔다.
UPDATE articles
SET title = :title,
version = version + 1
WHERE id = :id
AND version = :version;업데이트 row 수가 0이면 다른 트랜잭션이 먼저 수정한 것이다. 사용자에게 충돌을 알리거나 재조회 후 재시도한다.
JPA에서는 @Version을 사용할 수 있다.
@Version
private Long version;낙관적 락은 충돌을 막는 것이 아니라 충돌을 감지한다. 충돌 후에는 사용자에게 “다른 사용자가 먼저 수정했습니다”를 보여줄지, 최신 데이터를 다시 읽고 자동 병합할지, command를 다시 시도할지 정책이 필요하다.
상태 전이 조건
상태 변경은 현재 상태 조건을 함께 둔다.
UPDATE orders
SET status = 'CANCELED'
WHERE id = :orderId
AND status = 'CREATED';이미 결제된 주문을 취소하면 안 되는 규칙을 애플리케이션 if문만이 아니라 DB update 조건에 반영한다.
선택 기준을 요약하면 다음과 같다.
| 상황 | 우선 검토할 패턴 |
|---|---|
| 사용자당 좋아요 1회 | Unique 제약 |
| 재고/쿠폰 수량 차감 | 조건부 원자 update |
| 문서 수정 충돌 감지 | 낙관적 락 + 사용자 충돌 처리 |
| 계좌 이체처럼 순서가 중요한 변경 | 비관적 락 + 짧은 트랜잭션 |
| 인기 상품처럼 hot row가 과열 | 큐잉, 토큰 분산, 사전 할당 |
| 외부 재시도 가능 API | idempotency key + Unique 제약 |
실전 팁
- 중복 방지는 Unique 제약을 먼저 생각한다.
- 단순 수량 차감은 원자적 update를 검토한다.
- 충돌이 잦고 순차 처리가 필요하면 비관적 락을 쓴다.
- 충돌이 드물고 사용자 재시도가 가능하면 낙관적 락을 쓴다.
- 상태 전이는 현재 상태 조건을 SQL에 포함한다.
- 패턴 선택 전에 “막아야 하는 불변식”을 문장으로 쓴다. 예를 들어 “상품 재고는 0 미만이 될 수 없다”처럼 표현한다.
- JVM lock은 단일 인스턴스 안에서만 의미가 있으므로 다중 서버에서는 DB 제약, 분산락, 큐 중 어느 경계가 맞는지 다시 본다.
- hot row가 의심되면 lock wait, update latency, deadlock, retry count, queue lag를 같이 본다.
위험 신호!
select 후 if 검사 후 insert로 중복을 막는다.- 재고를 읽고 애플리케이션에서 계산한 뒤 update한다.
- 모든 동시성 문제를
synchronized로 해결하려 한다. - 여러 서버 인스턴스가 있는데 JVM lock만 믿는다.
- 낙관적 락 실패를 사용자에게 어떻게 보여줄지 정하지 않는다.
- 비관적 락을 걸고 외부 API를 호출한다.
- 낙관적 락 실패를 무한 재시도해 사용자 요청 지연을 키운다.
- hot row 문제를 인덱스 추가로 해결하려고만 한다.
확인 질문
- Unique 제약이 중복 방지에 강한 이유는 무엇인가?
- 동시 요청이 애플리케이션 검사를 통과해도 DB가 최종 저장 시점에서 중복을 막을 수 있기 때문이다.
- 원자적 UPDATE가 재고 차감에 유용한 이유는 무엇인가?
- 조건 확인과 변경을 하나의 SQL로 수행해 race condition을 줄이고 성공 여부를 영향 row 수로 판단할 수 있기 때문이다.
- 낙관적 락은 언제 적합한가?
- 충돌이 드물고 충돌 시 사용자 재시도나 재조회가 가능한 수정 작업에 적합하다.
- 동시성 패턴을 고르기 전에 먼저 정해야 하는 것은 무엇인가?
- 어떤 비즈니스 불변식을 지켜야 하는지, 충돌 시 재시도 가능한지, 실패를 사용자에게 어떻게 보여줄지, hot row가 될 가능성이 있는지 정해야 한다.