Logo
Published on

CodeGraph 프로젝트 분석: 코딩 에이전트 밑에 까는 코드 지능 계층은 어떻게 만들어졌나요?

Authors

분석 일자: 2026-06-04 대상 패키지: @colbymchenry/codegraph 0.9.9 대상 커밋: 629d8472b14168841cd1f26b7022bf5934ff205d (2026-06-02) 저장소: https://github.com/colbymchenry/codegraph 로컬 분석 경로: ~/workspace/opensources/codegraph


This article is mostly written by Claude Code

목차

  1. 왜 CodeGraph인가요?
  2. 기존 글들과 어디에 놓이나요?
  3. 프로젝트를 한 문장으로 이해하기
  4. 기술 스택과 규모
  5. 전체 그림: 4단계 파이프라인
  6. 코드베이스 지도
  7. Extraction: tree-sitter를 worker thread에서 돌립니다
  8. Storage: SQLite 지식 그래프와 FTS5
  9. Resolution: 참조를 정의로 잇습니다
  10. 동적 디스패치: grep이 못 따라가는 hop을 합성합니다
  11. 그래프 순회와 impact 분석
  12. codegraph_explore: 답을 파일이 아니라 답 크기로 돌려줍니다
  13. MCP 계층: engine, session, daemon, proxy
  14. Auto-sync: 그래프를 코드와 같은 호흡으로 유지합니다
  15. 교차 언어 브리징: iOS, React Native, Expo
  16. 토큰 경제학: 벤치마크가 말하는 것
  17. 코드를 읽는 추천 순서
  18. 인상적인 설계 포인트
  19. 주의해서 볼 지점
  20. 결론

1. 왜 CodeGraph인가요?

지금까지 이 블로그에서 분석한 프로젝트들은 대부분 에이전트 그 자체였습니다. Hermes Agent, Qwen Code, OpenHands는 LLM 응답과 tool call을 왕복시키는 런타임이었고, Claude Code Game Studios는 그 위에 올린 오케스트레이션이었습니다.

CodeGraph는 한 칸 아래에 있습니다. CodeGraph는 에이전트가 아니라, 에이전트가 코드베이스를 더 싸게 이해하도록 돕는 코드 지능(code intelligence) 계층입니다.

문제 의식은 단순합니다. Claude Code 같은 에이전트가 낯선 코드베이스에서 "요청이 어떻게 DB까지 도달하나요?" 같은 질문을 받으면, 보통 Explore 서브에이전트를 띄워 grep, glob, Read로 파일을 훑습니다. 그리고 그 모든 tool call이 토큰을 태웁니다. 큰 저장소일수록 답에 도달하기 전에 탐색 비용이 폭발합니다.

CodeGraph의 해법은 이렇습니다.

첫째, 코드베이스를 미리 인덱싱해 지식 그래프로 만듭니다. tree-sitter로 소스를 파싱해 심볼(함수, 클래스, 메서드)을 노드로, 호출·import·상속 관계를 엣지로 추출하고, 전부 로컬 SQLite에 넣습니다.

둘째, 그 그래프를 MCP 서버로 에이전트에 노출합니다. 에이전트는 파일을 스캔하는 대신 그래프에 질의합니다. README의 표현을 빌리면 "에이전트는 이미 한 작업(인덱싱)을 grep/read로 반복하지 않습니다."

셋째, 100% 로컬입니다. API 키도, 외부 서비스도 없고 SQLite 파일 하나가 전부입니다.

그래서 CodeGraph를 "또 하나의 코드 검색 도구"로만 보면 작게 보입니다. 더 정확하게는 코딩 에이전트의 토큰 예산을 탐색이 아니라 답에 쓰게 만드는, 에이전트 밑에 까는 인프라입니다.

2. 기존 글들과 어디에 놓이나요?

CodeGraph의 위치는 그동안 본 프로젝트들과 비교하면 선명해집니다.

중심 문제CodeGraph와의 관계
Hermes Agent자체 TypeScript 코딩 에이전트 런타임CodeGraph가 README에서 명시적으로 지원하는 에이전트입니다. Hermes가 코드를 읽는 주체라면, CodeGraph는 그 주체에게 지도를 쥐여 줍니다.
Qwen Code터미널 코딩 에이전트가 플랫폼이 되는 과정Qwen Code가 tool registry/scheduler로 도구를 품는다면, CodeGraph는 그 안에 꽂히는 MCP 도구 하나입니다.
OpenHands코딩 에이전트를 웹 제품·sandbox로 운영OpenHands가 에이전트를 제품으로 운영한다면, CodeGraph는 어떤 에이전트든 공통으로 깔 수 있는 코드 이해 계층입니다.
Playwright브라우저를 프로토콜로 추상화Playwright가 브라우저를 CDP로 노출하듯, CodeGraph는 코드베이스를 MCP 도구 7개로 노출합니다. 둘 다 "에이전트가 만질 표면"을 정의합니다.
Superpowers에이전트에게 절차·지식을 주입Superpowers가 어떻게 일할지를 가르친다면, CodeGraph는 대상 코드가 어떻게 생겼는지를 가르칩니다.
agentmemory장기 memory와 shared contextagentmemory가 세션을 넘나드는 기억이라면, CodeGraph는 코드베이스에 대한 구조적 기억(graph)입니다.
WeKnora문서를 RAG로 검색하는 지식 베이스WeKnora가 문서를 임베딩으로 검색한다면, CodeGraph는 코드를 AST 그래프로 검색합니다. 둘 다 "검색 가능한 지식"을 만들지만 진입점이 임베딩 vs 정적 분석으로 갈립니다.

