MSA2026년 5월 27일7분 읽기

MSA로 선착순 티켓팅 시스템 만들기: 전체 개요와 진화 로드맵

콘서트 티켓팅을 MSA로 학습하기 위한 사이드 프로젝트 노트. 도착지 다이어그램, 핵심 패턴 7개, 1→5단계 진화 순서, 그리고 '동작하는 최소 → 측정 → 한계 → 진화' 메타 원칙까지 정리했다.

#MSA#Architecture#CQRS#Kafka#Redis#Bulkhead#Ticketing

MSA로 선착순 티켓팅 시스템 만들기: 전체 개요

다이어그램을 그대로 베끼지 않는다. "동작하는 최소 → 측정 → 한계 → 진화" 사이클로, 한 부품씩 만들면서 "왜 이게 거기 있는가"를 직접 체감하는 게 목적이다.


들어가며

이 글은 내가 진행 중인 사이드 프로젝트 — 선착순 예매(티켓팅) 시스템을 MSA로 만들기 — 의 전체 개요다. 시리즈의 첫 글이며, 도착지 다이어그램·핵심 패턴·진화 순서·메타 학습 원칙을 한 번에 잡아둔다.

후속 글들에서는 각 결정의 근거(왜 MSA? 왜 CQRS? 왜 같은 DB로 시작? 왜 Bulkhead?)를 따로 깊게 다룬다. 이 글은 "지도"이고, 나머지가 "골목길"이다.

이 프로젝트는 주니어와 토론하기 위해 만든 학습용 노트에서 출발했다. 그래서 모든 결정에 "왜 이 옵션을 안 골랐는가"의 트레이드오프가 따라온다.


1. 우리가 만드는 것

선착순 예매(티켓팅) 시스템. 콘서트 티켓 예매 같은 도메인이다.

대규모 트래픽이 동시에 몰리고(수만~수십만 동접), 좌석은 한정되어 있어 정확히 한 사람만 한 좌석을 가져가야 한다. 동시성 + throughput + 정확성 셋이 다 어려운 도메인이다.

학습 목표는 두 가지다.

  1. 인프라부터 직접 구현 — Redis, Kafka 같은 핵심 부품을 외부 매니지드 서비스 대신 Kotlin/Netty로 직접 짜본 뒤 그 위에 도메인을 얹는다. 동작 원리를 체득하는 게 목표다
  2. 점진적 진화 — 처음부터 다이어그램 통째로 구현하지 않는다. "동작하는 최소 → 측정 → 한계 발견 → 진화" 사이클을 돈다

2. 도착지 다이어그램 (참고만)

text
                  ┌────────────────────┐
                  │  Client (Browser)  │
                  │  polling 5s → 1s   │
                  └─────────┬──────────┘
                            │
                  ┌─────────▼──────────┐
                  │  API Gateway       │
                  │  URL routing       │
                  └─────────┬──────────┘
              ┌─────────────┼──────────────────┐
              │             │                  │
      Queue Server Group           Booking Server Group
      (자원 격리)                  (자원 격리)
              │                            │
      ┌───────▼───────┐           ┌────────▼───────┐
      │ Queue API × N │           │ Booking API × N│
      │ UUID·JWT·순번  │           │ 토큰검증·좌석·결제│
      └───────┬───────┘           └────────┬───────┘
              │                            │
      ┌───────▼──────────┐         ┌───────▼──────────┐
      │ 대기열 Redis      │         │ 잔여좌석 Redis    │
      │ ZSET [UUID:ts]   │         │ Seat Counter Lua │
      │ Active User Set  │         │ Seat Lock SETNX  │
      └──────────────────┘         └────────┬─────────┘
                                            │
                              ┌─────────────▼───────┐
                              │   Message Queue     │ ← Kafka
                              │   (events)          │
                              └─────────┬───────────┘
                                        │ consume
                                ┌───────▼───────┐
                                │   Worker      │
                                │   (MQ → DB)   │
                                └───────┬───────┘
                                        │ INSERT
                                ┌───────▼───────┐
                                │   RDB         │  ← Postgres
                                │  Primary/Replica
                                └───────────────┘

