판매 개시 순간의 고경합
여러 요청이 같은 등급의 잔여 수량을 동시에 조회하고 임시 예약을 시도한다.
방지 대상: 초과 판매Spring Boot 4 · Kotlin · Java 25
판매 개시 직후 한정된 재고에 요청이 집중되는 상황에서 대기열, 재고 임시 예약, 결제 결과 확인, 취소·환불, 티켓 발급을 하나의 일관된 상태 모델로 처리하는 예제다.
01 · 예제 구상
단순 재고 차감 예제는 정상 처리만 보여 준다. 이 예제는 동일 사용자와 IP의 반복 요청, 결제 응답 유실, 취소와 지연된 승인, 보조 저장소 장애가 함께 발생하는 시나리오를 사용한다.
여러 요청이 같은 등급의 잔여 수량을 동시에 조회하고 임시 예약을 시도한다.
방지 대상: 초과 판매동일한 구매 요청이 재전송되어도 별도의 결제 작업이나 주문이 생성되면 안 된다.
방지 대상: 중복 결제결제사 타임아웃은 거절이 아니다. 동일 작업 ID로 최종 결과를 다시 조회해야 한다.
방지 대상: 성급한 재고 복구취소 요청 이후 승인이 확인되면 티켓 발급을 차단하고 환불 완료까지 추적해야 한다.
방지 대상: 미환불 주문티켓 판매는 짧은 시간에 재고, 사용자, IP, 결제 작업이라는 여러 공유 자원에 요청이 집중된다. 동시에 외부 결제사는 즉시 확정되지 않을 수 있다. 따라서 동시성 제어와 장애 복구를 분리해서 설명하기보다 하나의 상태 전이로 연결해야 설계 판단이 드러난다.
0 ≤ held + sold ≤ total
결제 승인, 거절, 취소, 환불, 티켓 회수 중 어느 경로에서도 이 관계를 위반하지 않는다.
02 · 설계 방향
Redis는 요청 집중을 완화하지만 재고 예약을 확정하지 않는다. PostgreSQL 트랜잭션이 판매 시간, 사용자/IP 중복, 정책 버전, 재고 수량을 다시 검증하고 상태 변경을 완료한다.
Redis 데이터가 유실되어도 이미 시작된 결제·환불·티켓 복구는 PostgreSQL 기록을 기준으로 계속 실행한다. 반대로 PostgreSQL 장애는 상태 변경을 완료할 수 없으므로 준비 상태 검사에 실패한다.
03 · 구현 방향
Spring Modulith 기반 모듈러 모놀리스로 단일 트랜잭션의 장점을 유지한다.
각 업무 모듈은 api 패키지만 공개하며 다른 모듈의 테이블과 내부 서비스를 직접 참조하지 않는다.
판매 생명주기, 판매 시간, 정책 버전을 관리한다.
SalePurchaseAuthority
대기열과 입장 권한, USER/IP 리스를 관리한다.
TransactionalAdmissionCommands
멱등성, 재고, 임시 예약, 주문 상태를 변경한다.
PurchaseCommands
결제 작업을 점유하고 결과 불명확 상태를 재조정한다.
PaymentWorker
티켓 발급·회수 작업과 중복 처리 방지 기록을 관리한다.
TicketEffectWorker
제한된 복구 실행과 운영 상태 조회를 제공한다.
OperationsService
04 · 처리 흐름
각 시나리오는 실제 상태 전이 함수와 통합 테스트가 검증하는 결과를 요약한다. 탭을 선택하면 재고, 구매 상태, 결제 작업, 티켓 처리 결과가 함께 변경된다.
입장 권한을 소비하고 재고를 임시 예약한 뒤 결제 승인과 티켓 발급 결과를 순서대로 반영한다.
05 · 실제 구현
아래 구조도는 모듈 README가 사용하는 실제 아키텍처 자료다. 오른쪽 목록은 핵심 동작을 구현한 현재 소스이며, 상태 전이와 장애 복구는 PostgreSQL·Redis 통합 테스트로 검증한다.
purchase/internal/PurchaseService.kt
정해진 잠금 순서로 멱등성, USER/IP 보호, 구매 한도, 재고를 검증하고 한 트랜잭션으로 구매 시도를 생성한다.
payment/internal/PaymentWorker.kt
짧은 점유 구간에서 토큰과 리비전을 기록한 뒤 외부 결제사를 호출하고 유효한 결과만 반영한다.
purchase/internal/RefundService.kt
환불 결과와 티켓 미발급 또는 회수 완료를 함께 확인한 경우에만 판매 수량을 복구한다.
redis/MultiKeyLeaseAdapter.kt
USER/IP 리스를 Lua 스크립트 한 번으로 획득·갱신·해제하여 부분 점유를 방지한다.
Ticket*IntegrationTest.kt
고경합, Redis 장애, 지연된 승인, 중복 처리, 프로세스 재시작 이후의 불변식을 검증한다.
06 · 예제 실행
수동 실행에는 JDK 25, PostgreSQL 18, Redis 8이 필요하다. 데모 프로필은 루프백에서만 실행하며, 공개된 화면은 생성 API가 아닌 기존 구매 시도의 복구 조회를 제공한다.
07 · 해결하는 기술적 문제
이 예제의 핵심은 모든 요청을 한 번만 실행한다고 가정하는 데 있지 않다. 요청과 외부 효과가 반복되더라도 동일한 최종 상태로 수렴하도록 식별자, 트랜잭션, 점유 토큰, 중복 처리 방지 기록을 결합한다.
등급별 재고 행을 잠그고 모든 수량 변경을 동일 트랜잭션에서 적용한다.
결과: held + sold ≤ total 유지요청 지문과 키를 저장하고 USER/IP별 활성 구매를 PostgreSQL 고유 제약으로 제한한다.
결과: 응답 유실 후에도 같은 구매 결과 반환타임아웃을 실패로 확정하지 않고 동일 작업 ID와 점유 리비전으로 최종 결과를 재조회한다.
결과: 오래된 작업자의 결과 반영 차단환불과 티켓 상태가 모두 안전한 경우에만 판매 수량을 복구한다.
결과: 사용 가능한 티켓과 복구된 재고의 동시 존재 방지신규 구매 진입만 차단하고 이미 시작된 결제·환불·조회는 PostgreSQL 기록으로 계속 처리한다.
결과: 보조 저장소 장애가 복구 경로를 차단하지 않음결제·환불·티켓 작업에 안정적인 작업 ID와 소비자별 처리 기록을 사용한다.
결과: 재시작과 재전달 후에도 효과 1회 반영