이 연결이 중요한 이유는, CodeGraph가 그동안 본 에이전트 글들의 공통 전제를 건드리기 때문입니다. 모든 코딩 에이전트는 "코드베이스를 이해한다"는 비용을 어떤 식으로든 치릅니다. Hermes도, Qwen Code도, Claude Code도 결국 파일을 읽습니다.

CodeGraph는 그 비용을 한 번의 인덱싱으로 선불하고, 이후의 모든 에이전트 세션이 그 인덱스를 공유하게 만듭니다. RAG가 문서 검색에서 했던 일을, 정적 분석으로 코드에서 하는 셈입니다.

3. 프로젝트를 한 문장으로 이해하기

CodeGraph는 tree-sitter로 20여 개 언어의 소스를 파싱해 심볼·엣지·파일을 로컬 SQLite 지식 그래프(FTS5 포함)로 만들고, 그 그래프를 MCP 서버로 코딩 에이전트에 노출하며, 파일 워처로 코드와 그래프를 같은 호흡으로 유지하는 zero-config 코드 지능 도구입니다.

이 한 문장에 네 가지 축이 들어 있습니다.

  • Extraction — tree-sitter AST에서 노드와 엣지를 뽑습니다.
  • Storage — 로컬 SQLite + FTS5에 저장합니다.
  • Resolution — 추출 후 참조를 정의로 잇습니다(import, 상속, 프레임워크 라우트, 동적 디스패치).
  • Serve & Sync — MCP로 노출하고, 파일 워처로 자동 갱신합니다.

4. 기술 스택과 규모

package.json과 소스를 보면 의외로 가벼운 의존성으로 짜여 있습니다.

항목내용
언어TypeScript (src/42,800 LOC)
파싱web-tree-sitter + tree-sitter-wasms (WASM 그래머)
저장소SQLite — Node 내장 node:sqlite (WAL 모드)
CLIcommander + @clack/prompts (인터랙티브 설치 마법사)
파일 필터ignore, picomatch (.gitignore 존중)
런타임Node >=20 <25. CLI/MCP는 번들된 자체 런타임으로 동작(설치 시 Node 불필요), 라이브러리 임베드 시에는 node:sqlite 때문에 Node 22.5+ 필요
테스트vitest (__tests__/에 평가 러너 eval 포함)
라이선스MIT

소스 디렉터리별 규모를 보면 무게중심이 분명합니다.

디렉터리파일LOC역할
resolution/3212,970참조 해소 — 가장 무겁습니다
extraction/329,159tree-sitter 추출 + 언어별 쿼리
mcp/105,745MCP 서버, daemon, session, proxy
installer/163,262에이전트 자동 설정 마법사
db/42,313SQLite 어댑터, 쿼리 빌더, 마이그레이션
context/31,673codegraph_explore 컨텍스트 빌더
graph/31,102BFS/DFS 순회
sync/51,101파일 워처, git hook, worktree
search/2553쿼리 파서/유틸

resolutionextraction이 전체의 절반을 넘습니다. 즉 CodeGraph의 본질적 난이도는 "파싱"보다 **"파싱한 조각을 의미 있는 그래프로 잇는 일"**에 있습니다. 이 점은 뒤에서 동적 디스패치 합성을 볼 때 더 분명해집니다.

5. 전체 그림: 4단계 파이프라인

README의 다이어그램을 코드 관점에서 다시 그리면 이렇습니다.

[1] Extraction          [2] Storage              [3] Resolution           [4] Serve & Sync
 src/extraction/         src/db/                   src/resolution/          src/mcp/ + src/sync/
 ─────────────           ─────────                 ───────────────          ────────────────
 tree-sitter WASM        nodes / edges / files     import-resolver          MCP 도구 7 worker thread           unresolved_refs           name-matcher             daemon (다중 세션)
 언어별 쿼리             FTS5 (nodes_fts)          framework resolvers      file watcher
   │                       │                       callback-synthesizer       (FSEvents/inotify)
   │  노드+엣지+미해소      │  미해소 참조 적재       │  미해소 → 엣지로 승격     │  debounce 2s
   └──────────────────────►└──────────────────────►└────────────────────────►└─────────────►
                                                                              codegraph_explore
MCP로 노출

핵심은 추출과 해소가 분리되어 있다는 점입니다. tree-sitter는 한 파일만 봅니다. "이 함수 호출이 어느 정의를 가리키는가"는 파일 하나로는 알 수 없으므로, 추출 단계에서는 *미해소 참조(unresolved_refs)*로 적어 두고, 모든 파일을 다 본 뒤 resolution 단계에서 전역적으로 잇습니다. 이 2-pass 구조가 CodeGraph 설계의 뼈대입니다.

6. 코드베이스 지도

