동시성 문제가 생기는 이유
한 줄 정의
동시성 문제는 둘 이상의 요청이 같은 공유 자원을 동시에 읽고 쓰면서, 실행 순서에 따라 결과가 달라지는 문제다.
실무에서 왜 문제 되는가
- 재고가 1개인데 두 요청이 모두 주문에 성공할 수 있다.
- 같은 쿠폰이 여러 번 발급되거나 같은 사용자가 중복 가입될 수 있다.
- 포인트 차감, 결제 상태 변경, 선착순 이벤트처럼 하나의 상태를 여러 요청이 동시에 바꾸면 데이터가 깨진다.
- 단일 요청 테스트에서는 통과하지만 운영 트래픽에서만 간헐적으로 발생한다.
동작 원리
- 요청 A와 요청 B가 같은 데이터를 거의 동시에 조회한다.
- 두 요청 모두 현재 상태가 유효하다고 판단한다.
- 각 요청이 독립적으로 변경 값을 계산한다.
- 나중에 저장한 요청이 먼저 저장한 결과를 덮어쓰거나, 중복 변경을 만든다.
- 결과적으로 업무 불변식이 깨진다.
실무 판단 기준
| 상황 | 먼저 확인할 것 | 이유 |
|---|---|---|
| 같은 row를 여러 요청이 수정 | lost update 가능성 | 조회 후 저장 패턴이 위험할 수 있다 |
| 중복 생성이 문제 | unique key | 애플리케이션 if문보다 DB 제약이 강하다 |
| 상태가 순서대로 바뀌어야 함 | 상태 전이 조건 | 이미 처리된 상태를 다시 처리하면 안 된다 |
| 외부 요청 재시도 포함 | 멱등성 key | 재시도와 동시 요청이 함께 올 수 있다 |
| 다중 인스턴스 환경 | JVM lock 한계 | synchronized는 한 프로세스 안에서만 보호한다 |
자주 나는 실수
- 먼저 조회하고 나중에 저장하는 로직이 항상 안전하다고 생각한다.
if (stock > 0)검증만으로 재고 정합성을 지킬 수 있다고 생각한다.- 서버가 여러 대인데
synchronized나 in-memory lock으로 해결하려 한다. - DB 제약 조건 없이 애플리케이션 코드로만 중복을 막는다.
- 동시성 테스트 없이 단일 요청 테스트만 작성한다.
확인 방법
- 테스트: 같은 API를 동시에 여러 번 호출해 최종 DB 상태를 검증한다.
- 로그: 요청 id, 대상 자원 id, 조회 값, 변경 값, 저장 결과를 남긴다.
- 메트릭: lock wait, deadlock, retry count, 중복 실패 count를 본다.
- DB 제약: unique key, version column, check constraint가 있는지 확인한다.
장점과 한계
| 해결 접근 | 장점 | 한계 |
|---|---|---|
| DB 제약 조건 | 최종 방어선이 강하다 | 예외 처리와 사용자 응답 설계가 필요하다 |
| 조건부 update | 단일 SQL로 경쟁 조건을 줄인다 | 복잡한 검증에는 한계가 있다 |
| 락 | 임계 구역을 명확히 보호한다 | 대기, 데드락, 처리량 저하가 생길 수 있다 |
| 멱등성 | 중복 요청과 재시도에 강하다 | key 저장과 만료 정책이 필요하다 |
짧은 예제
@Transactional
public void unsafeDecreaseStock(Long productId, int quantity) {
Product product = productRepository.findById(productId).orElseThrow();
if (product.getStock() < quantity) {
throw new SoldOutException();
}
product.decreaseStock(quantity);
}이 코드는 단일 요청에서는 정상처럼 보인다. 하지만 두 요청이 동시에 같은 재고를 조회하면 둘 다 검증을 통과할 수 있다. 이런 흐름은 조건부 update, 비관적 락, 낙관적 락 중 하나로 보호해야 한다.
핵심 요약
동시성 문제는 여러 요청이 같은 공유 자원을 동시에 바꿀 때 발생한다.
핵심은 “어떤 자원이 공유되는가”와 “어떤 불변식이 깨지는가”를 찾는 것이다.
단일 요청 테스트가 통과해도 동시 요청에서는 결과가 달라질 수 있다.
실무에서는 lock만 생각하지 말고 DB 제약, 조건부 update, 상태 전이, 멱등성을 함께 검토한다.
해결책은 정합성 요구사항과 경합 빈도, 재시도 가능성에 따라 선택한다.
꼬리 질문
- 동시성 문제는 왜 단위 테스트에서 잘 드러나지 않는가?
synchronized는 다중 서버 환경에서 왜 한계가 있는가?- 조회 후 저장 패턴이 위험한 경우는 무엇인가?