실시간 좌석 예매 시뮬레이터
Spring Boot, React, Redis, Kafka, Nginx를 활용하여 대용량 트래픽이 몰리는 타임딜 예매 상황을 모의하고 동시성 제어와 실시간 모니터링을 해결한 포트폴리오 프로젝트입니다.
1. 프로젝트 개요 및 선정이유
인기 콘서트 티켓팅, 한정판 타임딜 세일 등 대규모 동시 접속자가 급격히 몰리는 서비스는 언제나 백엔드 개발자에게 흥미롭고 까다로운 과제입니다. 수많은 사용자가 동시에 좌석을 선점하려 할 때 발생하는 데이터베이스 락(Lock) 경합, 서버 자원 고갈, 긴 응답 지연 등 고전적인 병목 현상을 해결하는 고성능 예매 아키텍처를 실험하고자 본 프로젝트를 설계 및 개발했습니다.
단순한 가상 이론이 아니라, 채용 담당자나 사용자가 직접 웹 브라우저에서 티켓팅에 참여(인간 유저)하고, 동시에 수많은 가상 AI 유저가 트래픽을 폭발적으로 발생시켰을 때 전체 시스템이 어떻게 부하를 분산하고 처리하는지 실시간으로 관찰할 수 있도록 구현했습니다.

