본문으로 건너뛰기
wooncloud

AI 에이전트가 grep으로 코드를 더듬는 비용, CodeGraph로 걷어내기

CodeGraph는 코드베이스를 로컬 SQLite 그래프로 미리 인덱싱해 호출 관계·블라스트 반경·소스를 explore 한 번으로 돌려주고, 데이터가 기계 밖으로 나가지 않아 grep 반복 탐색을 줄입니다.

·15 min read·— views··

AI 코딩 에이전트를 며칠만 써 보면 한 가지 패턴이 눈에 들어옵니다. 무언가를 시키면 본격적인 작업에 들어가기 전에 한참을 "탐색"합니다. grep 한 번, 파일 하나 읽고, 거기서 나온 심볼을 다시 grep, 또 파일을 읽습니다. 정작 코드를 고치기도 전에 컨텍스트 창의 절반이 파일 덤프로 차 있습니다. 사람이 IDE에서 "정의로 이동" 한 번이면 끝낼 일을, 에이전트는 텍스트 검색으로 더듬더듬 재구성하고 있는 셈입니다.

CodeGraph는 이 탐색 단계를 통째로 들어내려는 도구입니다. 코드베이스를 미리 한 번 파싱해서 심볼과 그 관계를 그래프로 만들어 두고, 에이전트가 물어보면 "필요한 소스 + 그게 누구를 부르고 누가 그걸 부르는지"를 한 번의 호출로 돌려줍니다. grep/read 루프 수십 번이 explore 한두 번으로 줄어듭니다. 아래 내용은 v1.1.0을 한 프로젝트에 붙여서 한동안 써 본 기록입니다.

grep으로 코드를 "읽는" 게 왜 비싼가

LLM 에이전트가 코드를 이해하는 기본 동작은 결국 텍스트 검색입니다. requirePermission이 어디서 쓰이는지 알고 싶으면 그 문자열을 grep하고, 매칭된 파일들을 열어서 읽고, 그중 또 호출 관계를 따라가려면 다시 grep을 돕니다. 문제가 몇 가지 겹칩니다.

  • 호출 관계를 텍스트로는 못 따라갑니다. 콜백, 동적 디스패치, JSX 자식으로 넘어가는 흐름은 문자열 매칭으로 안 잡힙니다. 에이전트는 추측하거나, 더 많은 파일을 읽거나, 그냥 놓칩니다.
  • 토큰이 샙니다. 함수 하나만 보면 되는데 매칭된 파일 전체를 읽어 들입니다. 컨텍스트는 한정 자원인데 관련 없는 코드로 채워집니다.
  • 느립니다. grep → read → grep 은 본질적으로 직렬입니다. 매 라운드가 왕복 한 번이고, 실제 작업은 그 뒤에 시작됩니다.

핵심은, 이 "구조 파악"이 매번 새로 할 필요가 없는 일이라는 점입니다. 호출 그래프는 코드가 바뀔 때만 바뀝니다. 그렇다면 한 번 계산해서 저장해 두고 재사용하면 됩니다. 그게 CodeGraph가 하는 일입니다.

CodeGraph가 하는 일

한 줄로 줄이면, 코드베이스의 지식 그래프를 로컬 SQLite에 미리 만들어 두고 에이전트에 MCP 도구로 물려주는 인덱서입니다. README의 표현을 빌리면 "surgical context · fewer tool calls · faster answers · 100% local"인데, 마지막 항목이 개인적으로 가장 컸습니다. 데이터가 기계 밖으로 안 나갑니다. API 키도, 외부 서비스도 없고 SQLite 파일 하나가 전부입니다. 회사 코드를 다루는 입장에선 이게 채택 가능 여부를 가릅니다.

인덱싱 파이프라인 자체는 단순합니다.

tree-sitter가 소스를 AST로 파싱하고, 언어별 쿼리로 노드(함수·클래스·메서드·라우트)와 엣지(호출·import·상속)를 뽑아 로컬 SQLite에 넣습니다. 검색은 FTS5(전문 검색)로 깔립니다. 파일 하나에 갇히지 않도록 참조를 교차 해석하는 게 포인트입니다. 호출은 정의로, import는 원본으로, 상속 체인을 파일 경계 너머로 잇습니다. 그 뒤로는 OS 네이티브 파일 워처(macOS면 FSEvents)가 변경을 감지해 디바운스 후 자동으로 sync하므로, 한 번 init하면 코딩하는 동안 그래프가 알아서 따라옵니다.

