박종훈 기술블로그
close menu

K-DEVCON 시스템 디자인 스터디 시즌 3 기록 - 『대규모 시스템 설계 기초 2』 7~13장

시즌 2를 마치고 약 한 달간 쉬었다가, 2026년 2월 26일부터 시즌 3를 시작했다. 교재는 『가상 면접 사례로 배우는 대규모 시스템 설계 기초 2』의 나머지, 7~13장이다.

지난 기수 인원이 전원 그대로 참여 의사를 보여주셔서, 이번 기수는 같은 멤버로 진행했다. 그래서인지 처음부터 논의가 깊게 들어갔다.

회차별 기록

1회차 — 호텔 예약 시스템

  • 책이 제시한 전제부터 의심해보기. 이 챕터는 5천 개 호텔, 100만 개 객실을 기준으로 설계한다. 함께 스터디를 운영하는 시온님은 늘 이 “가정의 현실성”을 먼저 검토하시는 분인데, 이번에도 직접 찾아보시고 실제 메리어트가 약 1만 개 호텔, 150만 개 이상의 객실을 보유하고 있다는 자료를 공유해주셨다. 왜 그렇게 하시냐고 여쭤보니, 시스템 규모에 따라 적용해야 할 솔루션이 달라지기 때문에 전제 자체를 꼼꼼히 확인한다고 하셨다. 설계에서 중요한 건 어떤 전제를 두고 문제를 정의하느냐라는 걸 다시 느꼈다.
  • MSA는 정말 필요한가. 대부분의 시스템은 모놀리식으로도 충분하고, 서버 스펙을 더 올릴 수 없거나 조직이 커져 도메인을 강제로 찢어야 할 때가 도입 시점이 아닐까 하는 게 내 의견이었다. 그런데 반대 의견을 주신 분이 있었다. 서비스가 커질 대로 커진 후에 분리를 시도하고 있는데 이미 얽힐 대로 얽혀서 매번 분리에 실패하고 있다는 사례였다. 결론은 초기에는 모놀리식으로 가되 도메인 경계를 명확히 하는 모듈화에 신경 쓰자는 쪽이었다.
  • SAGA 패턴. “일반 업무에 Saga까지 도입하는 건 과하지 않나” 싶다가도, 1원의 오차도 허용되지 않는 금융 서비스라면 이야기가 달라진다. 오케스트레이션 방식을 쓴 토스뱅크 환전 사례 영상도 공유받았다.
  • 샤딩 실패담. MongoDB로 샤딩을 하다가 샤드 키를 적절히 잡지 못해 자동 리밸런싱이 발생했고, 그 과정에서 에러가 났다는 사례. 클라우드 벤더에 문의했더니 “애플리케이션 레벨에서 재시도 로직을 구현하라”는 원론적 답변만 돌아왔다고 한다. 결과적으로 샤드 개수를 너무 크게 잡았던 것 같다는 코멘트가 인상적이었다.

이번 회차는 전반적으로 오버엔지니어링에 대한 이야기를 많이 나눈 시간이었다.

K-DEVCON 원문 후기

2회차 — 분산 이메일 서비스

  • 이메일 프로토콜은 정말 오래됐다. SMTP는 1981년, POP은 1984년, IMAP은 1986년에 나왔다. HTTP가 1991년이니 훨씬 앞선다. 한 회원님이 이메일 프로토콜이 HTTP보다 앞자리 포트를 할당받았다는 것에서도 유추할 수 있다는 재미있는 관점을 이야기해주셨다.
  • 파일 기반 저장 구조가 남긴 흔적. 전통적 이메일 시스템은 메일을 파일로 저장했다. 메일 서비스가 횟수가 아니라 저장 용량 기반으로 과금하는 것도, 리눅스나 OpenJDK 같은 오래된 오픈소스가 메일링 리스트로 협업해온 것도 여기서 오지 않았을까 싶었다. 메일이 파일이니 웹 서버에 올리면 그 자체로 아카이브가 됐을 것이다.
  • NoSQL에서 “안 읽은 메일”을 빠르게 찾으려면. RDB라면 is_read에 인덱스를 걸면 되지만, DynamoDB나 Cassandra는 파티션 키·클러스터링 키 기반 조회에 특화되어 있어 WHERE is_read = false 같은 쿼리는 전체 파티션 스캔이 된다. 그래서 비정규화를 쓴다. 읽은 메일 테이블과 안 읽은 메일 테이블을 분리하고, 읽으면 한쪽에서 지우고 다른 쪽에 넣는다.
  • 이메일 스레드가 이상하게 묶이는 이유. 스레딩은 생각보다 표준화가 안 되어 있다. JWZ 알고리즘은 Message-Id, In-Reply-To, References 헤더로 트리를 만드는데, 모든 클라이언트가 이 헤더를 정확히 기록하지는 않는다. 그래서 Gmail은 헤더를 완전히 신뢰하지 않고 제목 정규화(Re:, Fwd: 제거 후 비교)와 시간 근접성을 조합한다. 헤더가 불완전한 메일도 묶을 수 있는 대신, “회의 일정 확인” 같은 제목의 서로 다른 메일이 한 스레드로 합쳐지는 일이 생긴다.
  • 번외로 재미있었던 것들. 이메일 표준에는 읽음 확인 기능이 없다. 본문 끝에 1픽셀 투명 이미지를 넣고 그 로드 요청을 감지하는 편법을 쓴다(그래서 포워딩된 메일을 남이 열어도 읽음으로 잡힌다). Gmail의 전송 취소는 설정한 시간 동안 실제로 보내지 않고 서버에서 대기시키는 지연 발송이다. 그리고 전 세계 이메일의 약 50%가 스팸이라, 정상 메일을 보내려면 SPF·DKIM·DMARC 등록이 사실상 필수다.