- 서비스 링크: https://ticket.chocojipsa.blog
2. 전체 시스템 아키텍처
본 프로젝트는 프론트엔드와 백엔드가 완전히 격리된 멀티 인스턴스 환경에서 구동됩니다. 비용 효율적인 아키텍처 구성을 위해 AWS Lightsail의 가상 인스턴스 여러 대와 Vercel을 활용해 분산 구조를 확립했습니다.
[ Browser ] (ticket.chocojipsa.blog)
│
▼
[ Nginx Reverse Proxy & Load Balancer ] (ticket-api.chocojipsa.blog)
│
├─► /api/** ──► [ Spring Boot API Server A ] (AWS Lightsail) ─┐
├─► /api/** ──► [ Spring Boot API Server B ] (AWS Lightsail) ─┤
│ ▼
└─► /health [ Infrastructure & Data ]
├─► PostgreSQL (RDS)
├─► Redis (Waiting Queue / Cache)
└─► Apache Kafka (Event Sourcing)
💻 기술 스택 요약
- Frontend: React 18, Vite, TypeScript, Tailwind CSS, Lucide React (Vercel 호스팅)
- Backend: Spring Boot 3.3.5, Java 17, Spring Data JPA, Flyway (DB 마이그레이션)
- Cache & Queue: Redis (Sorted Set & Strings)
- Event Broker: Apache Kafka (메시징 큐 및 비동기 처리)
- Database: PostgreSQL 16 (RDS)
- Infrastructure: AWS Lightsail, Docker / Docker Compose, Nginx (Reverse Proxy)
3. 분리형 2-윈도우 아키텍처 (2-Window Architecture)
대규모 트래픽 지표와 실시간 좌석 현황판을 렌더링하는 작업은 브라우저에 큰 부하를 줍니다. 실시간 데이터 동기화(SSE)의 성능을 최적화하고 브라우저 쓰로틀링(Throttling)을 방지하기 위해 사용자가 접근하는 화면을 두 개의 독립적인 창(Window)으로 분리 설계했습니다.
graph TD
A["메인 대시보드 (Main Browser Tab)"] -->|새 창/팝업 오픈| B["예매 클라이언트 (Ticketing Popup)"]
A -->|SSE Connection 1| C["전체 이벤트 스냅샷 스트림 (Heavy Snapshot)"]
B -->|SSE Connection 2| D["개인화 대기열 스트림 (Lightweight)"]
style A fill:#EEF2F6,stroke:#4F46E5,stroke-width:2px
style B fill:#EEF2F6,stroke:#00C2C2,stroke-width:2px
- 메인 대시보드 (
/)- 역할: 백엔드 제어(가상 유저 발생기 제어, 시뮬레이션 시작/중단), 전체 시스템 부하 모니터링(TPS 실시간 차트, Kafka Lag, 활성 접속자 상태 관찰).
- 데이터 흐름:
/api/simulations/{simulationId}/events엔드포인트를 통해 대용량 JSON 스냅샷 스트림을 실시간 수신(SSE).
- 예매 클라이언트 창 (
/ticketing/{eventId})- 역할: 채용 담당자(인간)의 1인칭 예매 프로세스 모의 체험 (
대기열 대기 ➔ 실시간 좌석 선택 ➔ 결제 수단 ➔ 영수증). - 데이터 흐름: 해당 참가자의 대기 순번 정보 및 입장 허가만을 수신하는 라이트 스트림(SSE)으로 연결을 분리해 네트워크와 브라우저 렌더링 오버헤드를 극대화로 절감했습니다.
- 역할: 채용 담당자(인간)의 1인칭 예매 프로세스 모의 체험 (
4. 핵심 기술 구현 및 문제 해결
1) Redis Sorted Set을 활용한 FIFO 대기열 구현
서버의 자원을 보호하기 위해 대규모 유입 트래픽을 순차적으로 진입시키는 **진입 대기열(Waiting Queue)**을 구축했습니다. RDB의 락 대신 메모리 연산이 매우 빠른 Redis의 Sorted Set 데이터 구조를 사용해 병목 현상을 방지했습니다.
@Service
public class WaitingQueueService {
private final StringRedisTemplate redis;
private final Clock clock;
public void enterQueue(String simulationId, String virtualUserId) {
// 동시 요청에 대해 유니크한 순번 보장을 위해 Redis Increment 사용
Long sequence = redis.opsForValue().increment(sequenceKey(simulationId));
double score = sequence != null ? sequence.doubleValue() : clock.millis();
redis.opsForZSet().add(queueKey(simulationId), virtualUserId, score);
}
public long position(String simulationId, String virtualUserId) {
Long rank = redis.opsForZSet().rank(queueKey(simulationId), virtualUserId);
return rank != null ? rank + 1 : 0L; // 사용자의 실시간 대기 순번 조회
}
}
- FIFO 정합성: 동시 유입 상황에서 단순히
System.currentTimeMillis()를 스코어로 사용하면 동일 밀리초 내의 순서가 보장되지 않아 데이터가 꼬일 수 있습니다. 이를 극복하고자 Redis의 원자적(Atomic) 증가 연산인INCR을 결합한 시퀀스를 스코어로 적용해 확실한 선입선출(FIFO)을 구현했습니다. - 임시 진입 토큰: 대기열 1번이 되어 입장 허가를 받으면 Redis에 60초 만료 기간(TTL)을 가진 입장 토큰(
simulation:{id}:admission:{userId})을 발급하여 예매를 마칠 때까지 유효성을 체크하고, 시간이 지나면 토큰을 자동 만료(자동 청소)해 다른 대기자에게 기회를 넘깁니다.
2) Server-Sent Events (SSE) 기반 실시간 스트리밍
HTTP Polling 방식은 수많은 클라이언트가 주기적으로 요청을 보내기 때문에 서버의 부하가 매우 큽니다. 서버가 클라이언트로 데이터를 즉각 푸시할 수 있도록 SSE 방식을 채택했습니다.
- SseEmitter 공유:
SimulationEventHub를 구현해 각 시뮬레이션의 이벤트 채널로 들어오는 실시간 데이터(가상 유저 상태, 예매율, 시스템 메트릭 등)를 다수의 SSE 커넥션 브로드캐스팅 구조로 멀티플렉싱했습니다. - Nginx 스트리밍 최적화 문제: 배포 초기, SSE 연결이 주기적으로 끊기거나 실시간 렌더링이 멈춘 채 한 번에 몰아서 전달되는 현상이 발생했습니다. 이는 Nginx의 프록시 버퍼링(Proxy Buffering) 때문이었습니다.
- 해결책: Nginx 설정에서
proxy_buffering off;와proxy_read_timeout 3600;설정을 도입하고, SSE 응답 헤더에X-Accel-Buffering: no를 실어 보내 버퍼링을 강제로 해제해 지연 없는 실시간 스트리밍을 실현했습니다.
- 해결책: Nginx 설정에서
3) Apache Kafka를 활용한 비동기 결제/예매 완료 처리
실제 좌석을 선택한 후 결제를 수행하는 비즈니스 로직은 높은 트랜잭션 안전성이 요구되며 DB 쓰기 작업이 무겁게 일어납니다. 동시 다발적인 쓰기 부하를 완충하고자 Apache Kafka를 전면 배치했습니다.
좌석 선택 & 결제 요청 ──► API 서버 (가용성 검증) ──► Kafka 토픽 (payment-events) 발행
│
▼
Kafka Consumer Worker
(DB 트랜잭션 수행,
좌석 확정 및 영수증 발행)
- 관심사 분리 (Decoupling): API 서버는 사용자가 결제를 요청하면 Redis의 가용성 검증을 거쳐 가볍게 승인한 뒤 Kafka에
PaymentAttemptEvent를 던지고 즉시 응답합니다. 이후 백그라운드의 전용 Worker 컨테이너들이 이벤트를 수신하여 데이터베이스(RDS PostgreSQL)에 실제 좌석 예약 상태를 반영합니다. - 이 구조를 통해 사용자는 결제 대기화면에서 멈추지 않고 빠른 응답을 받으며, 백엔드는 데이터베이스 커넥션 풀 고갈 걱정 없이 유동적으로 트래픽 인입 강도를 완충할 수 있게 되었습니다.
5. 분산 배포 및 DevOps 전략
AWS Lightsail에 인스턴스 3대를 두어 성능과 가격의 트레이드오프를 맞춘 분산 프로덕션 환경을 설계했습니다.
- Lightsail A: Nginx Reverse Proxy 및 로드 밸런서,
api-a애플리케이션 - Lightsail B:
api-b애플리케이션,kafka-worker,traffic-generator(부하 발생기) - Lightsail C: 공용 Redis, Apache Kafka 브로커
- RDS PostgreSQL: 완전 관리형 관계형 데이터베이스로 영속성 격리
6. 프로젝트를 통해 배운 점 및 성과
- 분산 인프라 설계 능력 배양: Nginx 로드밸런싱 설정과 Cross-Origin 자원 공유(CORS) 처리, 크로스 도메인 환경에서의 Stateless 토큰 검증 흐름을 몸소 체득했습니다.
- 동시성 병목 지점 극복: 데이터베이스의 Row-level Lock이나 Application 동기화 기법 대신 Redis 대기열을 도입해 유입량 자체를 관리하는 것이 웹 애플리케이션 가용성을 확보하는 가장 안정적인 수단임을 배웠습니다.
- 실시간 데이터 핸들링: 웹 브라우저가 수백 수천 개의 데이터 변경 이벤트를 끊김 없이 처리하도록 가벼운 SSE 스트림 설계와 React 가상 렌더링 기법을 매치해 시각적인 만족도와 웹 최적화를 달성했습니다.