지원 언어도 넉넉합니다. TypeScript/Python/Go/Rust/Java/C#/Swift/Kotlin 등 20개 이상이고, Django·Flask·Express·Rails 같은 웹 프레임워크 17종의 라우팅을 인식합니다. 라우트가 노드 종류로 따로 잡히는 게 백엔드 코드에선 은근히 유용했습니다(뒤에 status 출력의 route 항목으로 다시 나옵니다).

실제로 붙여 보기

설치는 세 단계입니다. 스크립트 한 줄로 깔고, 에이전트에 등록하고, 프로젝트마다 인덱스를 만듭니다.

curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
codegraph install   # Claude Code, Cursor, Codex 등에 MCP 서버 등록
codegraph init      # 현재 프로젝트 인덱싱

init이 끝나면 .codegraph/codegraph.db가 생깁니다. 상태를 찍어 보면 그래프 규모가 한눈에 보입니다. 아래는 제가 인덱싱한 중간 규모 TS/JS 프로젝트의 실제 수치입니다.

CodeGraph Status
 
Index Statistics:
  Files:     586
  Nodes:     6,625
  Edges:     21,989
  DB Size:   20.19 MB
  Backend:   node:sqlite — built-in (full WAL)
 
Nodes by Kind:
  import     2,637
  function   1,596
  constant     768
  file         582
  interface    494
  route        151
  ...

586개 파일에서 심볼 6,625개와 엣지 21,989개를 뽑아 20MB짜리 SQLite 하나에 담았습니다. 이 20MB가 에이전트가 grep으로 매번 재구성하던 구조 정보 전부입니다.

explore — 보통 이거 하나면 끝난다

가장 자주 쓰는 명령입니다(MCP에선 codegraph_explore 도구로 노출됩니다). 자연어 질문이나 심볼 이름을 던지면 세 덩어리를 돌려줍니다. 연관 심볼의 verbatim 소스, 호출 관계, 그리고 블라스트 반경입니다. 출력은 대략 이렇게 생겼습니다(내용은 설명용으로 단순화했습니다).

$ codegraph explore "how does the permission check work"
 
Found 23 symbols across 9 files.
 
Blast radius — what depends on these (update/verify before editing)
- permissionToApi (.../mappers.ts:84) — 7 callers in permissions.service,
  files.service, folders.service; tests: core-extended.test
- insertOwnerPermission (.../permissions.repo.ts:75) — 2 callers; ⚠️ no covering tests
 
Relationships
  calls:  GET /:id/permissions → listPermissions
          addPermission → permissionToApi
 
Source Code   (줄번호 붙은 실제 on-disk 소스 — Read 한 것과 byte-for-byte 동일)
 
  server/auth/permission.ts — requirePermission (function)
  131  export function requirePermission(role: Role = "viewer") {
  132    return async (req, res, next) => {
  133      const granted = await resolveRole(req.user, req.params.id)
  134      if (!hasRole(granted, role)) return res.status(403).end()
  135      next()
  136    }
  137  }

세 가지가 한 응답에 같이 온다는 게 핵심입니다. "이 코드"만 주는 게 아니라 이걸 고치면 누가 깨지는지(블라스트 반경), 그리고 테스트가 걸려 있는지까지 같이 알려줍니다. grep으로는 ⚠️ no covering tests 같은 건 절대 못 뽑습니다. 게다가 Source Code가 줄번호 붙은 실제 디스크 내용이라, 받은 그대로 바로 편집에 쓸 수 있습니다. 에이전트 입장에선 "여러 파일을 읽어 보겠습니다" 단계가 통째로 사라지는 셈입니다.

impact — 고치기 전에 폭을 본다

전체 그림이 아니라 심볼 하나의 영향 범위만 보고 싶을 때가 있습니다.

codegraph node requirePermission   # 한 심볼의 소스 + 호출자/피호출자
codegraph callers makeId           # 이 함수를 부르는 곳 전부
codegraph impact makeId            # 이걸 바꾸면 영향받는 코드(테스트 포함)