처음 읽을 때 길잡이가 될 진입점들입니다.

  • src/index.tsCodeGraph 클래스. init/open, indexAll, sync, searchNodes, getCallers, buildContext, watch 등 공개 API의 파사드입니다.
  • src/bin/codegraph.ts — CLI 진입점. install, init, index, sync, serve --mcp, callers/callees/impact, affected 명령.
  • src/extraction/index.ts — 인덱싱 오케스트레이터(스캔 → 파싱 → 저장 → 해소).
  • src/extraction/tree-sitter.ts + languages/*.ts — AST에서 노드·엣지를 뽑는 언어별 쿼리.
  • src/db/schema.sql + db/queries.ts — 스키마와 쿼리 빌더.
  • src/resolution/index.ts — 참조 해소 오케스트레이터.
  • src/resolution/callback-synthesizer.ts — 동적 디스패치 엣지 합성.
  • src/graph/traversal.ts — BFS/DFS 그래프 순회.
  • src/context/index.ts + formatter.tscodegraph_explore 출력 생성.
  • src/mcp/engine.ts / session.ts / daemon.ts / proxy.ts — MCP 서버 4총사.
  • src/mcp/server-instructions.ts — 에이전트에게 주는 사용 지침(단일 진실 소스).
  • src/sync/watcher.ts — 파일 워처.

7. Extraction: tree-sitter를 worker thread에서 돌립니다

추출 오케스트레이터(src/extraction/index.ts)는 단순한 "파일 읽고 파싱"이 아니라, WASM 파서를 안정적으로 굴리기 위한 운영 코드가 대부분입니다. 세 개의 상수가 그 철학을 압축합니다.

const FILE_IO_BATCH_SIZE = 10 // I/O를 10개씩 겹쳐 파싱 CPU와 오버랩
const PARSE_TIMEOUT_MS = 10_000 // 한 파일 파싱이 행되면 10초 후 worker 재시작
const WORKER_RECYCLE_INTERVAL = 250 // 250파일마다 worker thread를 갈아엎음

특히 WORKER_RECYCLE_INTERVAL의 주석이 인상적입니다.

WASM linear memory는 늘어날 수 있지만 절대 줄어들지 않습니다(WebAssembly 스펙 한계). tree-sitter의 WASM 힙을 되찾는 유일한 방법은 worker thread를 종료하고 새로 띄워 V8 isolate 전체를 파괴하는 것입니다.

즉 CodeGraph는 tree-sitter WASM이 메모리를 영원히 붙들고 있다는 사실을 알고, 파서를 메인 스레드가 아니라 worker thread에서 돌린 뒤 주기적으로 통째로 버리는 방식으로 대응합니다. 대형 모노레포(VS Code 약 1만 파일 같은)를 인덱싱해도 메모리가 평평하게 유지되는 비결입니다. 동시에 PARSE_TIMEOUT_MS로 파싱이 행되는 파일이 전체 인덱싱을 얼리지 못하게 막습니다.

Playwright 글에서 브라우저 프로세스를 격리해 다루던 것과 결이 비슷합니다. 신뢰할 수 없는(혹은 자원을 안 돌려주는) 무거운 컴포넌트를 별도 프로세스/스레드에 가두고, 죽이고 다시 띄울 수 있게 설계하는 패턴입니다.

언어별 추출 로직은 src/extraction/languages/에 20여 개 파일로 나뉩니다(typescript.ts, python.ts, go.ts, rust.ts, swift.ts, kotlin.ts, …). 여기에 더해 vue-extractor.ts, svelte-extractor.ts, mybatis-extractor.ts, liquid-extractor.ts, dfm-extractor.ts 같은 프레임워크/템플릿 전용 추출기와 generated-detection.ts(생성 코드 식별)가 붙습니다.

8. Storage: SQLite 지식 그래프와 FTS5

스키마(src/db/schema.sql)는 작지만 정교합니다. 핵심 테이블 네 개입니다.

  • nodes — 심볼. id, kind, name, qualified_name, file_path, language, 시작/끝 라인·컬럼, signature, docstring, visibility, is_exported/async/static/abstract, decorators(JSON), type_parameters(JSON).
  • edges — 관계. source, target, kind, metadata(JSON), line, col, provenance. 노드 삭제 시 ON DELETE CASCADE로 엣지도 함께 정리됩니다.
  • files — 추적 파일. content_hash, language, size, modified_at, indexed_at, node_count. 동기화 시 (size, mtime) + 콘텐츠 해시로 변경을 판단합니다.
  • unresolved_refs — 아직 정의로 못 이은 참조. 추출 단계가 여기에 적어 두면 resolution 단계가 비웁니다.

전문 검색은 FTS5 가상 테이블 nodes_fts로 처리하고, **트리거 3개(INSERT/DELETE/UPDATE)**로 nodes와 자동 동기화합니다. 덕분에 노드를 갱신하면 검색 인덱스가 별도 코드 없이 따라옵니다.

엣지 인덱스 주석도 눈여겨볼 만합니다.

-- idx_edges_source / idx_edges_target are intentionally omitted —
-- the (source, kind) and (target, kind) composites below cover the
-- corresponding source-only / target-only lookups via SQLite's
-- left-prefix scan, so the narrow indexes are dead weight on writes.

(source, kind) 복합 인덱스가 SQLite의 left-prefix 스캔으로 source만 쓰는 조회까지 커버하므로, 단독 인덱스는 쓰기 비용만 늘리는 dead weight라 일부러 뺐다는 설명입니다. 인덱싱은 대량 쓰기 작업이므로 write amplification을 줄이는 게 합리적입니다.

저장소는 WAL 모드로 엽니다. README의 트러블슈팅에 따르면, 번들 Node의 node:sqlite WAL 모드에서는 동시 읽기가 writer에 막히지 않으므로 database is locked가 나지 않아야 합니다(네트워크 공유나 WSL2 /mnt처럼 WAL이 안 켜지는 파일시스템은 예외). 이건 뒤의 daemon 다중 세션 설계와 직결됩니다.

9. Resolution: 참조를 정의로 잇습니다

resolution/이 가장 큰 디렉터리인 이유가 여기 있습니다. tree-sitter는 "이 줄에 userService.find() 호출이 있다"까지만 압니다. 그 find어느 파일의 어느 메서드인지는 전역 정보가 필요합니다.

해소 오케스트레이터(src/resolution/index.ts)가 동원하는 전략들입니다.

  • import-resolver.ts — import 경로를 실제 소스 파일로 잇습니다. JVM import, C/C++ include dir, re-export까지 다룹니다.
  • name-matcher.ts — 이름 기반 매칭. 후보가 여러 개일 때 점수를 매깁니다.
  • path-aliases.tstsconfig의 path alias(@/components 같은)를 해석합니다.
  • go-module.ts, workspace-packages.ts — Go 모듈, 모노레포 워크스페이스 패키지 경로.
  • swift-objc-bridge.ts — Swift ↔ Objective-C 자동 브리징 이름 규칙.
  • frameworks/ — 14개 프레임워크의 라우팅 패턴(Django path(), Express app.get(), NestJS 데코레이터, Spring @GetMapping, Rails, Laravel, Gin 등)을 인식해 URL → 핸들러 엣지를 만듭니다.
  • callback-synthesizer.ts — 동적 디스패치 엣지 합성(다음 절).

오케스트레이터는 잘 알려진 빌트인(JS console/Promise, Python print/len, Go 표준 패키지 fmt/os 등)을 Set으로 들고 있다가 해소 대상에서 제외합니다. 그래야 console.log가 사용자 정의 심볼로 잘못 매칭되지 않습니다.

대형 코드베이스를 위해 각 리졸버 캐시는 LRU로 상한이 걸려 있습니다(DEFAULT_CACHE_LIMIT = 5000, CODEGRAPH_RESOLVER_CACHE_SIZE로 조정). 2만 파일짜리 저장소에서도 메모리가 평평하게 유지되도록 한 선택입니다 — 앞서 본 worker recycle과 같은 "규모에서 평평함을 유지한다"는 철학입니다.

10. 동적 디스패치: grep이 못 따라가는 hop을 합성합니다

CodeGraph에서 가장 흥미로운 부분입니다. 정적 분석의 한계는 동적 디스패치입니다. 콜백을 등록해 두고 나중에 호출하거나, 이벤트 이름으로 핸들러를 부르거나, JSX가 자식 컴포넌트를 렌더링하는 흐름은 AST만으로는 "누가 누구를 부르는지" 이을 수 없습니다. grep으로는 더더욱 못 따라갑니다.

callback-synthesizer.ts는 이 구멍을 휴리스틱 패턴 매칭으로 메우는 별도 패스입니다. 처리하는 채널 모양이 여러 가지입니다.

// 등록자 이름과 디스패처 이름을 정규식으로 식별
const REGISTRAR_NAME =
  /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/
const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i
  • 필드 기반 옵저버onUpdate(cb)로 콜백을 필드에 모으고, triggerUpdate()가 그 필드를 순회하며 호출하는 패턴 → triggerUpdate → (등록된 콜백) 엣지 합성.
  • 문자열 키 EventEmitterthis.on('mount', fn) 등록과 emit('mount') 디스패치를 이벤트 이름으로 짝지음. 단, 'error'처럼 너무 흔한 이름(fan-out이 EVENT_FANOUT_CAP = 6을 넘는)은 타입 정보 없이는 잘못 잇기 쉬워 건너뜁니다.
  • 클로저 컬렉션 디스패치 — Swift 우선. 메서드가 클로저를 컬렉션에 append하고, 다른 메서드가 coll.forEach { $0() }로 각 원소를 호출하는 패턴. $0( 호출이 "이 컬렉션은 클로저를 담는다"를 증명하므로 파일·클래스가 달라도 높은 정밀도로 짝지을 수 있습니다(주석은 Alamofire의 Request/DataRequest 예를 듭니다).
  • JSX/Vue — JSX 자식 컴포넌트(<MyView/>), Vue kebab-case 자식(<el-button>)과 이벤트 바인딩(@click="fn"), composable 구조분해까지.

설계 원칙은 **고정밀·저재현(high-precision, low-recall)**입니다. 이름이 붙은 콜백만, 흔한 이벤트 이름은 제외하고, fan-out 상한을 두고 짝짓습니다. 그리고 이렇게 합성한 엣지는 전부 provenance: 'heuristic'로 태깅하고 metadata.synthesizedBy에 채널 이름(swift-objc-bridge, rn-event-channel, fabric-native-impl 등)을 박아 둡니다. 에이전트가 "이 hop이 정적으로 확실한지, 휴리스틱으로 추정한 것인지"를 한눈에 구분할 수 있게 한 것입니다.

docs/design/dynamic-dispatch-coverage-playbook.md에는 이 작업의 동기가 실증적으로 적혀 있습니다. excalidraw에서 "업데이트가 어떻게 화면에 도달하는가"라는 질문을 던졌을 때, 핵심 엣지가 그래프에 없으면 에이전트는 파일을 읽어 흐름을 재구성하고, 엣지가 있으면 파일을 한 번도 읽지 않고 답을 끝낸다는 것을 측정으로 확인했다고 합니다. 즉 에이전트가 grep/Read를 안 하게 만드는 레버는 프롬프트가 아니라 그래프 커버리지라는 결론입니다.

이 지점이 CodeGraph를 단순 심볼 인덱서와 가르는 핵심입니다. 정적으로 잇기 어려운 흐름을 휴리스틱으로라도 메워야 에이전트가 그래프만으로 답을 완성하고, 그래야 토큰을 아낄 수 있다는 인과를 코드 전체가 따르고 있습니다.

11. 그래프 순회와 impact 분석

src/graph/traversal.tsGraphTraverser는 BFS/DFS를 제공합니다. 옵션으로 maxDepth, edgeKinds(특정 관계만), nodeKinds, direction(incoming/outgoing), limit(기본 1000), includeStart를 받습니다.

이 순회 위에 세 개의 분석 도구가 올라갑니다.

  • callers — 들어오는 호출 엣지를 따라 "이걸 누가 부르나".
  • callees — 나가는 호출 엣지를 따라 "이게 뭘 부르나".
  • impact — 어떤 심볼을 바꿨을 때 영향받는 코드의 blast radius. depth를 주어 transitive하게 넓힙니다.

CLI에는 여기서 파생된 실용 명령 codegraph affected도 있습니다. 변경된 소스 파일에서 import 의존성을 transitive하게 추적해 영향받는 테스트 파일을 찾아 줍니다. git diff --name-only | codegraph affected --stdin으로 CI에서 바뀐 코드와 관련된 테스트만 골라 돌리는 식입니다. 그래프를 검색뿐 아니라 빌드 파이프라인 최적화에도 쓸 수 있게 한 확장입니다.

12. codegraph_explore: 답을 파일이 아니라 답 크기로 돌려줍니다

MCP 도구는 7개입니다.

도구용도
codegraph_explore주력. 한 번 호출로 관련 심볼의 원본 소스를 파일별로 묶어 반환
codegraph_search이름으로 심볼 위치 찾기
codegraph_callers무엇이 이 함수를 부르나
codegraph_callees이 함수가 무엇을 부르나
codegraph_impact이 심볼을 바꾸면 무엇이 깨지나
codegraph_node특정 심볼 하나의 전체 소스(모호한 이름은 모든 오버로드 반환)
codegraph_files인덱싱된 파일 구조
codegraph_status인덱스 상태·통계

이 중 codegraph_explore가 설계의 정점입니다. 자연어 질문("X는 어떻게 동작하나요")이든 심볼 이름 묶음이든 받아서, 관련 심볼들의 verbatim 소스를 파일별로 묶고, 관계 맵과 blast radius를 곁들여 한 번에 돌려줍니다. 에이전트 입장에서는 사실상 Read와 동등하지만, 여러 파일에 흩어진 답을 한 round-trip에 받는다는 게 다릅니다.

흥미로운 건 출력 크기를 프로젝트 규모에 맞춰 적응적으로(adaptive) 조절한다는 점입니다(src/mcp/tools.tsgetExploreBudget, ExploreOutputBudget).

export function getExploreBudget(fileCount: number): number {
  if (fileCount < 500) return 1
  if (fileCount < 5000) return 2
  if (fileCount < 15000) return 3
  if (fileCount < 25000) return 4
  return 5
}

작은 프로젝트는 총량 cap, 기본 파일 수, 파일당 cap, 클러스터링을 모두 빡빡하게 잡습니다. 100파일짜리 프로젝트에 질문 하나 던졌다고 파일 한 통째를 컨텍스트에 쏟아붓지 않기 위함입니다. 큰 코드베이스는 후한 기본값을 유지합니다 — 그 규모에서는 에이전트의 native 탐색 비용(grep+find+여러 Read)이 살찐 explore 호출을 압도하니까요.

docs/design/adaptive-explore-sizing.md에는 이 튜닝의 실전 기록이 생생합니다. 초기 버전은 "interchangeable한 형제 구현이 많은(sibling-heavy)" 흐름에서 출력을 너무 잘라내(skeletonize) 에이전트가 그 파일을 다시 Read하게 만드는 회귀가 있었습니다. OkHttp의 RealCall, Django의 compiler.py가 그랬습니다. 그래서 "에이전트가 이름을 댄 callable이 있는 파일은 full로 살리되, ≥3개 구현의 슈퍼타입을 정의하는 family 파일은 어차피 Read당하니 오히려 skeleton으로 줄여 예산을 형제 파일에 넘긴다"는 규칙으로 다듬어, OkHttp는 3% 더 비싸던 것을 ~10% 싸게, Django는 10% 비싸던 것을 ~14–17% 싸게 만들었다고 적혀 있습니다.

요지는 이것입니다. codegraph_explore는 "파일 N개를 돌려주는" 도구가 아니라 "답 하나에 필요한 만큼을 돌려주는" 도구를 지향합니다. 그리고 그 "만큼"을 실제 에이전트 A/B로 끊임없이 보정합니다. 이건 단순 검색 API가 아니라 LLM 컨텍스트 윈도우를 1급 자원으로 다루는 출력 설계입니다.

13. MCP 계층: engine, session, daemon, proxy

src/mcp/는 네 개의 협력자로 나뉩니다.

  • engine.ts (MCPEngine) — 무거운 공유 상태. 프로젝트의 CodeGraph 인스턴스, 파일 워처, ToolHandler 캐시를 한 벌만 듭니다. "엔진 하나, 세션 여럿"이 원칙입니다.
  • session.ts (MCPSession) — MCP 프로토콜 상태 머신. 소켓 연결마다 하나씩.
  • daemon.ts — 프로젝트 루트마다 detached 데몬 하나. Unix 도메인 소켓(Windows는 named pipe)으로 N개의 MCP 클라이언트를 받습니다.
  • proxy.ts — MCP 호스트가 실제로 띄우는 얇은 프로세스. 데몬과 호스트 사이를 잇습니다.

이 구조의 동기(이슈 #411)가 흥미롭습니다. Claude Code, Cursor 등 여러 에이전트가 같은 프로젝트를 동시에 열면, 각자 워처(inotify set)와 SQLite 연결과 tree-sitter 워밍업을 따로 가지는 건 낭비입니다. 그래서 데몬 하나가 그 비용을 한 번만 치르고 모든 세션이 같은 WAL, 같은 워처를 공유합니다.

생명주기 처리가 견고합니다.

  • 데몬은 detached로 뜹니다(자체 세션/프로세스 그룹). 어떤 MCP 호스트의 자식도 아니므로, 터미널 하나를 닫거나 Ctrl-C해도 다른 세션이 끊기지 않습니다.
  • 대신 각 호스트는 proxy를 통해 데몬과 통신하고, proxy는 PPID 워치독(이슈 #277)을 들고 있어 호스트가 SIGKILL당하면 proxy가 곧바로 정리되며, proxy의 소켓 종료가 데몬의 refcount를 깎습니다.
  • 마지막 클라이언트가 끊겨도 데몬은 CODEGRAPH_DAEMON_IDLE_TIMEOUT_MS(기본 300초)만큼 머무릅니다. 같은 프로젝트에서 연속으로 에이전트를 돌릴 때 startup 비용을 다시 치르지 않기 위함입니다.
  • 경쟁 데몬은 락파일(.codegraph/daemon.pid)을 atomic O_EXCL 생성으로 중재합니다. 빈 파일 윈도우 없이 레코드를 한 번에 씁니다.

또 하나, startup 경로에서 무거운 체인을 지연 로딩합니다.

// sqlite + query/graph/context 레이어를 MCP startup 경로에서 떼어냄.
// 도구가 실제로 프로젝트를 열 때만 require — initialize/tools-list엔 불필요.
const loadCodeGraph = () => require('../index').default

이렇게 한 이유는 헤드리스 에이전트의 cold-start 레이스 때문입니다. serve --mcp가 ~800ms 대신 거의 Node startup 시간에 바인딩하고 도구를 등록하게 만들어, 에이전트가 첫 호출에서 "No such tool available"로 헤매던 문제를 막습니다. 에이전트라는 까다로운 클라이언트의 타이밍 특성에 맞춘 최적화입니다.

Qwen Code 글에서 본 qwen serve daemon, Hermes Agent의 런타임 경계와 비교하면 방향이 반대인 게 재미있습니다. 그쪽은 에이전트를 데몬으로 노출했고, CodeGraph는 에이전트가 쓸 도구를 데몬으로 공유합니다.

14. Auto-sync: 그래프를 코드와 같은 호흡으로 유지합니다

지식 그래프의 숙명적 위험은 stale입니다. 코드는 바뀌었는데 그래프가 옛날을 가리키면, 에이전트는 조용히 틀린 답을 받습니다. CodeGraph는 이 위험을 세 겹으로 막습니다.

  1. 파일 워처 + debounced auto-sync. 네이티브 OS 이벤트(FSEvents/inotify/ReadDirectoryChangesW)로 소스 파일 변경을 잡고, debounce 윈도우(기본 2000ms, CODEGRAPH_WATCH_DEBOUNCE_MS[100ms, 60s] 범위 내 조정) 뒤 증분 재인덱싱합니다. 연속 편집은 한 번의 sync로 합쳐집니다.

  2. 파일별 staleness 배너. debounce 윈도우 동안, 아직 sync 안 된 파일을 참조할 MCP 응답에는 ⚠️ 배너를 붙여 "이 파일은 직접 Read하라"고 명시합니다. 응답이 참조하지 않는 pending 파일은 작은 footer로 알립니다. 조용히 틀린 답을 주는 대신 명시적 신호를 주는 설계입니다.

  3. Connect-time catch-up. MCP 서버가 (재)연결되면 첫 질의에 답하기 전에 working tree와 (size, mtime) + 콘텐츠 해시 대조를 빠르게 돌립니다. 워처가 안 돌던 사이의 변경(터미널 git pull, 다른 에디터 편집, 이전에 종료된 세션)을 다음 세션 첫 tool call에서 흡수합니다.

engine.tscatchUpSync()는 이 catch-up을 백그라운드로 돌리되, 반환된 promise를 ToolHandler의 일회성 gate로 꽂아 첫 tool call이 sync 완료를 기다리게 합니다. 안 그러면 sync를 앞질러 간 호출이 "디스크에 없는 파일"의 행을 돌려줄 수 있기 때문입니다. 정확성과 지연 사이의 균형을 코드 한 줄(setCatchUpGate(p))로 잡습니다.

agentmemory 글에서 본 "기억의 신선도" 문제와 같은 결입니다. 보조 계층(memory든 graph든)은 본체(코드)와 동기화가 깨지는 순간 가치가 음수가 됩니다. CodeGraph는 이걸 워처·배너·catch-up 3중으로 방어합니다.

15. 교차 언어 브리징: iOS, React Native, Expo

실제 iOS·RN 코드베이스는 여러 언어를 넘나듭니다. Swift가 자동 브리징된 Objective-C 셀렉터를 부르고, JS가 네이티브 모듈을 호출하고, JSX가 네이티브 뷰 매니저로 위임합니다. tree-sitter 추출은 각 언어 경계에서 멈추므로, CodeGraph는 이 틈을 메우는 전용 브리징을 둡니다.

경계JS/Swift 쪽네이티브 쪽방식
Swift → ObjCobj.foo(bar:)-fooWithBar:@objc 자동 브리징 규칙 + Cocoa 전치사 접두사
RN legacy bridgeNativeModules.X.fn()RCT_EXPORT_METHOD / @ReactMethod매크로/애너테이션 → JS이름↔네이티브메서드 맵
RN TurboModulesimport M from './NativeM'Codegen spec 구현Native<X>.ts spec을 ground truth로
RN 네이티브 → JS 이벤트NativeEventEmitter().addListenersendEventWithName: / .emit()이벤트 이름 리터럴로 채널 합성
Expo ModulesrequireNativeModule('X').fn()Module { AsyncFunction("fn") }Expo DSL 리터럴 파싱
Fabric/Paper 뷰<MyView/>Codegen spec + 네이티브 impl이름+접미사 규약(View/Manager 등) 매칭

각 브리지 엣지는 provenance:'heuristic'synthesizedBy 채널 이름이 박혀 있고, README에는 Charts·realm-swift·Wikipedia-iOS, react-native-firebase, expo-camera 등 실제 오픈소스 저장소로 검증했다고 명시됩니다. 정적 분석이 가장 약한 지점(언어 경계의 동적 연결)을 가장 공들여 메운 셈입니다 — 10절에서 본 "커버리지가 곧 토큰 절약"이라는 원칙의 연장선입니다.

16. 토큰 경제학: 벤치마크가 말하는 것

CodeGraph의 가치 명제는 결국 숫자로 증명되어야 합니다. README의 벤치마크는 7개 언어의 실제 오픈소스 7개에 대해, Claude Opus 4.8을 헤드리스로 돌려 CodeGraph 있을 때 vs 없을 때 같은 질문에 답하게 하고 4회 중앙값을 비교합니다(2026-06-02 재검증).

평균: 16% 저렴 · 토큰 47% 절감 · 22% 빠름 · tool call 58% 감소

코드베이스언어/규모비용토큰tool calls
VS CodeTS · ~1만 파일18% 저렴64% 절감81% 감소
DjangoPython · ~3천8% 저렴60% 절감77% 감소
AlamofireSwift · ~11040% 저렴64% 절감58% 감소
TokioRust · ~790even38% 절감57% 감소

특히 솔직한 건 방법론 주석입니다. "이 숫자는 이전 Opus 4.7 검증보다 낮다 — CodeGraph 회귀가 아니라 baseline이 세졌기 때문"이라고 적습니다. Opus 4.8은 메인 스레드에서 효율적으로 grep/read하므로, CodeGraph 없는 쪽도 예전보다 날렵해졌다는 것입니다.

여기서 두 가지를 읽을 수 있습니다. 첫째, CodeGraph의 가장 큰 효과는 토큰과 tool call 수입니다(47%, 58%). 비용 절감은 그보다 작고(평균 16%), response-heavy한 저장소(Excalidraw, Tokio)에서는 break-even에 가깝습니다 — 많은 작은 grep/read round-trip을 몇 개의 큰 cache-heavy 응답으로 바꾸는 trade이기 때문입니다.

둘째, 모델이 강해질수록 baseline이 따라온다는 점을 제작자 스스로 인정합니다. 이건 브라우저 자동화 비교 글에서 본 긴장과 닮았습니다. 에이전트를 보조하는 도구의 가치는, 에이전트 자체가 똑똑해질수록 재측정되어야 합니다.

17. 코드를 읽는 추천 순서

CodeGraph를 처음 읽는다면 아래 순서를 추천합니다.

  1. README.md — 가치 명제, 벤치마크, MCP 도구 7개, 지원 언어/프레임워크.
  2. src/index.tsCodeGraph 파사드. 공개 API의 전체 표면.
  3. src/db/schema.sql — 노드·엣지·파일·미해소참조·FTS5. 데이터 모델을 먼저 잡습니다.
  4. src/extraction/index.ts — 스캔→파싱→저장→해소 오케스트레이션과 worker recycle/timeout.
  5. src/resolution/index.ts — 참조 해소 전략의 큰 그림.
  6. src/resolution/callback-synthesizer.ts — 동적 디스패치 합성(이 프로젝트의 정수).
  7. src/context/index.ts + tools.tsExploreOutputBudgetcodegraph_explore가 출력을 어떻게 사이징하는지.
  8. src/mcp/daemon.ts + engine.ts — 다중 세션 데몬과 공유 엔진.
  9. src/mcp/server-instructions.ts — 에이전트에게 주는 사용 지침.
  10. docs/design/*.mdadaptive-explore-sizing, callback-edge-synthesis, dynamic-dispatch-coverage-playbook. 설계 의사결정의 실전 기록.

18. 인상적인 설계 포인트

1. 추출과 해소를 분리한 2-pass 구조

tree-sitter는 한 파일만 보므로, 추출 단계는 미해소 참조를 unresolved_refs에 적어 두고, 전 파일을 다 본 뒤 resolution이 전역적으로 잇습니다. 정적 분석 그래프 빌더의 정석을 깔끔하게 구현했습니다.

2. WASM 메모리 한계를 worker recycle로 정면 돌파

tree-sitter WASM 힙이 줄어들지 않는다는 사실을 알고, 파서를 worker thread에서 돌린 뒤 250파일마다 통째로 버립니다. 대형 모노레포에서도 메모리가 평평합니다.

3. codegraph_explore가 출력을 "답 크기"로 사이징

프로젝트 규모에 맞춰 예산을 적응적으로 조절하고, 실제 에이전트 A/B로 read-back 회귀까지 잡아 가며 보정합니다. LLM 컨텍스트를 1급 자원으로 다루는 출력 설계입니다.

4. 커버리지를 토큰 절약의 인과로 본 일관성

"그래프에 흐름이 없으면 에이전트가 파일을 읽는다"는 측정 결과를 출발점으로, 동적 디스패치 합성과 교차 언어 브리징에 가장 많은 공을 들였습니다. 프로젝트 전체가 하나의 가설을 따라 짜여 있습니다.

5. 다중 세션 데몬으로 비용을 한 번만 치름

워처·SQLite·tree-sitter 워밍업을 데몬 하나가 선불하고 모든 에이전트 세션이 공유합니다. detached 데몬 + PPID 워치독 proxy + 락파일 + idle timeout으로 생명주기가 견고합니다.

6. Stale에 대한 정직함

debounce 윈도우 동안 staleness 배너로 "직접 Read하라"고 명시하고, connect-time catch-up을 첫 tool call의 gate로 막습니다. 조용히 틀리는 대신 명시적으로 신호합니다.

19. 주의해서 볼 지점

1. 휴리스틱 엣지는 정밀도를 위해 재현을 포기합니다

동적 디스패치 합성은 high-precision/low-recall입니다. 흔한 이벤트 이름, fan-out이 큰 채널은 일부러 건너뜁니다. 즉 provenance:'heuristic' 엣지가 없다고 연결이 없는 건 아닙니다. 누락된 흐름은 여전히 있을 수 있습니다.

2. 가치는 모델 세대에 따라 재측정되어야 합니다

제작자가 인정하듯 Opus 4.8 baseline은 4.7보다 효율적이라 절감폭이 줄었습니다. 비용 절감은 평균 16%이고 일부 저장소는 break-even입니다. "토큰·tool call은 확실히 줄지만 비용은 워크로드에 따라 다르다"가 정확한 기대치입니다.

3. WAL이 안 켜지는 파일시스템

네트워크 공유, WSL2 /mnt 등에서는 WAL이 안 켜져 읽기가 쓰기에 막힐 수 있습니다(database is locked). 다중 세션 데몬의 동시성 이점이 이런 환경에선 약해집니다. 로컬 디스크 권장.

4. 인덱스가 없으면 가치도 없습니다

codegraph init -i로 인덱싱하지 않으면 도구가 동작하지 않습니다. 또 codegraph_explore직접 질의될 때만 효과가 있어서, 에이전트가 탐색을 파일 읽는 서브에이전트에 위임하면 그 서브에이전트는 파일을 읽고 CodeGraph는 오버헤드가 됩니다. server-instructions.ts가 "위임하지 말고 직접 답하라"고 강하게 지시하는 이유입니다.

5. 정적 분석 본연의 한계

리플렉션, 런타임 생성 코드, 매크로 전개, 극히 동적인 디스패치는 여전히 그래프에 안 잡힐 수 있습니다. 1MB 초과 파일, .gitignore/기본 제외 디렉터리는 인덱싱하지 않습니다.

20. 결론

CodeGraph는 "또 하나의 코드 검색 도구"보다 훨씬 구체적인 프로젝트입니다. 실제 정체성은 코딩 에이전트의 토큰 예산을 탐색이 아니라 답에 쓰게 만드는, 에이전트 밑에 까는 코드 지능 계층입니다.

Hermes AgentQwen Code가 코드를 읽고 고치는 주체라면, CodeGraph는 그 주체에게 미리 그린 지도를 쥐여 줍니다. WeKnora가 문서를 임베딩으로 검색 가능하게 만들었다면, CodeGraph는 코드를 AST 그래프로 검색 가능하게 만듭니다. RAG가 문서에서 했던 일을, 정적 분석이 코드에서 합니다.

CodeGraph를 볼 때 가장 중요한 질문은 "어떤 언어를 지원하나요?"가 아닙니다. 더 중요한 질문은 다음입니다.

코딩 에이전트가 낯선 코드베이스를 이해하는 비용을, 매 세션 grep/Read로 다시 치르지 않고 한 번의 인덱싱으로 선불해 공유하려면, 그 그래프는 얼마나 완전하고 얼마나 신선하며 출력은 얼마나 답 크기에 맞아야 하나요?

CodeGraph의 답은 2-pass 추출·해소, 동적 디스패치 합성, adaptive codegraph_explore, 다중 세션 데몬, 3중 auto-sync입니다. 이 경계들을 이해하면, CodeGraph가 단순 인덱서가 아니라 에이전트 시대의 코드 이해 비용을 재설계하려는 인프라임을 볼 수 있습니다.