msa

MSA — 왜 이렇게 설계했나

msa 로 돌아가기

각 설계 결정의 상황 / 옵션 / 선택 / 왜 / 트레이드오프 5단 구조. 주니어와 토론할 때 "왜 그 옵션은 안 택했어요?" 에 답할 수 있도록.


§1. 왜 MSA인가 (모놀리스가 아니라)

상황

티켓팅 서비스. 핵심 도메인 몇 개 (auth, 좌석조회, 예약쓰기, 대기열, 결제, ...).

옵션

선택: B (MSA)

  1. 트래픽 패턴이 도메인마다 다름
    • 좌석 조회: 항상 매우 높음 (한 사람이 N번 봄)
    • 예약 쓰기: 폭발적으로 짧게 (오픈 직후 수십 분)
    • 대기열 polling: 지속적, 인원수 비례
    • 모놀리스면 한 서비스를 통째로 스케일해야 함. 비효율.
  2. 장애 격리. 결제 모듈 버그가 좌석 조회를 멈추면 안 됨.
  3. 독립 배포·기술 선택. Worker는 가벼운 언어로 가도 됨. 학습 단위가 분리.
  4. 학습 목적이 MSA 자체. 모놀리스로 짜면 MSA 학습이 안 됨 (당연한 말이지만 진지하게).

트레이드오프 (의식하고 받아들임)

토론거리


§2. 왜 CQRS-ish 분리인가 (ticket-command / ticket-query)

상황

한 도메인(티켓)이지만 read와 write의 성격이 매우 다름.

옵션

선택: B

  1. 읽기/쓰기 스케일링 곡선이 다름 — 분리하면 각각 독립 스케일.
  2. DB Primary/Replica 분리할 때 query 쪽 코드 변경 거의 없음. URL만 바꾸면 됨.
  3. 읽기 최적화 모델(Read Model)로 분기할 자리.
    • command: 정규화된 Reservation(status, createdAt)
    • query: 비정규화된 ReservationProjection(status, createdAt, eventTitle, seatLabel, totalPrice)
  4. CQRS는 말로만 들어선 안 와닿음. 두 서비스가 같은 DB를 읽기/쓰기로 나눠 가지는 게 가장 작은 CQRS.

의도적 디자인

트레이드오프

토론거리


§3. 왜 같은 DB로 시작했나

상황

CQRS를 하기로 했으면 보통 "command DB / query DB" 둘로 갈라야 한다고 가르침.

옵션

선택: B

진화 단계

[1] 같은 DB (강한 일관성, 학습 변수 0)
    ↓
[2] Primary/Replica 분리 (replica lag 등장 — 학습)
    ↓
[3] command DB / query DB 완전 분리 + Kafka 동기화 (eventual consistency, outbox — 학습)

각 단계에서 무엇이 풀리고 무엇이 새로 어려워지는지 직접 체감하는 게 목표.

토론거리


§4. 왜 Bulkhead (대기열 ↔ 예매 자원 격리)

다이어그램의 핵심 결정.

문제 상황

콘서트 오픈 직후: 수만 명이 동시에 대기열 polling. 동시에 (먼저 입장한) 수백 명이 예매 시도. 같은 서버군이 받으면 polling 트래픽이 예매를 죽임.

Bulkhead 패턴이란

배의 격벽에서 따온 이름. 한 칸에 물이 들어와도 다른 칸은 안 잠긴다. 시스템 자원(서버, DB connection pool, thread pool, Redis)을 용도별로 격리.

우리 적용

효과

트레이드오프

토론거리


§5. 왜 Gateway를 앞에 두나

옵션

선택: B

  1. URL 라우팅 단일화/queue/* → Queue API, /booking/* → Booking API. 클라이언트는 한 URL만 안다.
  2. 횡단 관심사 집중 — JWT 검증, rate limiting, 로깅, CORS, TLS termination 한 곳에서.
  3. 내부 서비스 토폴로지 캡슐화 — 내부에서 서비스 분리/합치기 자유로움.
  4. 버전 라우팅/v1/*/v2/* 같은 점진 마이그레이션.

안 좋은 점

우리 선택

Spring Cloud Gateway. 같은 JVM 생태계, 같은 팀, 학습 비용 최소.

토론거리


§6. 왜 직접 인프라를 구현했나 (MyRedis, MyKafka)

상황

운영하려면 AWS ElastiCache, AWS MSK, 또는 docker로 redis/kafka 띄우면 됨. 그게 정상.

왜 직접 구현

의도적으로 안 넣은 것 (MyKafka 예)

→ 각각 "왜 필요한가"의 학습 거리. 부재가 한계로 드러나면 그때 구현.

트레이드오프

토론거리

자세한 Kafka 이야기 → kafka


§7. 왜 비관락 + @Version 둘 다

코드

@Lock(LockModeType.PESSIMISTIC_WRITE)
fun findAllByIdIn(ids: Collection<String>): List<SeatEntity>

@Version
var version: Long = 0

왜 둘 다

동시성 제어 옵션 비교

방식 장점 단점 어디 좋나
비관락 명확, 정확 DB 락 대기 → 트래픽 폭주 시 병목 경쟁 빈도 높고 충돌 비용 큰 경우
낙관락 락 안 잡음, 처리량 ↑ 충돌 시 재시도 로직 경쟁 빈도 낮은 경우
Redis SETNX DB 부담 X Redis 의존, 정합성 검증 어려움 빠른 응답, 짧은 lock
Kafka 파티션 큐잉 자연 직렬화 응답 지연, 복잡도 ↑ 처리 순서 보장 + 비동기 OK

다이어그램의 잔여좌석 Redis(SETNX + Lua)는 3번. 우리는 1번에서 시작해 부하 테스트로 한계 측정 후 진화.

토론거리


§8. 점진적 진화의 순서가 왜 이 순서인가

[1] command 동기 쓰기 (현재)
[2] + Worker (MyKafka 비동기 영속화)
[3] + DB Primary/Replica
[4] + Queue API + Scheduler
[5] + Gateway 완성 (JWT, 라우팅)

이 순서의 근거

왜 이 순서가 아니면 안 되나

토론거리


§9. 의식적인 메타 원칙

이 프로젝트 내내 따르는 6개 규칙. 각 결정에 영향:

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

이 원칙들이 깨질 때 학습이 생긴다

시나리오 B 측정에서 원칙 1을 의도치 않게 어김 (JIT warm-up이 숨은 변수). 사후에 발견 → 학습으로 전환. → discussion §5 참조.