이 중 impact에 손이 제일 자주 갔습니다. 공용 ID 생성 헬퍼 하나를 손보려다 codegraph impact makeId를 쳤더니 "66 affected symbols"가 떴습니다. 업로드 서비스, 컨트롤러, 그리고 제가 깜빡했을 게 분명한 테스트 파일까지 파일별로 쭉 나왔습니다. 헬퍼 하나가 이렇게 멀리 뻗어 있는 줄은 그래프를 보기 전엔 몰랐습니다. 변경 폭을 눈에 넣고 편집을 시작하는 것과, 고친 다음 무엇이 깨졌는지 뒤늦게 아는 것은 차이가 큽니다.

에이전트에 물리기

MCP 등록은 codegraph install이 알아서 해 주지만, 프로젝트에 박아 두고 싶으면 설정 파일 한 조각이면 됩니다. Claude Code 기준 .mcp.json은 이렇게 생겼습니다.

{
  "mcpServers": {
    "codegraph": {
      "type": "stdio",
      "command": "codegraph",
      "args": ["serve", "--mcp"]
    }
  }
}

여기에 "코드 읽기 전에 codegraph부터 써라"는 한 줄을 에이전트 지침에 넣어 두니, 작업 전에 파일을 우르르 열어 보던 습관이 눈에 띄게 줄었습니다.

git worktree를 쓴다면 — 한 번 걸린 함정

.codegraph/는 기본적으로 gitignore됩니다(인덱스는 빌드 산출물이니 당연합니다). 그런데 git worktree를 즐겨 쓴다면 여기서 발이 한 번 걸립니다. worktree로 새 작업 디렉터리를 만들면 untracked인 .codegraph/가 따라오지 않습니다. 정작 편집이 일어나는 워크트리에서 그래프가 깜깜이가 되는 것입니다.

해결은 간단합니다. MCP 설정(.mcp.json)은 추적 파일로 커밋해 모든 워크트리에 전파하고, 워크트리에 인덱스가 없으면 자동으로 만들게 훅을 겁니다. Claude Code라면 SessionStart 훅으로:

command -v codegraph >/dev/null 2>&1 \
  && [ ! -d ".codegraph" ] \
  && ( codegraph init . >/dev/null 2>&1 & ) ; true

세션 시작 시 .codegraph/가 없으면(=새 워크트리) 백그라운드로 그 브랜치 기준 인덱스를 새로 뜹니다. codegraph가 안 깔린 환경에선 조용히 no-op이라 안전합니다. 워크트리당 20MB쯤 더 쓰지만 git worktree remove할 때 같이 정리됩니다. 막 작성한 코드까지 정확히 반영하려면 결국 워크트리별 인덱스가 맞습니다.

그래서, 쓸 만한가

README가 내건 벤치마크는 7개 코드베이스에서 "tool call 58% 감소 · 22% 빠름 · 파일 읽기 거의 0"이고, 절약 폭은 코드베이스 규모에 따라 다르다고 솔직하게 적어 두었습니다. 저는 이 수치를 재현해 보진 않았습니다. 어디까지나 정성적 인상이지만, 에이전트가 작업 전에 파일을 끌어모으던 습관이 줄고 호출 관계 질문에 답이 한 번에 나온다는 점은 분명했습니다.

한계도 있습니다.

  • 인덱스는 쓰기보다 약 1초 늦습니다. 파일 워처가 디바운스 후 sync하므로, 방금 저장한 코드를 곧바로 explore하면 직전 상태가 잡힐 수 있습니다. 보통은 무시할 만합니다.
  • 그래프가 답하는 건 "구조"지 "의도"가 아닙니다. 누가 누구를 부르는지는 정확하지만, 왜 그렇게 짰는지는 여전히 코드와 주석을 읽어야 합니다.
  • 동적 디스패치는 정적 호출만큼 깔끔하진 않습니다. 다만 콜백·런타임 디스패치 일부를 따라가 주므로, 적어도 grep보다는 확실히 더 잡습니다.

붙이고 나서 달라진 건 단순합니다. 에이전트한테 뭘 시키면 더는 파일부터 열지 않습니다. 호출 관계를 물으면 여러 파일을 읽어 보겠다는 말 없이 답이 바로 나오고, 헬퍼 하나를 고치기 전에 영향 범위가 먼저 손에 잡힙니다. 회사 코드라 데이터가 기계 밖으로 안 나가는 것도 마음에 들었습니다. codegraph init 한 번 치는 값이 이 정도면 쌉니다.