K-DEVCON 원문 후기 → 같은 회차에 대한 다른 참여자분들의 후기: 강주희 님, 윤서영 님

3회차 — S3와 유사한 객체 저장소

이 회차는 강주희 님이 후기를 작성해주셨다.

4회차 — 실시간 게임 순위표

이 회차는 후기 글을 남기지 못했다.

5회차 — 결제 시스템

  • PSP와 PG는 다르다. 한국에서는 PG(Payment Gateway)라는 표현을 많이 쓰지만, 엄밀한 의미의 PG는 데이터 전달이라는 기술적 게이트웨이에 가깝다. 여러 결제 수단을 통합해 제공하고 정산과 리스크 관리(FDS)까지 하는 공급자는 PSP(Payment Service Provider)다. PayPal·Stripe·Adyen, 국내로는 Toss Payments·NHN KCP·KG Inicis 등이 여기 해당한다.
  • 지갑과 원장. 지갑(wallet)은 상태를 저장하고, 원장(ledger)은 그 상태가 만들어지기까지의 금융 기록 전체를 남긴다. 시스템 오류나 데이터 변조를 감지할 수 있게 해준다.
  • DLQ가 꼭 필요할까. 재시도 큐로 일시적 문제는 해결되지만, 해결되지 않는 문제는 실패 메시지 큐(DLQ)로 격리해야 Poison Pill이 시스템을 마비시키는 걸 막을 수 있다. 그런데 원천 데이터가 잘 보관되어 있다면 배치나 스크립트로 다시 큐에 밀어넣으면 되지 않느냐는 이야기도 나왔다. 구독자가 하나면 그럴 수 있지만, 여럿일 경우 문제가 된다. DLQ는 각 구독자 서비스가 스스로 문제를 격리해 해결할 수 있는 구조를 만들어준다.
  • 대사(reconciliation)와 조정. 서로 다른 두 데이터 세트를 비교해 일치하는지 확인하고, 다르면 그 차이를 해소한다. 자동화할 수 있는 부분은 자동화하되 비용이 높으면 재무팀이 수동으로 조정하기도 한다. 조정이 끝나면 최종 확정(settlement)한다. 우버는 이벤트의 모든 부수 효과가 정상 발생했는지 확인하고 실패 시 경고를 보내는 별도의 검증기(validator)를 뒀다고 한다.

이번 시스템의 목표 TPS는 높지 않았다. 결제 시스템은 단순한 기능이 아니라 장애를 견디는 구조를 설계하는 문제였다.

K-DEVCON 원문 후기 → 관련 글: Outbox 패턴과 CDC

6회차 — 전자 지갑

