포스트

Spring

Spring과 JPA에서 Race Condition을 다루는 방법

Race Condition이란

여러 요청이 동시에 같은 자원을 읽고 수정할 때, 의도하지 않은 순서로 처리되어 데이터 정합성이 깨지는 상황을 말한다.

대표 예시는 다음과 같다.

  • 재고 차감
  • 중복 결제 방지
  • 쿠폰 선착순 발급
  • 좌석 예약

조회와 갱신이 분리된 흐름에서는 특히 쉽게 발생한다. 재고 차감을 예로 들면, 두 요청이 같은 재고를 읽은 뒤 각자 계산한 값을 저장하면 한 번의 차감이 사라진다.

sequenceDiagram
    participant A as 요청 A
    participant DB
    participant B as 요청 B
    A->>DB: 재고 조회 (stock = N)
    B->>DB: 재고 조회 (stock = N)
    A->>DB: stock = N - 1 저장
    B->>DB: stock = N - 1 저장
    Note over DB: 두 번 팔렸지만 재고는 한 번만 줄었다

왜 JPA에서 자주 문제가 되는가

JPA를 쓰면 엔티티 변경이 자연스럽게 보이기 때문에 동시성 문제도 자동으로 해결될 것처럼 착각하기 쉽다. 하지만 JPA는 ORM일 뿐이고, 동시성 제어는 DB 락 전략과 애플리케이션 설계가 결정한다.

해결 전략

1. 낙관적 락

버전 컬럼을 두고, 업데이트 시점에 버전이 바뀌었는지 검사한다.

  • 장점: 읽기 경쟁이 많은 환경에 유리
  • 단점: 충돌이 나면 재시도 정책이 필요

@Version 기반으로 쉽게 적용할 수 있다. Jakarta Persistence는 이 필드를 엔티티의 낙관적 락 값으로 정의한다(Version Javadoc). 충돌 처리에 따라 설계가 갈리는 사례는 뱅크샐러드 낙관적 락 리뷰에 있다.

2. 비관적 락

조회 시점부터 DB 락을 잡는다.

  • 장점: 충돌을 강하게 제어 가능
  • 단점: 대기 시간과 데드락 가능성 증가

Spring Data JPA에서는 리포지토리 쿼리 메서드에 @Lock을 붙여 LockModeType을 지정한다(Locking). 재고처럼 “절대 동시에 성공하면 안 되는” 케이스에서 자주 쓴다.

3. 원자적 업데이트

애플리케이션에서 읽고 계산하고 저장하는 대신, DB에 조건부 업데이트를 맡긴다.

예:

1
2
3
update product
set stock = stock - 1
where id = ? and stock > 0

읽기, 조건 확인, 쓰기가 SQL 한 문장 안에서 일어나므로 위 그림처럼 두 요청이 같은 값을 읽고 덮어쓰는 틈이 없다. 가능한 경우 가장 먼저 검토할 만하다.

4. 큐잉 또는 직렬화

경쟁이 아주 심한 경우에는 아예 처리 순서를 직렬화하는 편이 더 안정적이다.

실무 판단 기준

  • 충돌이 자주 발생하는가
  • 재시도가 가능한가
  • 처리량보다 정합성이 더 중요한가
  • 같은 자원에 경쟁이 집중되는가

예를 들어 선착순 이벤트는 정합성이 우선이고, 일반 조회성 화면은 처리량이 우선일 수 있다.

흔한 실수

  • @Transactional만 붙이면 안전하다고 생각함
  • synchronized로 멀티 인스턴스 환경을 해결하려고 함
  • 락은 걸었지만 조회 범위가 넓어 병목이 심해짐

@Transactional은 원자성 보장과 동시성 제어를 동일하게 해결해주지 않는다. 위 그림의 경쟁을 어디까지 막는지는 격리 수준과 락이 정한다.

정리

Race Condition 대응은 락을 쓸지보다 어디서 경쟁이 발생하고 어떤 수준의 정합성을 보장해야 하는지를 먼저 정하는 문제다. 그다음 Spring/JPA에서는 낙관적 락, 비관적 락, 조건부 업데이트, 큐 기반 직렬화 중 문제 성격에 맞는 방식을 선택해야 한다.

참고

데이터베이스 내부와 트랜잭션 Spring과 JVM 백엔드
이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

변경이력

2번 수정

  1. docs(notes): cite sources and ease reading in how-to-control-race-condition, add a diagram
  2. docs(posts): rewrite spring notes collection

댓글

아직 댓글이 없습니다