가상 면접 사례로 배우는 대규모 시스템 설계 기초 2에서는 마지막 장에서 거래소의 핵심 구조인 Matching Engine을 중심으로 시스템 아키텍처를 설명한다.
책을 읽고 거래소 시스템 구조를 정리하고, 핵심 개념인 Order Book과 매칭 로직을 Rust로 간단히 정리해 보았다.
일반적인 증권거래소 시스템은 다음과 같은 구조를 가진다.
Client
↓
Order Gateway
↓
Matching Engine
↓
Market Data
↓
Persistence
각 구성 요소의 역할은 다음과 같다.
외부 클라이언트의 진입점이다.
거래소의 핵심 컴포넌트다.
체결 결과와 호가 정보를 외부에 전달한다.
예를 들어
같은 데이터가 여기서 생성된다.
거래 기록을 저장하는 계층이다.
실제 거래소에서는 이벤트 로그 기반 저장을 많이 사용한다.
대표적인 거래소 예로는
같은 시스템이 있다.
거래소의 핵심 데이터 구조는 Order Book이다.
오더북은 체결되지 않고 대기 중인 주문을 관리하는 자료구조다.
주문은 두 가지로 나뉜다.
매수(Bid) — 높은 가격 우선 매도(Ask) — 낮은 가격 우선
───────────────────── ─────────────────────
100,500원 x 10주 101,000원 x 5주
100,000원 x 20주 101,500원 x 15주
99,500원 x 5주 102,000원 x 8주
체결은 다음 조건이 만족되면 발생한다.
매수 최고가 ≥ 매도 최저가
대부분의 거래소는 Price-Time Priority 규칙을 사용한다.
우선순위는 다음과 같다.
1️⃣ 가격 우선
2️⃣ 시간 우선
예를 들어 같은 가격에 두 주문이 있다면
10:01 주문
10:03 주문
먼저 들어온 주문이 먼저 체결된다.
오더북에서 가장 중요한 연산은 두 가지다.
1️⃣ 최우선 호가 조회
2️⃣ 같은 가격 내 FIFO 유지
이 두 조건을 만족하기 위해 다음 자료구조를 사용했다.
pub struct OrderBook {
bids: BTreeMap<Reverse<u64>, VecDeque<Order>>,
asks: BTreeMap<u64, VecDeque<Order>>,
}
구조 선택 이유는 다음과 같다.
keys().next()
Rust의 기본 정렬은 오름차순이다.
매수 주문은 높은 가격 우선이므로 Reverse로 뒤집는다.
같은 가격의 주문은 FIFO를 유지해야 한다.
push_back()
pop_front()
이 구조로 Price-Time Priority 규칙을 자연스럽게 구현할 수 있다.
매수 주문이 들어왔을 때 매칭 흐름은 다음과 같다.
1️⃣ 매도 측 최저가 확인
2️⃣ 가격 조건 확인
3️⃣ 체결 수량 계산
4️⃣ 주문 잔량 업데이트
5️⃣ 완전 체결 주문 제거
핵심 로직은 다음과 같다.
fn match_buy(&mut self, mut taker: Order) -> Vec<Trade> {
체결 가격은 항상 maker 주문 기준이다.
즉 먼저 들어온 주문의 가격으로 거래가 체결된다.
실제 거래소도 동일한 규칙을 사용한다.
서비스 간 통신 방식으로 REST 대신 gRPC를 사용했다.
| REST | gRPC | |
|---|---|---|
| 인터페이스 | OpenAPI | proto |
| 직렬화 | JSON | Protobuf |
| 성능 | 텍스트 | 바이너리 |
| 스트리밍 | 별도 구현 | 기본 지원 |
매칭엔진처럼 대량의 주문을 빠르게 처리해야 하는 서비스에서는 JSON 직렬화 비용이 부담이 될 수 있다.
Protobuf는
이라는 장점이 있다.
gRPC에서는 proto 파일이 서비스 계약(contract) 역할을 한다.
service MatchingEngine {
rpc SubmitOrder(OrderRequest) returns (OrderResponse);
rpc CancelOrder(CancelRequest) returns (CancelResponse);
rpc GetOrderBook(Symbol) returns (OrderBookSnapshot);
}
이 파일 하나로
가 자동 생성된다.
Rust에서는 tonic을 사용했다.
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("../proto/matching.proto")?;
Ok(())
}
이번 구현은 구조 이해를 위한 간단한 실험이다.
실제 거래소 시스템은 훨씬 복잡하다.
예를 들어
같은 기술들이 사용된다.
증권거래소 시스템에서 가장 중요한 컴포넌트는 Matching Engine이다.
핵심은 복잡한 알고리즘보다 자료구조 설계에 있다.
이번 글에서는
BTreeMap + VecDeque
조합을 사용해 Price-Time Priority 기반 오더북 구조와 매칭 로직을 Rust로 정리해 보았다.