이건 최종 도착지다. 우리는 한 부품씩 만들어가며 "왜 이게 거기 있는가"를 직접 체감하는 게 목적이다.

다이어그램을 그대로 베끼지 않는 이유는 단순하다. 그렇게 하면 부품이 왜 필요한지 모르고 만들게 된다. Worker를 먼저 만들면 "API 폭주가 DB 폭주로 직결되는" 고통을 겪지 못한 채 비동기를 도입하게 되고, 그 결정의 무게를 알 수 없다. 한계가 드러나야 부품의 존재 이유가 보인다.


3. 핵심 패턴 한 줄씩

다이어그램에 박혀 있는 패턴 7개를 한 줄로 요약하면 이렇다.

패턴어디 적용본질
CQRScommand service / query service 분리읽기/쓰기 스케일링 곡선이 다르니 모델·서비스도 따로
BulkheadQueue tier ↔ Booking tier 자원 격리한 쪽이 무너져도 다른 쪽이 살아있게
Polling 가변 주기Client 5s → 1s순번 가까울수록 빨라짐. 서버 부담 vs UX 균형
Sorted Set 순번 관리대기열 Redis (ZADD/ZRANK/ZPOPMIN)"순서"를 데이터 구조 자체에 위임
분산락 (SETNX) + Lua atomic잔여좌석 RedisDB락 대신 인메모리, 처리량 ↑
MQ 비동기 영속화Booking API → Kafka → Worker → DBAPI 응답 빠르게, DB 부담 완충
DB Primary/Replicacommand writes Primary, query reads Replica조회 부하 분산. replica lag 트레이드오프

각 패턴이 왜 거기 있는지는 후속 글에서 결정별로 다룬다.


4. 지금까지 진행 상황

완성된 것

  • MyRedis — Netty + RESP 직접 구현. 캐시/세션 동작
  • MyKafka MVP — 단일 브로커, PRODUCE / FETCH / CREATE_TOPIC / COMMIT_OFFSET / FETCH_OFFSET 5개 ApiKey, sparse offset index, segment rolling, batch all-or-none. 클라이언트 SDK는 아직 없음
  • auth-service — Spring Boot 4 + JWT + Postgres + MyRedis token store
  • frontend — Next.js, queue/seats/payment/confirmation 페이지
  • ticket-command-service — port 8082, 예약 생성/확정/취소. 비관락 + @Version
  • ticket-query-service — port 8083, 읽기 전용. Hikari read-only: true + @Transactional(readOnly = true). validate 모드
  • mock-data.sql — Event 1 / Section 3 / Seat 292 시드. 멱등
  • 부하 테스트 시나리오 A, B — k6 작성. baseline 측정

아직 미구현

  • gateway (뼈대만)
  • Queue API (대기열 진입)
  • Scheduler (1초마다 ZPOPMIN 100)
  • Worker (MQ → DB INSERT)
  • MyKafka client SDK
  • DB Primary/Replica 분리

다음 한 걸음

부하 테스트에서 cold/warm 분리, 또는 시나리오 C(점진 증가)를 진행할 차례다. 그 결과로 Worker 도입 트리거를 직접 확인하는 게 다음 마일스톤이다.


5. 컴포넌트 매핑표

다이어그램 부품과 실제 구현이 어떻게 대응되는지 표로 정리하면 진행 정도가 한눈에 보인다.

다이어그램 부품우리 구현상태
API Gatewaygateway/뼈대만
Queue API(없음)미구현
Booking APIticket-command-serviceDONE ★
(다이어그램 외) Read serviceticket-query-serviceDONE ★
Scheduler(없음)미구현
대기열 RedisMyRedis 공용ZSET/SETNX/Lua 지원 확인 필요
잔여좌석 RedisMyRedis 공용위와 동일
MQMyKafkaMVP DONE, client SDK 없음
Worker(없음)미구현
RDBPostgres ticket_db단일 (replica 미분리)

