1️⃣ 문제 인식 — “선착순”만으로는 부족했다
처음에는 단순한 선착순 강의 신청 시스템을 구현했다.
하지만 구현을 진행하면서 한 가지 한계를 느꼈다.
“선착순이라는 개념은 있지만, 실제 서비스 도메인과 연결되어 있지 않다”
예를 들어:
- 재고가 왜 중요한지
- 결제 실패 시 어떤 문제가 발생하는지
- 동시 요청이 몰릴 때 어떤 문제가 생기는지
이런 부분이 드러나지 않았다.
그래서 단순 선착순을 넘어서
👉 공연 티켓팅 도메인으로 확장하게 되었다.
2️⃣ 바로 구현하지 않고, 먼저 실험을 진행한 이유
처음부터 Redis, Outbox 같은 기술을 사용하는 대신
다음 기준을 세웠다.
❗ “기술은 선택하는 것이 아니라, 검증하는 것이다”
그래서 핵심 설계 포인트마다
실험을 먼저 진행한 뒤 실제 구현에 반영했다.
3️⃣ 동시성 핵심 — 재고 선점 방식 실험 (E1)
공연 티켓팅에서 가장 중요한 문제는 이것이다.
❗ 좌석보다 더 많이 예매되면 안 된다 (초과 발급 금지)
이를 해결하기 위해 3가지 방식을 비교했다.
- Redis DECR
- MySQL Pessimistic Lock
- MySQL Optimistic Lock
실험 조건
- 동시 요청: 100명
- 재고: 50개
결과
- Redis DECR → 초과 발급 0건 + 가장 높은 처리량
- Pessimistic Lock → 안전하지만 성능 저하
- Optimistic Lock → 충돌 증가로 재시도 폭발
결론
👉 Redis 기반 재고 차감 방식 채택
4️⃣ 하지만 단순 Redis DECR로는 부족했다
초기에는 아래처럼 단순하게 생각했다.
요청 → Redis DECR → 성공하면 예매
하지만 실제 테스트를 진행하면서 문제가 발생했다.
❗ “누가 먼저 요청했는지 보장되지 않는다”
즉,
- 단순 DECR은 “재고”만 관리할 뿐
- 요청 순서(선착순)를 보장하지 않는다
5️⃣ 해결 — Redis 대기열 + 입장권(admitted) 구조 도입
이 문제를 해결하기 위해 구조를 다음과 같이 변경했다.
전체 흐름
Queue 진입 → 대기열 순서 관리 → 입장 허용(admitted) → 예매 진행
핵심 아이디어
- Redis Sorted Set으로 대기열 구성
- score = timestamp (요청 순서 보장)
- 상위 N명만 입장 허용
- 입장 허용된 사용자에게 “admitted token” 발급
실제 구현 흐름
1. 대기열 진입
ZADD queue:concert:{id} timestamp userId
2. 입장 허용 (Lua Script)
- 상위 N명 추출
- admitted 키 발급 (TTL 포함)
SETEX admitted:concert:{id}:user:{uid} 600 1
3. 예매 요청 시 검증
admitted 키 존재 여부 확인
→ 없으면 예매 불가
👉 이 구조를 통해
- 선착순 보장
- 재고 초과 방지
- 서버 부하 제어
를 동시에 해결할 수 있었다.
6️⃣ 결제 단계 — 멱등성 처리 (E3)
결제 API에서 중요한 것은 다음이다.
❗ 같은 요청이 여러 번 들어와도 한 번만 처리되어야 한다
실제 테스트 중
- 네트워크 재시도
- 사용자 중복 클릭
으로 인해 동일 요청이 여러 번 들어오는 상황이 발생했다.
해결 방식
Redis의 SETNX를 활용해 멱등성을 보장했다.
SETNX payment:{idempotencyKey}
- 최초 요청만 성공
- 이후 요청은 차단
실제 구현은 더 강화했다
단순 Redis만 사용하는 것이 아니라
- Redis SETNX
- DB unique constraint
- 기존 결제 여부 조회
👉 3중 방어 구조로 설계
7️⃣ 결제 실패 문제 — 보상 트랜잭션 (E4)
결제는 외부 시스템이기 때문에
항상 실패 가능성이 존재한다.
문제는 이 시점이다.
❗ 이미 좌석은 차감됐는데 결제가 실패한 경우
이 경우
- 좌석이 영구적으로 사라지는 문제가 발생한다
8️⃣ 해결 — Outbox 패턴 도입
보상 로직을 안정적으로 처리하기 위해
Outbox 패턴을 도입했다.
흐름
결제 실패 → Outbox 테이블에 이벤트 저장
→ Scheduler가 polling
→ 보상 처리 수행 (예매 취소 + 재고 복구)
실제 처리
- Booking.cancel()
- Concert.bookedCount 감소
- 상태 업데이트
왜 Outbox를 선택했는가
- 트랜잭션 내에서 이벤트 저장 → 유실 방지
- 재시도 가능 → 결국 처리됨 (Eventually Consistent)
- 안정성 확보
9️⃣ 최종 아키텍처
실험과 구현을 통해 다음 구조로 정리되었다.
- 재고 및 선착순 → Redis Queue + admitted token
- 멱등성 → Redis SETNX + DB 보완
- 보상 처리 → Outbox 패턴 + Scheduler
🔟 정리
이 프로젝트에서 가장 중요하게 생각한 것은
“어떤 기술을 썼는가”가 아니었다.
❗ “왜 이 기술을 선택했는가”
- Redis는 빠르기 때문에 사용한 것이 아니라
→ 동시성 문제를 해결하기 위해 선택 - SETNX는 편해서가 아니라
→ 중복 결제를 막기 위해 선택 - Outbox는 트렌드라서가 아니라
→ 보상 유실을 막기 위해 선택
'프로젝트 > 공연 티켓팅 프로젝트' 카테고리의 다른 글
| 🎟️ Docker-compose up 한 번으로 실행되는 백엔드 만들기 (1) | 2026.05.18 |
|---|---|
| 🎟️ MySQL 인덱스 실험: 예약 만료 배치 쿼리 성능 개선 (10만 건 실험) (1) | 2026.05.13 |
| 🎟️ 결제 API는 정말 중복 결제를 막을 수 있을까? (1) | 2026.04.27 |
| 🎟️ 예약 API 동시성 테스트 (다중 사용자 경쟁) 결과 보고 (1) | 2026.04.27 |
| 🎟️ Redis 입장권 기반 예약 시스템은 정말 중복 예약을 막을까? (0) | 2026.04.27 |