Go와 Rust, 코빗은 이렇게 나눠 씁니다
들어가며
가상자산 거래소는 일반적인 웹 서비스와는 여러 면에서 다릅니다. 하루에도 수천만 건이 넘는 주문이 입력될 수 있고, 이용자 자산이 달린 만큼 데이터 정합성이 어긋나는 일이 일어나서도 안되고, 그렇다고 처리가 느려서도 안됩니다. 또한, 증시처럼 폐장이 있는 것도 아니기에 연중무휴 24시간 안정적으로 운영되어야 하고 그러면서도 대내외적으로 들어오는 요구사항들을 반영해 새로운 기능을 지속적으로 출시해야 합니다.
그런데 코빗의 과거 거래 시스템은 오랜 기간 기술 부채가 누적된 탓에 이런저런 문제를 안고 있었고, 심지어 체결 엔진(Matching Engine)에서조차 간헐적으로 장애가 발생한 적도 있었습니다. 이런 배경에서 저는 코빗의 CTO로 선임된 이후, 주문 입력에서부터 체결 엔진, 시세 정보 및 잔고 처리에 이르기까지의 거래 시스템 전체를 완전히 새롭게 만들기로 결정했고, 2023년 봄부터 차세대 거래 시스템 개발에 착수해 약 1년 반이 지난 2024년 6월에 비로소 코빗의 새로운 거래 시스템을 출시할 수 있었습니다.
코빗의 새로운 거래 시스템에서 특기할 만한 점은 고(Go)와 러스트(Rust)를 적극적으로 사용했다는 점입니다. 물론 자바(Java)나 코틀린(Kotlin)으로 만들어진 서비스도 존재하지만, 주문 서비스나 Open API 서비스는 Go, 체결 엔진과 시세 정보 서비스는 Rust로 개발되었기에 거래소의 핵심 시스템 중 매매와 시세에 관한 부분은 Go와 Rust만으로 만들어진 셈입니다.
이 글은 코빗 기술 블로그의 첫 글인 만큼, 코빗의 신규 거래 시스템 개발이라는 기술적 성과에 대해 이야기하면서도 조금 가벼운 주제로, 귀여운 설치류와 갑각류 그림의 마스코트로도1 잘 알려진 Go와 Rust를 코빗에서는 왜 선택했는지, 그리고 어떻게 적재적소에 나누어 쓰는지를 공유하고자 합니다. (코빗의 신규 거래 시스템 아키텍처에 대해서는 다른 글에서 이야기하려고 합니다.)
Rust: 핵심 엔진의 든든한 토대
새롭게 거래 시스템을 만들기로 한 이상, 최고 수준의 성능과 절대적인 안정성을 달성하는 것을 목표로 삼았습니다. 전체적인 시스템 아키텍처를 잘 만드는 것은 당연하고 특히 기술적으로 강력한 체결 엔진을 개발하고자 했습니다. 그러자면 당연히 메모리에서 호가, 즉 오더북을(In-memory Orderbook) 구현해야 했기에 Rust는 가장 먼저 떠오른 선택지였습니다.
또, 디스코드(Discord)가 Go에서 Rust로 전환한 사례도 아주 높은 부하를 고려한 환경에서 낮은 지연 시간과 높은 처리 성능을 다 갖춘 시스템을 목표로 하는 데 참고가 되었습니다.
Rust는 가비지 컬렉션(Garbage Collection)이 없어 그에 따른 'Stop-the-world' 현상도 없기에 예측 가능한 지연 시간을 달성할 수 있습니다. 따라서 주문이 폭주하더라도 일관된 속도를 기대할 수 있습니다. 또한, 비용 없는 추상화(Zero-Cost Abstraction) 덕분에 개발자는 가벼운 코드를 작성하면서도 런타임에 추가 오버헤드 없이 높은 성능을 얻을 수 있습니다. 게다가 전 세계 개발자들에게서 호평을 받고 있는 cargo 생태계는 개발 생산성을 더욱 높여 줍니다.
무엇보다도, 개발자 입장에서는 "컴파일이 된다면 안전하게 작동한다"고 기대할 수 있기에 (물론 모든 안전을 말하는 것은 아니지만) 마음이 든든한 것도 빠트릴 수 없습니다. 특유의 소유권(Ownership) 개념 덕분에 컴파일 시점에 메모리 관련 버그를 원천적으로 차단함으로써 메모리 안전성이 보장되기 때문입니다.
저희는 체결 엔진과 시세 정보 서비스를 Rust로 만들었는데, 이런 시스템들은 작동 방식이 명확합니다. 즉, 체결 엔진은 오더북을 구현해 매수, 매도 주문을 처리하고, 시세 정보 서비스는 체결 이벤트를 바탕으로 시세 정보를 구축하고 이용자에게 제공하는, 말 그대로 변하지 않는 원칙을 다루는 기본적인 엔진 역할을 합니다. 그러면서도 메모리에서 대량의 정보를 처리할 것을 요구할 정도로 강력한 성능을 필요로 했기에 Rust는 (상대적으로 생산성이 낮더라도) 딱 맞는 선택이었습니다.
덕분에 저희는 목표했던 성능을 달성할 수 있었습니다. 체결 엔진은 In-memory에서의 주문 체결 성능을 평가한 벤치마크에서 1초에 400만 건 이상의 체결을 기록한 바 있고2, 실제 서비스에서는 외부 연계가 더해지기에 이 수치를 전체 시스템 성능과 동일하게 볼 수는 없지만 그만큼 목표했던 성능은 달성했다고 보기에 충분했습니다.3 시세 정보 서비스에서도 실시간으로 지연 없이 시세를 제공하기 위해 Lock-Free Ring Buffer를 사용함으로써 Redis에 캐시된 값을 사용하던 이전 시스템보다 더욱 빠르게 호가(오더북) 정보나 최근 체결, 현재가 등의 시세 정보를 이용자에게 제공할 수 있게 되었습니다.
저희도 처음부터 Rust가 익숙한 것은 아니었습니다. Rust는 분명히 진입 장벽이 있고 학습 곡선도 만만하지는 않습니다. 저희도 처음 Rust를 시작할 때에는 unsafe 코드를 작성하기도 했고요. 하지만, 그런 과정들을 지난 후 저희는 Rust로 한 번 잘 만든 강건한(robust) 엔진은 오랫동안 신뢰를 가지고 운영할 수 있다는 결론을 내릴 수 있었습니다.
Go: 빠르고 효율적인 서비스 개발
Go는 특유의 단순한 문법으로 잘 알려져 있습니다. 덕분에 개발의 생산성과 유지보수의 편의성이 높습니다. 명확하고 가독성 좋은 코드를 작성할 수 있으면서도 고루틴(Goroutine)과 채널(Channel)을 통한 뛰어난 동시성 처리와 강력한 성능까지 보여줍니다.
비용 효율성 또한 빼놓을 수 없는 장점입니다. 상대적으로 적은 메모리를 사용하면서 컨테이너 환경에서 안정적으로 작동하기 때문에 클라우드 컴퓨팅 환경에서는 (저희는 모든 서비스가 AWS EKS 위에서 운영되고 있습니다.) 직접적인 비용 절감으로 이어집니다. 실제로 저희는 신규 시스템으로의 마이그레이션을 위해 레거시 웹소켓 서버와 동일한 사양의 서버를 Go로 새롭게 구현했는데, 월간 AWS 비용 지출이 크게는 약 80% 감소했습니다. 물론 이렇게 극적으로 비용이 줄어든 것은 기존 레거시 시스템의 최적화가 그만큼 부족했던 탓이 크지만, Go의 효율성이 개발 측면 뿐만 아니라 비용 절감까지도 이어진다는 것을 보여준 사례라고 생각합니다.
Go의 또다른 강점으로는 컴파일 시간도 아주 빠르고 자동으로 가비지 컬렉션을 해 주는 만큼 개발자 경험 측면에서 비즈니스 로직 구현에 집중할 수 있다는 점입니다. 또한, 표준 라이브러리(Standard Library)만으로도 어지간한 것을 다 만들 수 있어 외부 의존성도 낮출 수 있습니다. gopls라는 강력한 공식 언어 서버도 제공되어 자동 완성이나 정적 분석 등을 지원하고, 빌드에서부터 테스트, 벤치마크까지 필요한 도구들도 Go에 내장되어 있어 편리합니다. 물론 Go에도 호불호가 갈리거나 단점으로 꼽을 만한 것들이 있기는 합니다만 장점이 훨씬 크다고 생각했습니다.
그래서 코빗의 신규 거래 시스템에서는 주문 서비스와 Open API 서비스를 Go로 만들었습니다. 물론 신규 거래 시스템 외에도, 오토 트레이딩을 비롯한 여러 서비스에서 이미 Go를 사용하고 있었기에 익숙했다는 점도 있습니다. 결론적으로, 비즈니스 로직이 구현되는 애플리케이션 계층은 개발 속도와 유연성이 중요한데, Go는 이러한 요구와 성능까지 모두 만족시킬 수 있는 훌륭한 선택이었습니다.
적재적소에 맞게 활용하기
저희가 얻은 결론은 명확합니다. 두 언어를 놓고 말하자면, Rust로 핵심 엔진을 단단히 다지고, Go로 서비스 개발 속도를 높이자는 것입니다.
그래서 빠르게 개발이 필요하고 자주 변경될 수 있는 비즈니스 로직은 Go로 구현했습니다. Go는 높은 성능을 보여주면서도 개발의 효율성과 비용의 효율성을 모두 잡을 수 있었고 한 번 잘 만들어 두면 오랜 기간 안정적으로 계속 작동해야 하는 핵심 엔진은 Rust로 만들었습니다. 즉, 코빗에서는 상술했듯 주문 서비스나 Open API는 Go로, 체결 엔진이나 시세 처리 시스템은 Rust로 만들었습니다.
이 두 언어는 종종 보이는 "Go vs Rust" 라는 말처럼 경쟁 관계로 볼 것이 아니라 서로 다른 목표를 가진 언어로 보아야 한다고 생각합니다. 참고로 코빗에서는 이들 시스템 간 통신에 gRPC를 사용하거나 Kafka에 Protocol Buffers 메시지를 실어 사용하고 있습니다.
생성형 AI 시대의 개발 경험
생성형 AI와 함께 개발을 하는 시대가 되면서, Go와 Rust를 사용하는 경험도 변화가 있었습니다. Claude Code 같은 코딩 에이전트(Coding Agent)를 사용하면, 막히는 부분이 있으면 알아서 분석 도구를 사용하거나 pkg.go.dev에 접속해 필요한 내용을 찾아 바로 고쳐주기까지 합니다.
특히 Rust의 경우에는 예전보다는 많이 개선되었지만, 특유의 소유권이나 수명(Lifetime) 개념 때문인지 여전히 생성형 AI가 컴파일되지 않는 코드를 생성하는 경우가 종종 있는데 마찬가지로 코딩 에이전트가 자동으로 에러 메시지를 보고 문제를 해결해 줍니다.
어느 프로그래밍 언어나 마찬가지겠지만 생성형 AI 덕분에 Rust의 진입 장벽이 낮춰졌을 뿐만 아니라 개발의 편의성도 크게 높아졌다고 생각합니다.
마치며
코빗은 두 언어의 장점을 최대한 살려 안정적이면서도 유연한 거래 시스템을 구축했습니다. Rust로 만든 견고한 엔진 위에, Go로 유연한 비즈니스 로직을 구현하는 아키텍처는 빠르게 변화하는 시장에서 경쟁력을 유지할 수 있게 해주는 핵심적인 열쇠입니다. 이것은 저희의 시스템 목표에 맞춘 실용적인 엔지니어링 원칙이라고도 말할 수 있습니다.
즉, 코빗은 Rust의 견고함으로 시스템 신뢰성이라는 기반을 다지고, Go의 생산성으로 비즈니스의 변화에 빠르게 대응합니다. 이 두 언어의 장점을 극대화하고 단점을 상호 보완하는 실용적인 방식이 바로 저희가 택한 기술적 접근 방식입니다. 앞으로도 저희는 특정 기술에 얽매이기보다는 문제의 본질과 달성 목표에 맞는 최적의 기술을 선택하고 거래 시스템을 발전시켜 나가겠습니다.
- 1.
Go의 마스코트인 Gopher는 Renée French가, Rust의 마스코트인 Ferris는 Karen Rustad Tölva가 만들었습니다. (저작권자 표시)
- 2.
머신 성능이나 테스트 시나리오 등에 따른 차이가 있습니다. 심지어 개발 도중 벤치마크에서는 초당 1천만 건의 체결 기록도 달성한 바 있습니다만, 실제 거래 환경과는 거리가 있는 테스트였습니다.
- 3.
체결 엔진에서 아무리 고성능을 달성한다고 한들 청산결제가 동시에 이루어져야 하는 가상자산 거래소의 특성상 체결 엔진만의 성능은 결국 실제 이용자 경험과는 다른 하나의 벤치마크 지표에 불과할 것입니다. 그래서 코빗에서는 한 개의 종목을 가정하고 카프카(Apache Kafka)에 주문 메시지가 입력된 후부터 체결 엔진을 거쳐 다시 카프카로 체결 메시지가 출력될 때까지의 시간을 토대로 1초에 42,000건 이상의 거래 체결을 지원한다고 밝힌 바 있습니다.