전자 지갑은 높은 TPS보다 정확성이 중요한 서비스다. 그래서 분산 트랜잭션과 데이터 일관성 전략을 집중적으로 다뤘다.

  • 2PC의 진짜 문제. 2PC 자체가 나쁜 게 아니라, 커밋 단계에서 실패하면 정합성이 맞춰질 때까지 무한 대기에 들어가야 한다는 점이 문제다. 이게 서비스 전체의 마비로 이어질 수 있다. 그래서 2PC는 정산 시스템처럼 실시간성보다 정합성이 압도적으로 중요하고, 문제 시 잠시 멈추고 복구하는 게 용납되는 영역에서만 제한적으로 쓰인다.
  • Rollback과 보상은 다르다. TCC를 이야기하다 나온 정리인데 명확해서 남겨둔다. Rollback은 커밋 이전 상태라 아직 외부에 노출되지 않은 것을 되돌리는 것이고, 보상은 이미 커밋되어 외부에 노출된, 물리적으로 되돌릴 수 없는 상태에서 반대 동작을 수행해 결과적으로 0을 만드는 것이다.
  • TCC를 실제로 쓰시는 분의 이야기도 들었다. Try 단계에서는 빠르고 가볍게 자원을 선점하고 Confirm 단계에서 무거운 작업을 수행해 이중 처리를 막고, 시스템이 Cancel을 못 해준 경우엔 배치로라도 보상이 되도록 보장한다고 하셨다.
  • 이벤트 소싱과 RocksDB. 데이터가 꼬이는 근본 원인은 대개 Update에 있다. 모든 상태 변화를 Insert만으로 기록하면 재현성과 추적 가능성을 얻는다. 대신 매번 처음부터 계산하면 느려서 CQRS나 스냅샷을 쓴다. 책이 RocksDB를 고른 이유도 명확하다. B-Tree 기반 RDB는 insert 시 리밸런싱과 랜덤 I/O가 발생하지만, LSM은 메모리에 먼저 쌓고 디스크에 순차 쓰기를 하기 때문에 이벤트 소싱에 어울린다.
  • 그런데 이걸 실제로 쓸 수 있을까. 쉽지 않을 것 같다. 충분한 레퍼런스가 쌓이기 전까지 돈이 오가는 곳에서 새로운 컨셉을 섣불리 도전하기는 어렵다. 금융권이 여전히 Oracle RAC 같은 거대 솔루션에 의존하는 이유라고 생각한다.

K-DEVCON 원문 후기

7회차 — 증권 거래소 (完)

  • Limit Order Book 자료구조. How to Build a Fast Limit Order Book을 따라가 봤다. 책은 각 가격을 double linked list로 연결하는데, 이러면 새 가격 추가가 O(n)이 된다. 원문은 Limit 객체의 limitPrice를 기준으로 트리를 구성해 삽입을 O(log M)으로 만들고, 나머지 연산은 double linked list와 map으로 O(1)을 달성한다.
  • GC조차 허용되지 않는 세계. 매칭 엔진에서 JVM 계열을 쓸 때는 GC가 최대한 발생하지 않도록 Object Pooling으로 설계한다. ZGC가 10ms 내를 보장한다고 해도, 주식 거래에서는 그것도 큰 딜레이다.
  • 매칭 엔진은 왜 수평 확장을 안 하는가. 대규모 트래픽이면 보통 수평 확장을 택하지만, 거래소 체결 엔진은 순서 보장이 최우선이라 단일 노드에서 시퀀셜하게 처리한다(그래서 Kafka 같은 시스템도 쓰기 어렵다). 확장이 안 되는 구조 아니냐는 질문이 나왔는데, 맞다. 병렬 처리 대신 직렬 처리와 안정성을 선택한 케이스다. 장애 대비로는 스탠바이 서버를 둔다.
  • 그럼 코드 버그로 주 서버가 죽으면 부 서버로 넘어가도 버그가 따라오지 않나? 좋은 질문이었고, 토론 결론은 이랬다. 부 서버는 인프라 장애를 대비하는 용도이고, 코드 레벨 장애는 그 책임 범위가 아니다. 코드 버그는 충분한 사전 검증으로 해결해야 한다.
  • 매도가 먼저 차감될까, 매수가 먼저 차감될까. 당일에는 답을 정확히 못 했는데, “누가 먼저 대기하고 있었는가”로 생각하니 명확해졌다. 매칭 엔진은 순차 처리에 특화되어 있어 양쪽 큐에 요청이 동시에 쌓여 있는 상황은 없다. 먼저 만들어진 대기자(Maker)가 있고 그것을 차감시키는 요청자(Taker)가 있으므로, 엔진은 장부에 미리 기록된 Maker의 수량을 최우선으로 차감하고 그 결과에 따라 Taker의 남은 수량을 처리한다.
  • 콜로케이션이나 Direct Connect로 네트워크 홉을 줄이는 HFT의 인프라 전략 이야기도 나왔다.

이번 회차에서 나온 이야기를 마인드맵으로 정리하면 다음과 같다.

증권 거래소 시스템 디자인 마인드맵

K-DEVCON 원문 후기

시즌 3를 마치며

7회차를 끝으로 시스템 디자인 스터디 3기를 마쳤다. 마지막 날에는 스터디원 전원이 참여해주셨다.

세 시즌에 걸쳐 『대규모 시스템 설계 기초』 1권과 2권을 완주했다. 돌아보면 이 스터디에서 가장 크게 얻은 건 책 내용 자체보다, 책이 한 줄로 넘어간 곳에서 멈춰 서서 원문을 찾아보는 습관이었던 것 같다. 정족수 프로토콜, Snowflake의 남는 1비트, 차분 동기화, Levenshtein 오토마타, delta-of-delta, Maker/Taker 차감 순서 — 전부 책만 읽었으면 그냥 지나쳤을 것들이다.

시리즈