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

본 프로젝트는 프론트엔드와 백엔드가 완전히 격리된 멀티 인스턴스 환경에서 구동됩니다. 비용 효율적인 아키텍처 구성을 위해 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)
대규모 트래픽 지표와 실시간 좌석 현황판을 렌더링하는 작업은 브라우저에 큰 부하를 줍니다. 실시간 데이터 동기화(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
/)
/api/simulations/{simulationId}/events 엔드포인트를 통해 대용량 JSON 스냅샷 스트림을 실시간 수신(SSE)./ticketing/{eventId})
대기열 대기 ➔ 실시간 좌석 선택 ➔ 결제 수단 ➔ 영수증).서버의 자원을 보호하기 위해 대규모 유입 트래픽을 순차적으로 진입시키는 **진입 대기열(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; // 사용자의 실시간 대기 순번 조회
}
}
System.currentTimeMillis()를 스코어로 사용하면 동일 밀리초 내의 순서가 보장되지 않아 데이터가 꼬일 수 있습니다. 이를 극복하고자 Redis의 원자적(Atomic) 증가 연산인 INCR을 결합한 시퀀스를 스코어로 적용해 확실한 선입선출(FIFO)을 구현했습니다.simulation:{id}:admission:{userId})을 발급하여 예매를 마칠 때까지 유효성을 체크하고, 시간이 지나면 토큰을 자동 만료(자동 청소)해 다른 대기자에게 기회를 넘깁니다.HTTP Polling 방식은 수많은 클라이언트가 주기적으로 요청을 보내기 때문에 서버의 부하가 매우 큽니다. 서버가 클라이언트로 데이터를 즉각 푸시할 수 있도록 SSE 방식을 채택했습니다.
SimulationEventHub를 구현해 각 시뮬레이션의 이벤트 채널로 들어오는 실시간 데이터(가상 유저 상태, 예매율, 시스템 메트릭 등)를 다수의 SSE 커넥션 브로드캐스팅 구조로 멀티플렉싱했습니다.proxy_buffering off;와 proxy_read_timeout 3600; 설정을 도입하고, SSE 응답 헤더에 X-Accel-Buffering: no를 실어 보내 버퍼링을 강제로 해제해 지연 없는 실시간 스트리밍을 실현했습니다.실제 좌석을 선택한 후 결제를 수행하는 비즈니스 로직은 높은 트랜잭션 안전성이 요구되며 DB 쓰기 작업이 무겁게 일어납니다. 동시 다발적인 쓰기 부하를 완충하고자 Apache Kafka를 전면 배치했습니다.
좌석 선택 & 결제 요청 ──► API 서버 (가용성 검증) ──► Kafka 토픽 (payment-events) 발행
│
▼
Kafka Consumer Worker
(DB 트랜잭션 수행,
좌석 확정 및 영수증 발행)
PaymentAttemptEvent를 던지고 즉시 응답합니다. 이후 백그라운드의 전용 Worker 컨테이너들이 이벤트를 수신하여 데이터베이스(RDS PostgreSQL)에 실제 좌석 예약 상태를 반영합니다.AWS Lightsail에 인스턴스 3대를 두어 성능과 가격의 트레이드오프를 맞춘 분산 프로덕션 환경을 설계했습니다.
api-a 애플리케이션api-b 애플리케이션, kafka-worker, traffic-generator (부하 발생기)