왜 나는 MCP 서버를 Rust가 아닌 400줄의 순수 C++20으로 작성했는가 ~제작을 결심하기까지의 뒷이야기~
요약
대규모 소스 코드 분석을 위해 Python이나 TypeScript 대신 C++20을 사용하여 고성능 MCP 서버를 구축한 과정을 다룹니다. Tree-Sitter의 잠재력을 극대화하기 위해 GIL과 GC의 오버헤드를 제거하고 메모리 최적화에 집중했습니다.
핵심 포인트
- Python의 GIL과 TypeScript의 GC로 인한 성능 저하 문제 지적
- C++20을 통한 10스레드 병렬 처리 및 네이티브 메모리 제어 구현
- 대규모 리포지토리 분석 시 OOM 방지를 위한 메모리 레이아웃 최적화
- Tree-Sitter의 성능을 100% 끌어올리기 위한 저수준 구현의 중요성
서론: C++20으로 Tree-Sitter와 MCP를 결정했을 때의 일
Zenn이나 해외 기술 커뮤니티에서 "Tree-Sitter를 사용하여 소스 코드를 AST(추상 구문 트리)로 분석하고, MCP(Model Context Protocol) 서버를 통해 AI 에이전트에게 전달한다"는 접근 방식을 자주 접했습니다.
하지만 그 구현 기사의 대부분은 **"Python 래퍼 SDK"**를 경유하거나, **"TypeScript(WASM 버전 등)의 얇은 라이브러리"**를 관례적으로 호출하고 있는 현 상황이 있습니다.
분명히 말씀드리겠습니다. LLVM급의 거대한 코드베이스(수백만 줄)를 파싱하여 AI에게 먹이는 인프라에서, 그런 오버헤드투성이인 스크립트 언어 레이어를 끼워 넣는 것은 제정신이 아닙니다.
Tree-Sitter는 본래 순수한 C 언어로 작성된 초고속·초경량 인크리멘털 파서(Incremental Parser)입니다. 그런데도 세상의 개발자들은 편해지기 위해 그 주변에 굳이 무거운 SDK라는 "군더더기"를 몇 겹이나 둘러서 사용하고 있습니다. 그리고 "무겁다, 속도가 안 나온다, 거대 소스에서 작동하지 않는다"라고 고민하는 것은 본말전도입니다.
저는 이러한 미온적인 현상에 대한 안티테제로서, 불필요한 추상화나 과도한 클래스 은닉을 일절 거부하고, 순수한 C++20의 10스레드 병렬 처리를 통해 Tree-Sitter의 잠재력을 100% 끌어내는 폭속(爆速) MCP 코어를 구축하고자 직접 제작해 보기로 했습니다.
🛑 왜 Python/TypeScript를 통한 분석은 거대 소스에서 파탄 나는가?
Tree-Sitter 자체의 코어가 아무리 빠르더라도, 그것을 래핑하는 언어의 "군더더기"가 모든 것을 망쳐놓습니다. 대규모 리포지토리를 분석할 때, 개발자는 다음과 같은 세 가지 절망에 직면합니다.
Python의 GIL과 PyObject의 한계
멀티스레드로 병렬 파싱을 시도하려 해도, GIL(Global Interpreter Lock) 때문에 1개 코어밖에 사용할 수 없어 속도가 나지 않습니다. 게다가 대량의 AST 노드를 Python의 세계로 가져오는 순간, 모든 것이 무거운 **PyObject**로서 힙(Heap)에 할당되어 메모리를 순식간에 잡아먹습니다.
V8 엔진과 GC(Garbage Collection)의 함정
TypeScript(Node.js / Bun)로 구동할 경우, 수백만 규모의 노드를 파싱한 직후에 **GC(Garbage Collection)**의 Stop-the-world(일시 정지)가 발생하여 시스템 전체가 프리즈(Freeze)됩니다. AI 에이전트가 실시간으로 응답해야 하는 인프라에서 이러한 지연은 치명적입니다.
"C++20"이라면, 이 모든 것이 존재하지 않는다
GC의 군더더기도, 인터프리터의 락(Lock)도 일절 존재하지 않습니다. 10개의 스레드가 네이티브하게, 다이렉트로 메모리를 타격하기 때문에 처리를 한계까지 압축하는 것이 가능합니다.
🛠️ "C++라면 멀티스레드를 할 수 있다" —— 하지만 그것만으로는 거대 소스에 의해 KILL 당한다
Python의 GIL에 얽매이지 않는 C++20이라면, 하드웨어의 한계까지 CPU를 다 쓰는 진정한 멀티스레드(10스레드 병렬 등)를 구현하는 것은 간단합니다.
하지만 여기서 새로운 벽에 부딪힙니다.
대상이 LLVM과 같은 초거대 리포지토리인 경우, 아무리 C++의 네이티브 스레드로 질주하더라도, **"파싱한 AST(추상 구문 트리)를 모두 무식하게 메모리에 올리면, OS의 OOM Killer에 의해 순식간에 프로세스가 KILL(강제 종료)된다"**는 현실입니다.
그렇기에 mcp-cst-core에서는 메모리를 최소한으로 억제하기 위한 철저한 공력을 기울였습니다.
메모리 레이아웃 최적화: 불필요한 객체 생성이나 포인터 남발을 배제하고, 필요한 컨텍스트(CST)만을 가상 공간(Virtual space)을 격렬하게 순회하며 효율적으로 처리.
물리 메모리(RSS)를 901MB로 억제하려는 집념: 10스레드의 폭력으로 LLVM을 57초 만에 완전 파싱한다는 초가혹한 실행 열량(Execution heat)을 받아내면서도, 물리 메모리는 단 901MB라는 경이로운 컴팩트함을 유지.
🦀 왜, 지금 굳이 "Rust"를 사용하지 않았는가
🦀 왜, 지금 굳이 "Rust"를 사용하지 않았는가
성능을 추구하는 시스템 프로그래밍 (System Programming)에 있어, 현대라면 C++ 대신 Rust를 선택하는 것이 트렌드일지도 모릅니다. Rust는 매우 깊이 있게 설계된 훌륭한 언어이며, 컴파일 시점에 메모리 안전성 (Memory Safety)을 100% 보장하는 설계는 압도적인 정의입니다. 극한의 처리를 수행하기 위해 안전장치를 해제하는 unsafe 영역이 마련되어 있다는 점도 충분히 이해하고 있습니다.
하지만 이번 "궁극의 단일 기능 인프라"를 구축함에 있어, 저는 명확한 이유를 가지고 Rust를 "불채택"했습니다.
크레이트 (Crate/Cargo) 의존성 비대화라는 군더더기
Rust의 생태계는 편리하지만, 단일 기능의 초고속 코어를 만들고 싶을 때조차 의존성이 줄줄이 사탕처럼 늘어나 바이너리나 구성이 비대화(군더더기가 붙음)되는 경향이 있습니다.
만인을 위한 범용 SDK의 오버헤드 (Overhead) 회피
Rust의 Tree-Sitter 바인딩 (Binding)도 훌륭한 결과물이지만, 만인을 위해 안전하게 만들어졌기 때문에, 이번 "main 직격·STDIO 루프"와 같은 극한의 아키텍처 (Architecture)에組み込む에는 아무래도 오버헤드나 제약이 발생합니다.
C 언어 기반의 Tree-Sitter 코어를 가장 순수하게, 그리고 10개 스레드 병렬로 다이렉트하게 구동하기 위해, 저는 감히 익숙한 C++20이라는 야생의 폭력을 선택했습니다.
🏛️ 「도관 (MCP)」에 과도한 클래스 은닉은 불필요
아키텍처 사상으로서도 본 리포지토리 (Repository)는 철저하게 낭비를 걷어내고 있습니다.
통신의 창구(도관)에 불과한 MCP 계층에, 과도한 클래스 은닉이나 객체 생성의 추상화 (Abstraction)는 일절 불필요합니다.
main() 이 STDIO 루프를 스트레이트로 움켜쥔다.
이 강직한 설계 덕분에 오버헤드는 제로가 되었으며, 한계 속도에서의 패킷 통신과 파싱 (Parsing)의 폭주가 실현되었습니다. 현재 표준 출력 (STDIO) 기반의 파싱 통신 코어는 완벽하게, 그리고 실증된 상태로 동작하고 있습니다.
🔗 마치며
이 "낭비를 극한까지 걷어낸 코어"가 얼마나 폭력적인 속도를 뽑아낼지, 제 PC는 AI 학습용으로 샀던 59,800엔짜리 PC (GMKTEC M7)이기에, 여러분의 훌륭한 PC에서 실행한다면 어느 정도의 속도가 나올지 궁금합니다.
본 프로젝트는 **MIT 라이선스 (MIT License)**로 100% 오픈하게 완전 공개하고 있습니다.
자유롭게 즐겨주시면 감사하겠습니다.
Discussion
gitHub
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기