1 minute read

거래소 시리즈 세 번째 글입니다. 1편2편에서 Java/Spring으로 거래소를 만들었다면, 이번에는 그걸 통째로 갈아엎은 이야기입니다.

왜 갈아엎었나

Spring Boot 기반 거래소는 잘 돌아갔습니다. 그런데 운영을 하다 보니 두 가지가 계속 마음에 걸렸습니다.

메모리 풋프린트. 마이크로서비스 17개를 띄우면 JVM 힙만으로도 만만치 않습니다. ECS Fargate 비용은 메모리에 비례하는데, 서비스 하나당 512MB~1GB씩 잡아먹으니 개인이 감당하기엔 부담스러웠습니다.

매칭 엔진 레이턴시. 체결 로직은 GC 멈춤이 없어야 합니다. Java로도 튜닝하면 되지만, “이 부분만큼은 GC 자체가 없는 언어로 다시 쓰고 싶다”는 생각이 계속 들었습니다.

그래서 결정했습니다. API 서비스 13개는 Go로, 매칭 엔진 2개(현물·선물)는 Rust로. 레거시 Java 코드는 지우지 않고 backend/ 폴더에 마이그레이션 레퍼런스로 남겨뒀습니다. 새 코드를 짜다가 “원래 로직이 뭐였지?” 싶을 때 바로 대조할 수 있어서, 이 결정이 생각보다 큰 도움이 됐습니다.

Rust 매칭 엔진

핵심은 오더북입니다. 가격-시간 우선순위(price-time priority)를 자료구조로 표현하면 이렇게 됩니다.

  • 매수 호가: BTreeMap<Reverse<Decimal>, VecDeque<Order>> — 높은 가격 우선
  • 매도 호가: BTreeMap<Decimal, VecDeque<Order>> — 낮은 가격 우선
  • 같은 가격 레벨 안에서는 VecDeque로 FIFO

금액은 전부 rust_decimal을 썼습니다. 부동소수점으로 돈 계산을 하면 언젠가 반드시 사고가 납니다.

선물 엔진은 현물보다 훨씬 복잡했습니다. 청산(liquidation), 펀딩비(funding rate), TP/SL, 포지션 관리까지 — 파일 하나하나가 수백 줄짜리 상태 머신입니다. 솔직히 이 부분은 Claude Code가 없었으면 몇 달은 걸렸을 겁니다. 제가 거래소에서 5년 일하며 쌓은 도메인 지식으로 “청산 가격 계산은 이 공식이고, 엣지 케이스는 이런 게 있다”를 정확히 짚어주면, 구현과 테스트는 AI가 빠르게 채워주는 분업이 잘 맞았습니다.

재미있었던 설계 결정 하나

매칭 엔진은 반드시 싱글톤이어야 합니다. 두 개가 동시에 뜨면 체결 순서가 꼬입니다. 보통은 코드에서 분산 락으로 풀지만, 저는 인프라에서 강제했습니다.

Terraform에서 deployment_minimum_healthy_percent = 0으로 설정하면, ECS가 배포할 때 기존 태스크를 먼저 죽이고 새 태스크를 띄웁니다. 순단은 생기지만 두 엔진이 동시에 존재하는 순간이 아예 없습니다. 주문은 Kafka에 durable하게 쌓여 있으니 몇 초의 순단은 문제가 되지 않고요. 코드로 풀 문제와 인프라로 풀 문제를 구분하는 것 — 이게 연차가 주는 감각이 아닐까 싶습니다.

결과

  • Go 서비스 13개 + Rust 엔진 2개, 합쳐서 48,000줄 이상
  • MySQL 테이블 102개, Kafka 체결 파이프라인, STOMP 실시간 시세
  • Terraform으로 AWS ECS Fargate 배포, 메모리 사용량은 Java 대비 체감 1/3 수준
  • 라이브 데모로 한동안 운영하며 검증까지 마쳤습니다 (지금은 AWS 비용 문제로 내렸습니다 — 만들면서 배울 건 다 배웠으니 미련은 없습니다)

“처음부터 새로 만들었다”보다 “레거시를 남겨두고 재플랫폼했다”가 정확한 표현입니다. 그리고 재플랫폼이 훨씬 어렵습니다. 기존 동작을 전부 보존해야 한다는 제약이 있으니까요. 다음 글에서는 이 과정에서 만든 갭 분석 문서와 검증 방법을 다뤄보겠습니다.

Updated: