포트폴리오

실시간 좌석 예매 시뮬레이터

2026년 06월 16일
13

Spring Boot, React, Redis, Kafka, Nginx를 활용하여 대용량 트래픽이 몰리는 타임딜 예매 상황을 모의하고 동시성 제어와 실시간 모니터링을 해결한 포트폴리오 프로젝트입니다.


1. 프로젝트 개요 및 선정이유

인기 콘서트 티켓팅, 한정판 타임딜 세일 등 대규모 동시 접속자가 급격히 몰리는 서비스는 언제나 백엔드 개발자에게 흥미롭고 까다로운 과제입니다. 수많은 사용자가 동시에 좌석을 선점하려 할 때 발생하는 데이터베이스 락(Lock) 경합, 서버 자원 고갈, 긴 응답 지연 등 고전적인 병목 현상을 해결하는 고성능 예매 아키텍처를 실험하고자 본 프로젝트를 설계 및 개발했습니다.

단순한 가상 이론이 아니라, 채용 담당자나 사용자가 직접 웹 브라우저에서 티켓팅에 참여(인간 유저)하고, 동시에 수많은 가상 AI 유저가 트래픽을 폭발적으로 발생시켰을 때 전체 시스템이 어떻게 부하를 분산하고 처리하는지 실시간으로 관찰할 수 있도록 구현했습니다.

ticket.chocojipsa.blog_ (2)


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
  1. 메인 대시보드 (/)
    • 역할: 백엔드 제어(가상 유저 발생기 제어, 시뮬레이션 시작/중단), 전체 시스템 부하 모니터링(TPS 실시간 차트, Kafka Lag, 활성 접속자 상태 관찰).
    • 데이터 흐름: /api/simulations/{simulationId}/events 엔드포인트를 통해 대용량 JSON 스냅샷 스트림을 실시간 수신(SSE).
  2. 예매 클라이언트 창 (/ticketing/{eventId})
    • 역할: 채용 담당자(인간)의 1인칭 예매 프로세스 모의 체험 (대기열 대기 ➔ 실시간 좌석 선택 ➔ 결제 수단 ➔ 영수증).
    • 데이터 흐름: 해당 참가자의 대기 순번 정보 및 입장 허가만을 수신하는 라이트 스트림(SSE)으로 연결을 분리해 네트워크와 브라우저 렌더링 오버헤드를 극대화로 절감했습니다.

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를 실어 보내 버퍼링을 강제로 해제해 지연 없는 실시간 스트리밍을 실현했습니다.

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 가상 렌더링 기법을 매치해 시각적인 만족도와 웹 최적화를 달성했습니다.

댓글을 불러오는 중...