며칠 전 CodeGraph - AI 코딩 에이전트를 위한 코드 지식 그래프라는 글을 읽었습니다. Claude Code나 Cursor 같은 AI 코딩 에이전트가 코드베이스를 탐색할 때 매번 grep과 Read를 반복하면서 토큰을 태우는 문제를, 미리 만들어둔 코드 지식 그래프로 해결하겠다는 접근이 흥미로웠습니다. 평균 도구 호출 71% 감소, 토큰 57% 감소, 비용 35% 절감이라는 벤치마크 수치도 눈에 띄었고요.
CodeGraph는 한마디로 "코드베이스의 심볼,호출,구조를 SQLite에 인덱싱해두고, MCP(Model Context Protocol) 서버로 에이전트에게 질의 인터페이스를 제공하는 도구"입니다. 20개 이상의 언어를 Tree-sitter 기반으로 파싱하고, 파일 변경은 OS 네이티브 이벤트(FSEvents/inotify)로 따라잡습니다. 100% 로컬이라 외부 API 호출이 없는 것도 인상 깊었습니다.
읽다 보니 "그럼 팀 단위로 공유하려면 어떻게 올려야 할까?"라는 의문이 생겼습니다. 로컬 실행이 기본인 도구를 AWS 위에 어떻게 자연스럽게 올릴지 고민하다가, EC2와 EFS 조합으로 PoC를 진행해봤습니다. 이 글은 그 여정을 정리한 기록입니다.
테스트 대상 코드베이스는 약 12만 라인의 TypeScript/Python 혼합 모노레포로 잡았습니다. 평소 Claude Code로 작업하면서 "이 함수 어디서 호출해?" 같은 질문에 매번 grep이 4~5번 도는 게 답답했던 코드베이스입니다.
(A) 설계 의도와 아키텍처
먼저 CodeGraph 자체의 동작 원리부터 정리하면 이렇습니다. 핵심은 Tree-sitter로 AST를 만들고, 심볼,참조,호출 관계를 추출해 SQLite에 정규화된 그래프 형태로 저장하는 것입니다. 노드는 함수/클래스/모듈 같은 심볼이고, 엣지는 호출(calls), 참조(references), 상속(extends) 같은 관계입니다. 여기에 SQLite의 FTS5(Full-Text Search) 가상 테이블을 얹어서 심볼명 검색을 즉시 처리합니다.
기존 에이전트가 코드를 탐색할 때는 보통 Explore 서브 에이전트가 grep → 파일 후보 추리기 → Read로 본문 읽기 → 또 grep 하는 식으로 반복합니다. 이 과정 하나하나가 도구 호출이고 컨텍스트 윈도우를 갉아먹습니다. CodeGraph의 smart_context 도구는 이걸 한 번의 호출로 압축합니다. "이 심볼과 관련된 진입점, 호출자, 피호출자, 그리고 실제 코드 스니펫"을 그래프 조인 한 방으로 반환합니다.
또 하나 중요한 설계는 항상 최신 상태 유지(Always Fresh) 입니다. 인덱스가 오래되면 에이전트가 잘못된 정보를 받게 되니까요. CodeGraph는 OS 네이티브 파일 시스템 이벤트(macOS의 FSEvents, Linux의 inotify)를 구독하고, 디바운싱을 걸어서 변경된 파일만 부분 재파싱합니다. 전체 재인덱싱이 아니라 델타만 갱신하기 때문에 큰 모노레포에서도 부담이 적습니다.
이걸 AWS에 올리는 구도는 "EC2 1대 + EFS 마운트 + ALB 뒤에 MCP 엔드포인트" 형태로 잡았습니다. 컴퓨트는 t3.medium EC2(개인 PoC)에 ARM 기반 Graviton(t4g.medium)도 비교해봤습니다. 코드베이스는 EFS에 두고 여러 개발자가 동일한 인덱스를 공유합니다. CodeRepo는 CodeCommit이 아니라 GitHub을 그대로 두고, EventBridge로 푸시 이벤트를 받아 EC2 위 동기화 스크립트가 git pull을 트리거하도록 했습니다. SQLite DB 파일도 EFS에 두긴 했지만 이 부분은 뒤에서 트레이드오프로 다시 다룹니다.
CodeGraph의 핵심 동작을 개념적으로 단순화하면 다음과 같은 모양입니다. 실제 내부 스키마와는 다를 수 있지만, 심볼-관계 그래프의 구조를 이해하는 데는 이 정도면 충분합니다.
-- 심볼 노드: 함수, 클래스, 모듈 단위
CREATE TABLE symbols (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT, -- function, class, method 등
file_path TEXT,
start_line INTEGER,
end_line INTEGER
);
-- 관계 엣지: 호출, 참조, 상속
CREATE TABLE edges (
src_id INTEGER,
dst_id INTEGER,
rel TEXT, -- calls, references, extends
FOREIGN KEY(src_id) REFERENCES symbols(id)
);
-- FTS5: 심볼명 즉시 검색용 가상 테이블
CREATE VIRTUAL TABLE symbols_fts USING fts5(name, content='symbols');
(B) Deep Dive: 주요 이점 및 고려 사항
테스트 시나리오로 동일한 질문 10개를 두 번 던졌습니다. 첫 번째는 일반 Claude Code, 두 번째는 CodeGraph MCP를 붙인 Claude Code. 질문은 "X 함수의 영향 반경", "Y 라우트 핸들러가 호출하는 외부 서비스", "Z 클래스 계층" 같은 탐색형 질의였습니다. 결과는 도구 호출 수가 평균 11회에서 3.4회로 줄었고, 응답까지 걸린 시간도 체감으로 절반 정도였습니다. 글에서 주장한 70% 감소가 실제 체감과 크게 다르지 않았습니다.
특히 인상적이었던 건 Impact Analysis 도구였습니다. 실무에서 리팩토링을 할 때 가장 두려운 순간은 "이 함수 시그니처를 바꾸면 어디까지 깨지지?"를 판단하는 시점입니다. 보통은 grep으로 직접 호출처를 찾고, 그 호출처를 또 누가 호출하는지 한 단계 더 파고, 테스트 파일에서 이 함수를 모킹하고 있는 곳은 없는지 확인하는 식으로 3~4단계를 반복합니다. 코드베이스가 크면 이 과정에서 빠뜨리는 곳이 생기고, 그게 프로덕션 장애로 이어지기도 합니다.
CodeGraph의 Impact Analysis는 그래프에서 N홉 BFS(너비 우선 탐색)를 돌려서 한 번에 영향 반경을 반환합니다. 예를 들어 formatDate라는 유틸 함수의 파라미터를 바꾸고 싶다면, "이 함수를 직접 호출하는 곳(1홉) + 그 호출자를 다시 호출하는 곳(2홉) + 관련 테스트 파일"을 한 번의 도구 호출로 트리 형태로 보여줍니다. 단순히 목록만 나열하는 게 아니라 호출 깊이와 파일 경로, 라인 번호까지 포함되어 있어서 에이전트가 바로 수정 계획을 세울 수 있었습니다. 변경 전 영향 분석에 들어가는 사고 비용이 확실히 줄었고, 무엇보다 "빠뜨린 곳이 없다"는 확신을 그래프 구조가 보장해준다는 점이 심리적으로 큰 차이였습니다.
위 스크린샷처럼, codegraph impact 한 번이면 직접 호출처(1홉)부터 간접 호출처(2홉 이상)까지 트리로 펼쳐줍니다. 단순 텍스트 검색(grep)과 달리 호출 그래프 기반이라 런타임에 실제로 영향받는 심볼만 정확히 잡아내는 것이 핵심입니다.
또 하나 체감이 컸던 건 smart_context의 코드 스니펫 반환 방식입니다. 기존에는 에이전트가 파일 전체를 읽어서 컨텍스트 윈도우를 낭비하는 경우가 많았는데, CodeGraph는 심볼의 시작 라인과 끝 라인을 알고 있으니 해당 함수 본문만 정확히 잘라서 반환합니다. 500줄짜리 파일에서 20줄짜리 함수만 필요한 상황이라면 컨텍스트 절약 효과가 극적입니다.
반면 한계도 분명했습니다. 첫째, 동적 호출과 메타프로그래밍에는 약합니다. Python의 getattr 기반 디스패치나TypeScript의 Proxy를 통한 호출은 정적 분석으로는 잡히지 않습니다. 그래프가 비어있는 게 아니라, "이 코드는 자주 직접 호출되지 않네?"라는 잘못된 신호를 줄 수 있습니다. 에이전트가 그래프를 과신할 때 오히려 위험할 수 있다는 뜻입니다.
+실제로 테스트 중 데코레이터 패턴으로 감싸진 핸들러 함수가 그래프에서 "호출자 없음"으로 나타나는 케이스를 발견했고, 이런 경우에는 여전히 grep 기반 탐색이 보완적으로 필요했습니다.
둘째, AWS 운영 관점에서는 SQLite 동시 쓰기 문제가 가장 신경 쓰였습니다. EFS에 SQLite 파일을 두고 여러 EC2에서 마운트하면, EFS의 NFS 락 의미론과 SQLite의 파일 락이 충돌해서 쓰기 경합이 발생합니다. 결국 인덱서는 단일 EC2에서만 동작시키고, 다른 인스턴스는 읽기 전용으로만 EFS를 마운트하는 구조로 정리했습니다. WAL 모드를 켰지만 NFS 위에서는 권장되지 않아서 일반 rollback journal 모드로 되돌렸습니다.
셋째, 콜드 스타트 비용입니다. 12만 라인 모노레포 첫 인덱싱이 t3.medium에서 약 6분 걸렸습니다. Graviton(t4g.medium)으로 옮기니 4분대로 줄었지만, 어쨌든 EC2를 끄고 켤 때마다 인덱싱을 다시 하면 안 되니 EFS에 DB를 영속화하는 게 필수였습니다. 그 대신 EFS의 IOPS 비용이 추가됩니다. PoC 한 달 비용을 따져보니 EC2(t4g.medium 24시간 운영) 약 $25, EFS(약 2GB + IOPS) $5 정도로 개인 부담은 가벼운 편이었습니다.
넷째, MCP 서버를 외부에 노출할 때의 보안입니다. CodeGraph는 100% 로컬을 전제로 만들어졌기 때문에 인증 레이어가 없습니다. 팀에 공유하려면 ALB 앞단에 OIDC 인증을 붙이거나, 최소한 VPN/Tailscale 같은 사설 네트워크 안에서만 접근하도록 막아야 합니다. 저는 PoC라 ALB + Cognito OIDC 조합으로 막아뒀습니다.
(C) 아키텍처 트레이드오프
가장 먼저 고민한 건 Lambda vs EC2였습니다. MCP 서버는 stdio 또는 long-lived HTTP 연결로 도는 게 자연스럽고, 파일 시스템 이벤트 구독이라는 동작 모델이 Lambda와 맞지 않았습니다. 파일 시스템 워처가 살아있어야 하니 상시 실행되는 컴퓨트가 필요했고, 결국 EC2를 택했습니다. ECS Fargate도 후보였지만, 파일 시스템 이벤트가 컨테이너 레이어 위에서 어떻게 동작할지 검증할 시간이 부족해서 단순한 EC2로 갔습니다.
다음은 EFS vs EBS 선택입니다. EBS가 IOPS도 좋고 SQLite 친화적이지만, 저는 "팀원이 자기 노트북에서도 동일한 코드베이스 마운트해서 보고 싶을 수 있다"는 시나리오를 가정했기 때문에 EFS를 골랐습니다. 대신 인덱서는 단일 라이터 정책을 강제했고, EFS Provisioned Throughput은 켜지 않고 Bursting 모드로 운영했습니다. 12만 라인 정도면 Bursting 크레딧으로 충분했습니다. 만약 코드베이스가 100만 라인 단위라면 EBS gp3 + AMI 스냅샷으로 가는 게 맞다고 봅니다.
세 번째는 Git 동기화 방법입니다. cron으로 5분마다 git pull을 돌릴까, GitHub Webhook → API Gateway → Lambda → SSM Run Command로 이벤트 기반으로 갈까 고민했습니다. 결국 EventBridge + SSM 조합으로 갔습니다. 푸시가 없을 때는 아무 일도 일어나지 않고, 푸시가 있을 때만 EC2에서 git pull이 실행되니 인덱서가 OS 이벤트를 자연스럽게 받아 부분 재인덱싱합니다. cron보다 깔끔했지만, 대신 처음 IAM 권한 잡는 데 시간이 좀 들었습니다.
Neptune이나 OpenSearch 도입은 과도한 인프라 비용과 불필요한 리소스 낭비를 초래하는 오버엔지니어링이라 판단하여 배제했습니다.
현재 규모에서 관리형 DB가 제공하는 확장성은 높은 유지 비용 대비 실질적인 ROI를 보장하지 못합니다. 콜드 스타트 없는 경량 아키텍처라는 본질에 집중하고 비용 효율성을 극대화하기 위해, SQLite 기반의 미니멈 아키텍처를 그대로 보존하는 전략을 채택했습니다.
EC2 위에 CodeGraph를 설치하는 user-data 스크립트는 이렇게 단순합니다. 한 줄 설치를 표방하는 만큼 부트스트랩이 가볍습니다.
#!/bin/bash
# EFS 마운트 (코드베이스 + DB 영속화 위치)
mkdir -p /mnt/code
mount -t efs fs-0abc123:/ /mnt/code
# CodeGraph 설치 (OS별 번들 자체 포함, Node.js 불필요)
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
# MCP 서버를 systemd 서비스로 상시 가동
cat <<EOF > /etc/systemd/system/codegraph.service
[Service]
ExecStart=/usr/local/bin/codegraph mcp --workspace /mnt/code/repo
Restart=always
EOF
systemctl enable --now codegraph
GitHub 푸시를 EC2 동기화로 연결하는 EventBridge 룰 설정은 이런 모양입니다. SSM Run Command를 타깃으로 잡았습니다.
{
"Source": ["aws.partner/github.com/myorg"],
"DetailType": ["GitHub Push Event"],
"Targets": [{
"Arn": "arn:aws:ssm:ap-northeast-2::document/AWS-RunShellScript",
"RoleArn": "arn:aws:iam::123456789012:role/EventBridgeSSMRole",
"RunCommandParameters": {
"RunCommandTargets": [{"Key": "tag:Role", "Values": ["codegraph"]}]
},
"Input": "{\"commands\":[\"cd /mnt/code/repo && git pull\"]}"
}]
}
MCP 클라이언트 쪽(예: Claude Code) 설정은 ALB 엔드포인트를 가리키도록 합니다. Cognito 인증을 붙였기 때문에 토큰을 헤더로 넘깁니다.
{
"mcpServers": {
"codegraph": {
"url": "https://codegraph.internal.example.com/mcp",
"headers": { "Authorization": "Bearer ${COGNITO_ID_TOKEN}" }
}
}
}
Summary & Final Checklist
핵심 결정을 요약하면 이렇습니다.
MCP 서버는 상시 실행이 필요하므로 EC2(가능하면 Graviton).
인덱스 DB는 EFS에 영속화하되, 단일 라이터 정책 유지.
Git 동기화는 EventBridge + SSM으로 이벤트 기반.
외부 노출은 ALB + OIDC 인증으로 최소 보호.
Neptune 같은 매니지드 그래프 DB로의 치환은 의도적으로 거부.
원활한 PoC를 위한 체크리스트입니다.
EC2 보안 그룹은 ALB에서 오는 트래픽만 허용.
EFS 보안 그룹은 EC2 SG에서 2049 포트만 허용.
SQLite는 NFS 위에서 WAL 모드 끄기.
인덱서 EC2는 IMDSv2 강제, SSM 에이전트 활성화.
Cognito 토큰 만료 시간을 짧게 잡고 클라이언트에서 갱신.
Conclusion: Lessons Learned
가장 의외였던 건 "정적 분석 그래프 하나로도 에이전트의 행동 패턴이 이렇게 달라지나"였습니다. 그동안 RAG나 임베딩 기반 검색에만 관심이 쏠려 있었는데, 코드처럼 강한 구조가 있는 도메인에서는 임베딩보다 그래프가 더 정확한 컨텍스트를 적은 토큰으로 줄 수 있다는 걸 확인했습니다. 한국에서도 코드 어시스턴트 도입이 늘면서 토큰 비용이 슬슬 부담되는 시점인데, 이런 결정론적 인덱스 레이어가 다음 단계로 자연스럽게 보입니다.
다음에 다시 한다면 두 가지를 바꾸겠습니다.
첫째, EFS 대신 EBS gp3 + 일일 스냅샷으로 가서 SQLite의 동시성 이슈를 아예 회피하겠습니다. 팀 공유는 어차피 ALB 뒤 단일 인스턴스로도 충분합니다.
둘째, 인덱서를 ECS Fargate로 옮기는 실험을 해보겠습니다. 파일 시스템 이벤트가 컨테이너 안에서 잘 작동하는지만 확인되면 운영 부담이 더 줄어들 것 같습니다.
마지막으로 댓글에서 누군가 "그래프류가 너무 많아진다"고 했는데, 그 말도 일리는 있습니다. 다만 CodeGraph처럼 외부 의존성 없이 SQLite 한 파일로 끝나는 디자인은 PoC 비용이 거의 0에 가깝다는 게 큰 장점입니다. 일단 붙여보고 안 맞으면 떼면 그만이라는 점, 이게 도구 선택에서 의외로 중요한 요소라는 걸 다시 느꼈습니다.


Top comments (0)