bluetape4k-workshop · Visual Companion

Spring Boot 4 · Kotlin · Java 25

콘서트 티켓 Flash Sale

판매 개시 직후 한정된 재고에 요청이 집중되는 상황에서 대기열, 재고 임시 예약, 결제 결과 확인, 취소·환불, 티켓 발급을 하나의 일관된 상태 모델로 처리하는 예제다.

애플리케이션 구조Spring Modulith
최종 상태 저장PostgreSQL
진입 제어 가속Redis
판매 현황 SALE OPEN
STANDING A · 판매 가능 수량2026 SUMMER LIVE
128
#1042
#1043
입장
#1045
#1046
01대기실 입장 권한
02재고 임시 예약
03결제 결과 확인
04티켓 발급
Redis: USER/IP 리스 PostgreSQL: 재고·결제·티켓 상태

01 · 예제 구상

실제 운영에서 부딪히는 문제를 한 구매 흐름에 결합한다

단순 재고 차감 예제는 정상 처리만 보여 준다. 이 예제는 동일 사용자와 IP의 반복 요청, 결제 응답 유실, 취소와 지연된 승인, 보조 저장소 장애가 함께 발생하는 시나리오를 사용한다.

문제 01

판매 개시 순간의 고경합

여러 요청이 같은 등급의 잔여 수량을 동시에 조회하고 임시 예약을 시도한다.

방지 대상: 초과 판매
문제 02

반복 클릭과 응답 유실

동일한 구매 요청이 재전송되어도 별도의 결제 작업이나 주문이 생성되면 안 된다.

방지 대상: 중복 결제
문제 03

결제 결과 불명확

결제사 타임아웃은 거절이 아니다. 동일 작업 ID로 최종 결과를 다시 조회해야 한다.

방지 대상: 성급한 재고 복구
문제 04

취소와 지연된 승인

취소 요청 이후 승인이 확인되면 티켓 발급을 차단하고 환불 완료까지 추적해야 한다.

방지 대상: 미환불 주문

왜 이 시나리오인가

티켓 판매는 짧은 시간에 재고, 사용자, IP, 결제 작업이라는 여러 공유 자원에 요청이 집중된다. 동시에 외부 결제사는 즉시 확정되지 않을 수 있다. 따라서 동시성 제어와 장애 복구를 분리해서 설명하기보다 하나의 상태 전이로 연결해야 설계 판단이 드러난다.

전체 흐름에서 유지할 재고 불변식

0 ≤ held + sold ≤ total

결제 승인, 거절, 취소, 환불, 티켓 회수 중 어느 경로에서도 이 관계를 위반하지 않는다.

02 · 설계 방향

빠른 진입 제어와 최종 상태 변경 책임을 분리한다

Redis는 요청 집중을 완화하지만 재고 예약을 확정하지 않는다. PostgreSQL 트랜잭션이 판매 시간, 사용자/IP 중복, 정책 버전, 재고 수량을 다시 검증하고 상태 변경을 완료한다.

Redis

임시 조정
  • 대기실 입장 권한과 짧은 TTL을 관리한다.
  • USER/IP 키를 Lua 스크립트 한 번으로 점유한다.
  • 요청 속도를 제한하고 동시 구매 시도를 조기에 거부한다.
  • 장애가 발생하면 신규 구매 진입을 차단한다.

PostgreSQL

최종 상태 기록
  • 판매 시간과 수정불가한 정책 버전을 검증한다.
  • 재고 행을 잠그고 임시 예약·판매 수량을 변경한다.
  • USER/IP별 활성 구매를 고유 제약과 잠금으로 제한한다.
  • 결제 작업, 주문, 환불, 티켓 처리 결과를 보존한다.

Redis 데이터가 유실되어도 이미 시작된 결제·환불·티켓 복구는 PostgreSQL 기록을 기준으로 계속 실행한다. 반대로 PostgreSQL 장애는 상태 변경을 완료할 수 없으므로 준비 상태 검사에 실패한다.

03 · 구현 방향

한 번 배포하되 업무 모듈의 책임은 분리한다