6. 진화 순서 (현재 위치)

각 단계는 이전 단계에서 측정된 한계가 드러난 뒤 진행한다. 다음 단계의 도입 시점은 부하 테스트가 결정한다.

text
[1단계 · 현재] command 동기 DB 쓰기 (비관락)
    ↓ 부하 테스트로 한계 측정 ← 지금 여기
[2단계] command → MyKafka publish → Worker → DB INSERT (비동기)
    ↓ "API 폭주에도 안 죽음" 학습
[3단계] DB Primary/Replica → query는 replica
    ↓ "조회 부하 분산 + replica lag" 학습
[4단계] Queue API + Scheduler 도입 (대기열 throttling)
    ↓ "예매 진입 자체를 제어" 학습
[5단계] Gateway 완성 (JWT 검증 + 라우팅)

이 순서가 왜 이 순서인지는 후속 글의 §8에서 따로 다룬다. 짧게 말하면, 각 단계가 직전 단계에서 발생한 병목을 해결하도록 설계되어 있어 역순이 거의 불가능하다.


7. 메타 학습 원칙

이 프로젝트 내내 의식적으로 따르는 규칙 5개다. 어떤 결정에 부딪혔을 때 "이 원칙이 어느 쪽을 가리키는가"로 판단의 기준이 된다.

  1. 학습은 한 번에 한 변수. 새 개념과 새 도구를 동시에 도입하지 않는다
  2. 동작하는 최소 → 측정 → 한계 → 진화. 다이어그램 그대로 베끼지 않는다
  3. 공유 모델로 묶지 않는다 (지금은). CQRS의 자유도 보존
  4. 권한을 미리 잠가둔다. 미래 분리가 쉬워지는 방향
  5. 인프라 설치는 사용자가 직접. 도구 자동화는 안내까지만

특히 1번은 자주 깨진다. 시나리오 B 측정 중에 JIT warm-up이라는 숨은 변수가 끼어들어 "비관락이 잡혔는데 latency가 줄어드는" 역설적 결과를 본 적이 있다. 사후에 발견했고, 이게 또 좋은 학습거리가 됐다.


마치며: 이 시리즈의 다음 글

이 글은 "지도"다. 다음 글들에서 각 결정의 골목길로 들어간다.

  • §1. 왜 MSA인가 — 모놀리스가 아니라
  • §2. 왜 CQRS-ish 분리인가 — ticket-command / ticket-query 두 서비스
  • §3. 왜 같은 DB로 시작했나 — CQRS면 보통 DB도 갈라야 한다는데
  • §4. 왜 Bulkhead — 대기열 ↔ 예매 자원 격리
  • §5. 왜 Gateway — 단일 진입점의 가치와 비용
  • §6. 왜 직접 인프라를 구현했나 — MyRedis, MyKafka
  • §7. 왜 비관락 + @Version 둘 다
  • §8. 진화 순서가 왜 이 순서인가

각 §은 "상황 / 옵션 / 선택 / 왜 / 트레이드오프 / 토론거리" 5단 구조로 정리한다. "왜 그 옵션은 안 택했어요?"에 답할 수 있도록.

MSA는 만드는 것보다 운영하는 게 어렵다. 그리고 운영 복잡도를 모르고 도입한 MSA는 모놀리스보다 못한 분산 모놀리식으로 빠지기 쉽다. 그래서 이 프로젝트는 단계마다 "여기서 어디가 부서지는가"를 직접 보고 다음 단계로 간다. 다이어그램을 보고 처음부터 베끼지 않는 이유가 그것이다.

#MSA#Architecture#CQRS#Kafka#Redis#Bulkhead#Ticketing

황호민

Backend Engineer · Java/Kotlin · Spring Boot · Next.js