증권 거래소
- 13.1 문제 이해 및 설계 범위 확정
- 13.2 개략적 설계안 제시
- 13.3 상세 설계
- 13.4 마무리
**증권 거래소(Stock Exchange)**는 나스닥이나 NYSE 같은 주식 거래 시스템이야. 이 책의 마지막 장이자 가장 난이도가 높은 문제지. 극한의 낮은 지연시간이 요구되고, 매칭 엔진, 주문장(order book), 시퀀서(sequencer) 같은 금융 도메인 특유의 개념이 등장해.
기능 요구사항을 보면:
- 사용자가 매수/매도 주문을 제출한다
- 시스템이 매수와 매도를 매칭해서 거래를 체결한다
- **시장가(market order)**와 **지정가(limit order)**를 지원한다
- 실시간 **주문장(order book)**을 제공한다
비기능 요구사항이 극단적이야.
- 초저지연시간 — 주문 접수부터 체결까지 마이크로초~밀리초 단위. 1밀리초라도 느리면 차익거래 기회를 놓치지
- 높은 가용성 — 거래 시간 동안 다운되면 수십억 원의 손실
- 결정론적(deterministic) 처리 — 같은 주문이 같은 순서로 들어오면 항상 같은 결과가 나와야 해
- 보안과 규정 준수 — 금융 규제가 매우 엄격
규모 — 하루 거래 건수 수십억 건, 초당 수만~수십만 건의 주문. 하지만 QPS보다 지연시간이 더 중요한 문제야.
핵심 컴포넌트들을 보자.
게이트웨이(Gateway) — 외부에서 들어오는 주문을 받아서 유효성 검증, 속도 제한 등을 수행한 뒤 내부로 전달하지.
시퀀서(Sequencer) — 모든 주문에 **고유한 순서 번호(sequence number)**를 부여해. 이게 왜 중요하냐면, 매칭 엔진이 주문을 처리하는 순서가 공정해야 하거든. 먼저 접수된 주문이 먼저 매칭돼야 하지. 시퀀서가 주문의 **전체 순서(total order)**를 확정해.
매칭 엔진(Matching Engine) — 증권 거래소의 심장이야. 매수 주문과 매도 주문을 매칭해서 거래를 체결하지. 주문장(order book)을 관리하면서, 새 주문이 들어오면 기존 주문과 매칭 가능한지 확인해.
주문장(Order Book) — 아직 체결되지 않은 주문들의 목록이야. 매수 쪽과 매도 쪽이 분리되어 있지. 매수 쪽은 가격 내림차순(높은 가격이 우선), 매도 쪽은 가격 오름차순(낮은 가격이 우선)으로 정렬돼.
시장 데이터 서비스 — 체결된 거래 정보, 실시간 호가 등을 외부에 전파하지.
보고/감사 서비스 — 모든 주문과 거래를 기록해서 규제 기관에 보고해.
거래 체결 흐름은 이래:
- 트레이더가 매수 주문 제출 (예: AAPL 100주를 $150에 매수)
- 게이트웨이가 유효성 검증 후 시퀀서로 전달
- 시퀀서가 순서 번호를 부여
- 매칭 엔진이 주문장에서 $150 이하의 매도 주문을 탐색
- 매칭 가능한 매도 주문이 있으면 → 거래 체결, 양쪽 주문장에서 제거 (또는 부분 체결)
- 매칭 불가능하면 → 주문장에 추가하고 대기
분산 시스템에서 "이 주문이 저 주문보다 먼저 왔다"를 정확히 판단하는 건 어려운 문제야. 서버마다 시계가 미세하게 다르고(clock skew), 네트워크 지연도 다르니까.
시퀀서는 단일 지점에서 순서를 부여하는 방식으로 이 문제를 해결해. 모든 주문이 시퀀서를 통과하면서 순차적 번호를 받으니까, 이 번호가 곧 공정한 처리 순서가 되지.
시퀀서가 단일 장애점(SPOF)이 되지 않도록 이벤트 로그(event log) 기반으로 구현해. 시퀀서가 주문에 번호를 매기면서 동시에 로그에 기록하지. 시퀀서가 죽으면 로그에서 마지막 번호를 찾아서 이어 시작해.
실전에서는 시퀀서와 이벤트 로그에 Raft 같은 합의 프로토콜을 적용해서 여러 노드로 복제해. 리더 시퀀서가 죽으면 팔로워가 빠르게 승격하지.
매칭 엔진의 성능은 주문장의 자료구조에 달려 있어.
매수 쪽과 매도 쪽 각각 가격 레벨(price level) 별로 주문을 관리해. 같은 가격의 주문은 **시간 순서(FIFO)**로 정렬하지.
자료구조 선택:
- 가격 레벨을 빠르게 탐색해야 하니까 정렬된 맵(sorted map/tree)
- 각 가격 레벨 내에서는 큐(queue) — 먼저 들어온 주문이 먼저 매칭
새 매수 주문이 들어오면:
- 매도 쪽 주문장에서 가장 낮은 매도 가격을 확인
- 매수 가격 >= 매도 가격이면 매칭 성공 → 거래 체결
- 부분 체결이면 남은 수량은 주문장에 유지
- 매칭 불가능이면 매수 쪽 주문장에 추가
이 연산의 시간 복잡도가 매우 중요해. 정렬된 트리를 쓰면 최적 가격 조회가 O(1)(최솟값/최댓값), 삽입이 O(log n)이지.
같은 입력이 주어지면 항상 같은 출력이 나와야 해. 이건 테스트와 디버깅에도 중요하고, 복구 시에도 필수지.
이를 위해 매칭 엔진은 외부 상태에 의존하지 않아. 현재 시각, 랜덤 값 같은 비결정적 요소를 사용하지 않고, 오직 시퀀서가 부여한 순서대로 주문을 처리하지.
장애 복구 시 이벤트 로그를 처음부터 재생하면 동일한 주문장 상태를 복구할 수 있어. 12장의 이벤트 소싱과 같은 원리야.
일반적인 웹 서비스와는 완전히 다른 최적화가 필요해.
단일 스레드 매칭 — 락 경쟁을 없애기 위해 매칭 엔진을 단일 스레드로 돌려. "동시성으로 처리량을 올리는" 일반적인 접근의 반대지. 락이 없으니 오버헤드가 제로이고, 결정론적 처리도 자연스럽게 보장돼. 단일 스레드의 처리량이 충분하다면 이게 최적이야.
메모리 기반 처리 — 주문장과 매칭 로직이 전부 메모리에서 동작해. 디스크 I/O는 이벤트 로그 기록 시에만 발생하지.
네트워크 최적화 — 커널 바이패스(DPDK, kernel bypass), FPGA 등으로 네트워크 지연시간을 극한까지 줄여. 일반적인 TCP/IP 스택을 거치지 않고 NIC에서 직접 데이터를 읽는 방식이야.
GC 없는 언어 — C++이나 Rust로 매칭 엔진을 구현해. Java/Go의 가비지 컬렉션이 일으키는 예측 불가능한 지연(stop-the-world)을 피하기 위해서지.
핫 패스 최적화 — 가장 빈번하게 실행되는 코드 경로를 CPU 캐시에 최대한 올려서 캐시 미스를 줄여.
체결된 거래 정보와 실시간 호가를 트레이더와 외부 시스템에 전파해야 해. 여기는 **멀티캐스트(multicast)**나 전용 메시징 시스템을 사용하지. 일반적인 REST API로는 지연시간 요구를 맞출 수 없거든.
주문이 매칭 엔진에 도달하기 전에 리스크 체크를 수행해. 주문 수량 한도, 잔고 확인, 가격 이상 감지 등이지. 이 체크도 마이크로초 단위로 끝나야 하니까 인메모리 캐시에서 처리해.
증권 거래소는 시스템 설계의 극한 버전이야. 일반적인 웹 서비스에서 "충분히 빠른" 수준의 지연시간이 여기서는 "용납할 수 없이 느린" 수준이 되지. 단일 스레드 매칭, 커널 바이패스, GC 없는 언어 같은 기법은 일반적인 시스템 설계에서는 잘 안 보이지만, "지연시간이 돈인" 도메인에서는 필수야.
이 장에서 다룬 시퀀서, 이벤트 소싱, 결정론적 처리 같은 개념은 증권 거래소뿐 아니라 주문의 공정한 처리가 필요한 모든 시스템에 적용할 수 있지.
정리
13장 읽고 기억할 거 세 가지:
- 시퀀서가 모든 주문에 전체 순서를 부여해서 공정한 처리를 보장해. 분산 시스템에서 "누가 먼저?"를 확정하는 건 어려운 문제인데, 시퀀서가 단일 지점에서 이를 해결하지.
- 매칭 엔진은 단일 스레드로 동작하며 주문장을 메모리에서 관리해. 락 오버헤드를 없애고 결정론적 처리를 보장하기 위한 설계 선택이야. 처리량보다 지연시간이 우선인 도메인이지.
- 초저지연을 위해 GC 없는 언어, 커널 바이패스, 인메모리 처리 같은 극한 최적화를 적용해. 일반적인 웹 서비스 설계와는 완전히 다른 세계지만, 트레이드오프를 이해하는 데 좋은 사례야.