Spring Modulith 기반 모듈러 모놀리스로 단일 트랜잭션의 장점을 유지한다. 각 업무 모듈은 api 패키지만 공개하며 다른 모듈의 테이블과 내부 서비스를 직접 참조하지 않는다.

salecontrol

판매 생명주기, 판매 시간, 정책 버전을 관리한다.

SalePurchaseAuthority

admission

대기열과 입장 권한, USER/IP 리스를 관리한다.

TransactionalAdmissionCommands

purchase

멱등성, 재고, 임시 예약, 주문 상태를 변경한다.

PurchaseCommands

payment

결제 작업을 점유하고 결과 불명확 상태를 재조정한다.

PaymentWorker

ticketing

티켓 발급·회수 작업과 중복 처리 방지 기록을 관리한다.

TicketEffectWorker

operations

제한된 복구 실행과 운영 상태 조회를 제공한다.

OperationsService
허용된 모듈 참조 방향: admission → salecontrol · purchase → salecontrol/admission · payment/ticketing → purchase · operations → 각 공개 API

04 · 처리 흐름

장애 상황을 선택해 상태 전이를 비교한다

각 시나리오는 실제 상태 전이 함수와 통합 테스트가 검증하는 결과를 요약한다. 탭을 선택하면 재고, 구매 상태, 결제 작업, 티켓 처리 결과가 함께 변경된다.

정상 구매와 티켓 발급

입장 권한을 소비하고 재고를 임시 예약한 뒤 결제 승인과 티켓 발급 결과를 순서대로 반영한다.

수렴 완료
재고 변경 시점
외부 호출 처리
복구 기준

05 · 실제 구현

설계 항목을 클래스와 테스트까지 연결한다

아래 구조도는 모듈 README가 사용하는 실제 아키텍처 자료다. 오른쪽 목록은 핵심 동작을 구현한 현재 소스이며, 상태 전이와 장애 복구는 PostgreSQL·Redis 통합 테스트로 검증한다.

단일 Spring Boot 애플리케이션 안에서 업무 모듈을 분리하고 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가 아닌 기존 구매 시도의 복구 조회를 제공한다.

PostgreSQL과 Redis 시작

Docker 호환 컨테이너 런타임

            
확인:

07 · 해결하는 기술적 문제

빠른 정상 처리보다 장애 이후의 수렴 조건을 명확히 한다

이 예제의 핵심은 모든 요청을 한 번만 실행한다고 가정하는 데 있지 않다. 요청과 외부 효과가 반복되더라도 동일한 최종 상태로 수렴하도록 식별자, 트랜잭션, 점유 토큰, 중복 처리 방지 기록을 결합한다.

01

고경합 재고 직렬화

등급별 재고 행을 잠그고 모든 수량 변경을 동일 트랜잭션에서 적용한다.

결과: held + sold ≤ total 유지
02

요청 멱등성과 사용자 중복 구매

요청 지문과 키를 저장하고 USER/IP별 활성 구매를 PostgreSQL 고유 제약으로 제한한다.

결과: 응답 유실 후에도 같은 구매 결과 반환
03

결제 타임아웃 복구

타임아웃을 실패로 확정하지 않고 동일 작업 ID와 점유 리비전으로 최종 결과를 재조회한다.

결과: 오래된 작업자의 결과 반영 차단
04

취소·환불·티켓 회수 경쟁

환불과 티켓 상태가 모두 안전한 경우에만 판매 수량을 복구한다.

결과: 사용 가능한 티켓과 복구된 재고의 동시 존재 방지
05

Redis 장애 범위 제한

신규 구매 진입만 차단하고 이미 시작된 결제·환불·조회는 PostgreSQL 기록으로 계속 처리한다.

결과: 보조 저장소 장애가 복구 경로를 차단하지 않음
06

외부 효과 중복 처리 방지

결제·환불·티켓 작업에 안정적인 작업 ID와 소비자별 처리 기록을 사용한다.

결과: 재시작과 재전달 후에도 효과 1회 반영

재고, 결제, 환불, 티켓 처리 결과가 서로 모순되지 않는 상태에 도달해야 구매 처리가 완료된다.

이 예제는 그 수렴 조건과 복구 절차를 실행 가능한 Spring Boot 코드와 통합 테스트로 고정한다.