공연 좌석 예약 — 대기열 · 락 전략 · 이벤트 전달
좌석 예약 시스템
- 기간
- 2026.02 – 2026.05
- 역할
- 설계 · 구현 · 측정 전체
- 참여 인력
- 개인 프로젝트
Java 21Spring Boot 3.4.1PostgreSQL 16Redis 7 · Redisson 3.40.2Apache Kafka (cp 7.6.0)JPAFlywayTestcontainersk6 v1.5.0
GitHub에서 코드 보기 ↗서비스
관객이 콘서트를 고르고, 대기열을 거쳐 좌석을 선택하고, 결제까지 마치는 예매 서비스입니다.
예매는 같은 좌석에 사람이 몰리는 순간에만 어려워집니다. 그래서 좌석 하나를 두고 경합이 일어나는 구간에 검증을 집중했습니다.
결제는 mock 즉시 성공 구조이며 실제 결제는 일어나지 않습니다.
- 가입
- 콘서트 목록
- 대기열
- 좌석 선택
- 결제
- 예매 내역



- 데모 UI
- React · TypeScript · Vite · Playwright e2e
- 직접 띄워보기
docker compose -f docker-compose.yml -f docker-compose.demo.yml up→ localhost:4173
e2e가 화면에서 재현하는 것
- 두 브라우저가 한 좌석을 경쟁하면 한 쪽만 성공하고 진 쪽은 복구된다
- 대기열 토큰 응답이 유실돼 재요청해도 예매는 한 건만 생긴다
위 화면은 로컬 데모 실행 결과입니다. 운영 중인 서비스가 아닙니다.
요약
- 같은 좌석에 100명이 몰릴 때 나던 중복 판매를 락 3종 비교로 막아 oversell 0건, p95는 106–215ms로 실측
- 다른 좌석인데 예매가 서로 실패하던 원인이 잔여석 카운터 한 줄이었음을 밝혀 성공률 40% → 100%
- 매진된 좌석 요청까지 DB를 잡던 것을 Redis에서 미리 걸러 쓰기 p95 37ms → 6ms, 총 RPS 969 → 1,005
- 결제·만료 race와 멱등 replay, 대기열 토큰 우회를 시나리오로 재현해 체크 594/594 통과
01좌석 예약 · 동시성 제어
동시 예매 중복 판매를 막는 락 3종을 같은 조건에서 비교 — oversell 0건
문제 원인
- 좌석이 남았는지 보는 것과 예약을 남기는 것이 한 번에 처리되지 않으면, 두 요청이 같은 "잔여 있음"을 읽고 둘 다 예약을 기록
- 이 실패는 예외를 남기지 않고 두 요청 모두 정상 응답으로 종료되어 로그로 탐지 불가
- "충돌이 잦으면 비관적 락, 드물면 낙관적 락"이라는 통념만으로는 이 도메인에 어느 쪽이 맞는지 판단 근거 없음
해결 과정
- 비관적 락 · 낙관적 락 · Redis 분산 락을 같은 예약 도메인 위에 전략 패턴으로 구현하고 실행 시 플래그로 교체
- k6 시나리오를 전략만 바꿔 동일 조건으로 실행 (100 VU · 동일 좌석 1개 · VU당 1회)
- 매 실행 전 좌석·Redis 재고·대기열을 같은 상태로 되돌리고, 끝난 뒤 성공·실패 건수를 실제 좌석 상태와 대조
결과
- 세 전략 모두 성공 1건 / 실패 99건 / oversell 0건 — 동일 좌석 경합에서 중복 판매 차단 확인
- p95는 낙관 106ms · Redis 145ms · 비관 215ms로 벌어져 락 대기 비용이 응답 시간에 직결됨을 확인
- 결제·만료 race와 멱등 replay, 대기열 토큰 우회까지 같은 시나리오에 넣어 3전략 × 3회 반복 — 체크 594/594 통과
구현 기능
위 문제 해결 외에, 서비스가 돌아가기 위해 구현한 것들입니다.
- JWT 인증(가입·로그인)과 내 정보 조회, 데모 계정 원클릭 로그인
- 콘서트 목록·회차·좌석 배치 조회와 예매 내역 조회·취소, 대기열 순번은 SSE로 전달
- Transactional Outbox로 커밋과 발행을 분리하고, 재고 정합성 대조와 Kafka DLT 수동 replay를 운영용 엔드포인트로 제공