코딩 에이전트가 리포지토리를 grep하며 토큰을 낭비하고 있다면: 한 줄의 명령어로 해결하는 방법
요약
코딩 에이전트가 리포지토리를 탐색할 때 발생하는 과도한 토큰 소비 문제를 해결하기 위해 MCP 서버인 graphlens를 소개합니다. Tree-sitter와 타입 인식 리졸버를 사용하여 코드의 구조화된 그래프를 제공함으로써, 에이전트가 효율적이고 정밀하게 코드베이스를 이해하도록 돕습니다.
핵심 포인트
- 에이전트의 단순 grep 방식은 막대한 토큰 비용과 컨텍스트 낭비를 초래함
- graphlens는 MCP를 통해 코드의 타입이 지정된 그래프(typed graph)를 제공함
- Tree-sitter와 타입 인식 리졸버를 결합하여 정확한 코드 관계를 파악함
- Claude Code, Cursor 등 호환되는 클라이언트에서 즉시 사용 가능함
graphlens-mcp는 Claude Code, Cursor 및 호환되는 클라이언트에게 코드의 타입이 지정된 그래프(typed graph)를 제공합니다. 이를 통해 에이전트는 "create_order를 호출하는 곳이 어디인가요?"라고 질문하고, 코드베이스의 절반을 읽는 대신 하나의 작은 답변을 얻을 수 있습니다. 아래에서는 엔진의 작동 방식, 936회 실행 벤치마크를 통해 확인한 실제 효용성, 그리고 5분 설치법을 다룹니다.
저는 graphlens를 공개적으로 구축해 왔습니다. 전체 이야기는 Habr의 세 개 포스트(엔진, 벤치마크, 제품)에 담겨 있지만, 이 버전은 그 자체로 완결성을 갖추고 있으며 중요한 내용들을 통합했습니다. 링크는 마지막에 있습니다.
모두가 알고 있는 루프
대규모 프로젝트를 상상해 보세요. 수십만 줄의 코드, 백엔드는 Python, 프론트엔드는 TypeScript, 그리고 아무도 건드리고 싶어 하지 않는 레거시(legacy) 영역이 있는 프로젝트입니다. 당신은 코딩 에이전트(coding agent)를 가리키며 평범한 질문을 던집니다: "여기서 인증(auth)은 어떻게 작동하나요?" 또는 "이 메서드의 시그니처(signature)를 변경하면 무엇이 깨지나요?"
에이전트는 전체 리포지토리(repo)를 한 번에 볼 수 없습니다. 그래서 에이전트는 할 수 있는 유일한 일을 합니다: 이름을 grep하고, 파일을 열고, 읽고, 임포트(import)를 따라가고, 다시 grep하는 것입니다. 에이전트는 수십 개의 파일을 읽고, 그 파일들 각각이 컨텍스트 윈도우(context window)에 담기며, 다음 턴마다 매번 비용이 다시 청구됩니다.
이는 가설적인 오버헤드가 아닙니다. Anthropic의 엔지니어링 블로그에서도 도구 정의(tool definitions)와 중간 결과물들이 "에이전트가 요청을 읽기도 전에 50,000개 이상의 토큰을 소비할 수 있다"고 언급했습니다. 즉, 에이전트가 당신의 질문에 답변을 시작하기도 전에 윈도우가 가득 차 버리는 것입니다.
코드 그래프(code graph)는 바로 이 문제를 해결합니다. "파일을 읽고 눈으로 확인하는" 대신, 에이전트는 "create_order를 호출하는 곳이 어디인가요?"와 같은 정밀한 질문을 던지고, 텍스트 검색과 요행을 바라는 대신 해결된 엣지(resolved edges)라는 작고 구조화된 답변을 돌려받습니다.
이것이 핵심 제안입니다. 이 포스트의 나머지 내용은 이 제안이 실제로 유효한지, 그리고 이를 사용 가능하게 만들기 위해 무엇이 필요했는지를 다룹니다.
엔진이 실제로 하는 일
"코드 → 그래프" 과정에서 어려운 점은 상자를 그리는 것이 아닙니다. 바로 엣지(edges)입니다. 대부분의 경량 도구들은 이름으로 참조를 해결합니다. 즉, save() 호출을 보면 save라는 이름을 가진 모든 곳으로 엣지를 연결합니다. 빠르긴 하지만 틀린 방식입니다. 실제 코드베이스에는 수십 개의 save가 존재하기 때문입니다.
MCP 서버의 엔진인 graphlens는 작업을 두 단계로 나눕니다:
- Tree-sitter가 각 파일을 구체 구문 트리(Concrete Syntax Tree, CST)로 파싱합니다: 정확한 구조와 1부터 시작하는 정밀한 스팬(span) 위치를 제공합니다. 모든 사용 지점(use-site)은 호출(call), 읽기(read), 쓰기(write), 어노테이션(annotation), 기본 클래스(base class)와 같은 역할을 가진 하나의 '발생(occurrence)'으로 기록됩니다.
- 언어별로 특화된 **타입 인식 리졸버(type-aware resolver)**가 각 발생 지점에 대해
definition_at(file, line, col)질문에 답합니다. 리졸빙된 정의는 실제 선언(declaration)으로 향하는 실제 엣지(edge)가 됩니다.
이 리졸버들은 여러분의 IDE가 실행하는 것과 동일한 메커니즘입니다: Python의 경우 Astral의 Rust 타입 체커인 ty, TS의 경우 TypeScript 컴파일러 API, Go의 경우 gopls, Rust의 경우 rust-analyzer를 사용합니다. 따라서 CALLS 엣지는 실제 함수를 가리키고, HAS_TYPE은 실제 클래스를, INHERITS_FROM은 실제 기본 클래스를 가리킵니다. 이는 "아마 관련이 있을 것"과 "관련이 있음"의 차이입니다. 엔진은 services.py에 있는 process_order가 tests/에 있는 이름이 같은 함수가 아니라, api.py에서 호출되는 바로 그 함수라는 것을 알고 있습니다.
이것으로 첫 번째 장벽인 이름의 모호성(name ambiguity) 문제를 해결합니다. 두 번째 장벽은 대부분의 코드 인텔리전스(code-intel) 도구들이 단일 언어(monolingual)라는 점입니다. 이들은 Python은 아주 잘 이해하지만, TypeScript 프론트엔드가 FastAPI 라우트를 호출하는 순간 눈이 멀어버립니다. 실제 시스템은 다중 언어(polyglot) 환경이지만, 그 주변의 도구들은 대개 그렇지 못합니다.
graphlens는 서비스가 노출하거나 소비하는 인터페이스(HTTP 라우트, 큐 토픽, gRPC 메서드 등)에 대해 언어 중립적인 BOUNDARY 노드를 생성합니다. 경계(boundary) ID에는 프로젝트나 언어 정보가 포함되지 않으며, HTTP 경로는 정규화되어 /users/1, /users/{user_id} (FastAPI), <int:id> (Flask), :id (Express)가 모두 동일한 키로 수렴합니다. 따라서 FastAPI 라우트와 해당 엔드포인트로 향하는 TypeScript fetch는 동일한 경계 ID를 생성합니다. 두 그래프를 병합하고 연결하면 언어의 경계를 넘나드는 엣지를 얻을 수 있습니다. 이를 통해 에이전트는 "어떤 프론트엔드 호출이 이 엔드포인트를 타격하는가?"라는 질문에 답할 수 있게 됩니다. 이는 단일 언어 도구라면 질문 자체를 구성조차 할 수 없는 문제입니다.
신뢰를 위해 중요한 두 가지 선택지가 더 있습니다. ID는 결정론적(deterministic)입니다. 노드의 ID는 project::kind::qualified_name의 SHA-256 해시값이므로, 동일한 스캔을 수행하면 어떤 머신에서든 동일한 ID가 생성됩니다. 이것이 차이점 비교(diffing)와 증분 업데이트(incremental updates)를 가능하게 하는 핵심입니다. 또한 그래프는 불완전한 상태에 대해 결코 거짓말을 하지 않습니다. 툴체인(toolchain)이 누락되었거나 파일의 타입 체크(type-checking)가 실패하면, 리졸버(resolver)는 절반만 해결된 그래프를 조용히 반환하는 대신 상태(ok / degraded / unavailable)를 기록합니다. CI(지속적 통합) 환경에서는 ok가 아닌 모든 상태는 빌드 실패로 처리됩니다.
실제로 효과가 있을까? 936회의 실행
여기에 대부분의 "내 도구가 더 빠르다"라는 게시물들이 생략하는 부분이 있습니다. 저는 첫 번째 글에서 에이전트가 grep을 수행하며 토큰을 낭비한다고 주장했지만, 그에 대한 어떠한 수치적 근거도 제시하지 않았습니다. 그래서 이를 확인하기 위해 벤치마크를 구축했고, 그 결과는 저를 놀라게 했습니다.
설정은 하나의 통제 변수만을 사용합니다. 동일한 에이전트(Claude Code), 동일한 프롬프트(prompts), 동일한 작업(tasks). 유일하게 변하는 것은 어떤 MCP 서버가 에이전트에게 컨텍스트(context)를 제공하느냐입니다. 네 가지 "손"은 다음과 같습니다: filesystem (grep + read), graphlens (구조적 그래프), serena (LSP), 그리고 codegraph (경쟁 관계의 그래프 도구). 세 가지 모델(Haiku, Sonnet, Opus), 세 가지 시드(seeds), apache/superset (~40만 라인, Python + TS)에 대한 26가지 작업. 총 936회의 실행이 이루어졌습니다.
수치가 의미를 갖도록 몇 가지 사항을 고정했습니다. 내장된 Claude Code 도구들(Read, Grep, Bash)은 비활성화되었습니다. 그렇지 않으면 에이전트가 MCP 서버를 무시하게 되어 테스트가 아무것도 측정할 수 없기 때문입니다. 참조 정답(Reference answers)은 고정된 태그를 기준으로 수동 검증되었으며, 결정적으로 테스트 대상인 어떤 도구에 의해서도 생성되지 않았습니다. temperature=0 설정이 모델을 결정론적(deterministic)으로 만들지는 않으므로, 세 개의 시드를 사용하였고 평균(mean)이 아닌 중앙값(median)을 보고합니다. 답변을 내놓지 못한 채 턴 제한(turn ceiling)에 도달한 실행은 정확도 0으로 간주합니다. 즉, "데이터가 없음"이 아니라 "도구가 예산 내에 완료하지 못함"으로 처리합니다.
핵심 결과: 작업의 종류에 따라 순위가 뒤바뀝니다.
단순한 지점 조회(point lookups) — "클래스 X가 어디에 정의되어 있는가", "무엇을 상속받는가" — 의 경우, 네 가지 도구 모두 정확도 면에서 동일합니다. 유일한 차이점은 가격으로, 약 3배의 차이가 나며 graphlens는 중간 지점에서 눈에 띄지 않는 수준입니다. 만약 제가 이것들만 측정했다면, "그래프는 가치가 없으며 grep이면 충분하다"라고 썼을 것입니다. 그것은 절반의 진실일 뿐이었을 것입니다.
실제로 중요한 작업 — 영향 범위(blast-radius) 질문, 모든 오버라이드(override) 찾기, 모호한 이름 해결하기 — 에 있어서는 도구들의 성능이 극명하게 갈립니다:
| 도구 | 정확도 | 토큰 | 도구 호출 (Tool calls) | 작업당 비용 ($/task) |
|---|---|---|---|---|
| filesystem (grep) | 0.71 | 12,596 | 27 | $0.424 |
| ... |
grep은 무너집니다. 정확도가 가장 낮으며, 실행의 83%에서만 정답에 도달합니다. 나머지 실행은 50회 호출 제한(50-turn ceiling)을 소진하며 실패합니다. 간신히 완료된 실행들은 비용이 1023배 더 많이 들고 시간도 1018배 더 오래 걸립니다. 질문이 "이것에 대한 모든 호출" 또는 "동일한 이름을 가진 10개의 메서드 중 어느 것인가"일 때, 텍스트 검색은 노이즈에 파묻혀 버립니다.
단순 조회에서는 지루해 보였던 동일한 graphlens가 이제는 가장 저렴하고($0.018) 빠른 옵션이 되어, 27번의 호출 대신 단 한 번의 도구 호출로 답을 내놓습니다. 이는 이러한 작업에서 grep 대비 토큰을 약 94% 절감한 수치입니다. codegraph가 가장 정확하며(0.93), serena도 제 역할을 다합니다.
제가 예측하지 못한 두 번째 반전이 있습니다. 최적의 도구는 어떤 모델(model)을 실행하느냐에 따라 달라진다는 점입니다. graphlens는 그래프 이웃(graph neighborhoods), 참조 목록 등 토큰 소모가 많은 결과를 반환합니다. 저렴한 모델에서는 이러한 장황함(verbosity)이 거의 비용이 들지 않으므로, Haiku에서는 graphlens가 네 가지 중 가장 저렴합니다. 반면, 동일한 토큰 가격을 훨씬 높게 책정하는 Opus에서는 graphlens가 구조적 도구 중 가장 비싸집니다(그래도 grep보다는 저렴합니다). serena와 codegraph는 간결하고 명확한 결과를 반환하므로 어떤 모델에서도 저렴함을 유지합니다.
결론적으로 제가 돈을 걸 수 있을 만큼 확신하는 단 하나의 교훈은 다음과 같습니다: 구조적 도구를 사용하는 저렴한 모델이 grep을 사용하는 비싼 모델보다 낫습니다. codegraph + Haiku (~$0.023, 0.99 정확도)는 모든 지표에서 filesystem + Opus ($0.087, 0.93)를 동시에 압도합니다.
저의 예측 중 하나가 완전히 틀렸으며, 이는 보고할 가치가 있습니다. 저는 단일 언어 도구들이 실패할 것이라 확신하며 스트레스 테스트(stress test)의 일환으로 교차 언어 작업(cross-language tasks, 즉 /api/v1/... 경계를 넘어 TS 호출이 Python 핸들러로 연결되는 작업)을 배치했습니다. 하지만 실패하지 않았습니다. grep을 포함한 모든 수단이 두 가지 모두를 해결했습니다. 에이전트는 어떤 컨텍스트(context)가 제공되든 스스로 경계를 넘어 이동합니다. 당신이 바랐던 것만을 확인해 주는 벤치마크(benchmark)는 벤치마크가 아닙니다.
솔직한 세부 사항을 말씀드리자면: 하나의 리포지토리(repo), 하나의 하네스(harness), 26개의 작업(단순 작업 20개, 어려운 작업 6개)을 대상으로 했습니다. 비용 차이는 통계적으로 확실하며, 어려운 작업에서의 정확도 격차는 강력한 신호이지만 n=6에서는 증명된 것이 아닙니다. cost_usd는 API 환산 비용이며, 귀하의 구독 요금이 아닙니다. 이것은 하나의 사례에 대한 재현 가능한 측정치이며 보편적인 순위가 아닙니다. 또한, 여러분이 자신의 코드에 직접 실행해 보고 싶다면 전체 하네스와 원시 데이터(raw data)를 공개해 두었습니다.
아무도 언급하지 않는 격차: 엔진은 제품이 아니다
따라서 엔진은 그래프(graph)가 작동해야 하는 작업들에서 잘 작동합니다. 하지만 제가 두 번의 Habr 포스트 모두에서 간과했던 구멍이 있습니다. 엔진은 에이전트에게 있는 그대로 건네줄 수 있는 것이 아니라는 점입니다.
graphlens는 설계상 그래프를 생성하는 단계에서 멈춥니다. 데이터베이스를 소유하지 않고, 파일 시스템(filesystem)을 감시하지 않으며, 스스로 재인덱싱(reindex)하지 않고, 장기 실행 서비스(long-running service)를 띄우지도 않습니다. 엔진으로서는 올바른 결정입니다. 작은 코어(core)는 테스트, 캐싱(cache), 조합(compose)하기가 매우 쉽기 때문입니다. 하지만 이를 실제로 에이전트에 연결하려면, 누군가는 그 위에 레이어(layer)를 작성해야 합니다: 그래프 저장소(graph storage), 무효화(invalidation, 파일이 변경될 때 어떤 파일을 재인덱싱할지), 파일 시스템 와처(filesystem watcher), 에이전트가 호출할 수 있는 도구들을 갖춘 MCP 서버, 각 클라이언트의 설정 형식에 따른 등록, 그리고 에이전트가 이 모든 것을 어떻게 사용하는지 알 수 있게 하는 탐색 기술(navigation skill) 등이 필요합니다.
그 레이어는 결국 모든 사람이 수동으로 다시 작성하게 되는 작업입니다. 저는 이를 한 번 작성하여 graphlens-mcp로 패키징했습니다. 이는 엔진 위의 얇은 런타임(runtime)으로, 저장소, 최신성 모델(freshness model), 그리고 에이전트가 보는 모든 것을 관리합니다.
한 줄의 명령어
uv tool install graphlens-mcp # 또는: pipx install graphlens-mcp
cd your-project && graphlens-mcp init
init 명령어는 프로젝트의 언어를 감지하고, 툴체인(toolchain) "doctor"를 실행하며, 코드를 로컬 그래프(local graph)로 인덱싱합니다. 또한 에이전트의 설정 파일에 MCP 서버를 작성합니다(Claude Code, Cursor, Windsurf, VS Code/Copilot, Codex CLI를 인식하며, 기존의 다른 서버를 덮어쓰지 않고 멱등성(idempotently) 있게 작성합니다). 마지막으로 내비게이션 기술(navigation skill)을 설치합니다. 에이전트는 해당 설정으로부터 서버를 직접 시작하므로, 사용자가 직접 serve를 실행할 필요가 없습니다. 에이전트를 재시작한 뒤 "create_order의 시그니처(signature)를 변경하면 무엇이 깨지나요?"와 같은 질문을 던져보세요.
요구 사항: Python ≥ 3.13 (엔진으로부터 상속됨). MIT 라이선스. 현재 버전 0.1.2이며, 초기 단계입니다 — 이에 대한 자세한 내용은 아래에서 다룹니다.
에이전트가 얻게 되는 것
코드에 대한 특정 질문에 맞춰 설계된 8개의 도구(tools)가 제공됩니다:
| 도구 | 답변하는 내용 |
|---|---|
search_symbols | 심볼 이름(symbol names)에 대한 전체 텍스트 검색 — 진입점 |
| ... |
모든 응답에는 그래프 품질 상태(ok 또는 degraded)가 포함되어 있어, 에이전트가 부분적인 답변을 완전한 답변으로 오해하는 일이 발생하지 않습니다. 리스트는 상한선이 정해져 있으며, 조용히 잘리는 대신 truncated(잘림)로 표시됩니다.
내비게이션 기술이 가르치는 패턴은 다음과 같습니다: search_symbols에서 시작하여 get_callers / find_references로 범위를 넓히고, 모든 호출 파일을 처음부터 끝까지 읽는 대신 실제 소스 코드가 필요한 지점에 대해서만 get_node_info를 호출하는 방식입니다.
타이핑하는 동안 그래프가 최신 상태를 유지하는 이유
제품과 "엔진에 스크립트를 더한 것"을 구분 짓는 차이점은, 사용자가 편집하는 동안 그래프가 스스로 최신 상태를 유지한다는 점입니다.
파일 시스템 와처 (filesystem watcher)는 서버와 함께 시작됩니다. 디스크 상의 파일이 변경되면, 서버는 연결된 집합 (connected set) — 즉, 변경된 파일, 해당 파일을 임포트(import)하는 파일들, 그리고 해당 파일가 임포트하는 파일들 — 을 단 한 번의 전체 패스(full pass)로 재인덱싱(reindex)합니다. 이를 통해 파일 간의 엣지(edge)가 부분적으로 깨지는 대신, 파일 간 관계가 올바르게 재구축됩니다. 파일을 삭제하면 해당 심볼(symbol)을 제거하고 이를 임포트하는 파일들을 업데이트합니다. 모두가 잊어버리는 경우인 서버가 꺼져 있는 동안 이루어진 편집은 시작 시 수행되는 원샷 화해(one-shot reconcile) 과정을 통해 처리됩니다. 즉, 프로젝트를 스캔하고, 새로운 것을 인덱싱하며, 사라진 것을 제거하고, 변경된 것을 새로고침한 다음, 제어권을 와처(watcher)에게 넘깁니다.
그래프는 .graphlens/graph.db (SQLite)에 저장됩니다. 이는 재생성 가능한 캐시(cache)이므로 삭제해도 안전하며, reindex 명령어로 다시 구축할 수 있습니다. .graphlens/를 VCS 무시 목록(ignore)에 추가하세요.
의도적으로 구현하지 않은 것들
현재 상태는 초기 단계이며, 이를 미화하기보다는 솔직하게 말씀드리고 싶습니다. 내비게이션 코어(navigation core)는 작동하지만, 나머지는 진행 중입니다.
와처(watcher)는 변경 사항과 연결된 집합만을 재인덱싱하며 프로젝트 전체를 재인덱싱하지는 않습니다. 따라서 여러 단계의 간접 참조(indirection)를 통해 파급되는 리팩터링(refactor)의 경우, 완벽하게 정확한 그래프를 얻으려면 전체 reindex가 필요할 수 있습니다. 언어 간 COMMUNICATES_WITH 엣지는 전체 재인덱싱 시 재구축되지만, 증분 편집(incremental edits) 시에는 약화될 수 있습니다. Python 이외의 언어는 해당 툴체인(toolchain)이 갖춰져 있어야 합니다 (Python은 즉시 작동하며, ty가 함께 제공됩니다). 툴체인이 없으면 해당 언어는 degraded 상태로 보고됩니다. 이는 구조는 파싱되었으나 호출(calls)과 타입(types)이 완전히 해결되지 않았음을 의미합니다. 하지만 init 과정이 이로 인해 차단되지는 않으며, status를 통해 정확히 무엇이 누락되었는지 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기