-
graphify는 LLM도 임베딩도 쓰지 않는다 — 답을 만들지 않기로 한 코드 검색 도구의 설계IT 2026. 8. 20. 21:00

▶ 동영상 개요 — graphify는 LLM도 임베딩도 쓰지 않는다 — 답을 만들지 않기로 한 코드 검색 도구의 설계
8분 47초 — 「graphify는 LLM도 임베딩도 쓰지 않는다 — 답을 만들지 않기로 한 코드 검색 도구의 설계」의 내용을 훑는다 — NotebookLM 동영상 개요로 생성 🎧 오디오 개요 — graphify는 LLM도 임베딩도 쓰지 않는다 — 답을 만들지 않기로 한 코드 검색 도구의 설계
15분 53초 — 「graphify는 LLM도 임베딩도 쓰지 않는다 — 답을 만들지 않기로 한 코드 검색 도구의 설계」의 내용을 훑는다. 화면은 이 글의 인포그래픽 한 장 고정 — NotebookLM 오디오 개요로 생성 코딩 에이전트에게 "이 프로젝트에서 검증 로직이 어떻게 동작해?"라고 물으면, 화면 뒤에서 벌어지는 일은 대체로 이렇다. 키워드로 저장소를 검색해 40군데쯤 히트를 얻고, 그중 그럴듯해 보이는 파일 여덟 개를 읽고, 여섯 개는 헛다리였다는 걸 확인한 뒤 답을 만든다. 답 자체는 꽤 괜찮다. 문제는 그 다음이다. 대화창을 닫으면 방금 알아낸 "아, 이 함수가 핵심이었구나"가 통째로 사라진다. 내일 같은 질문을 하면 처음부터 다시 40군데를 뒤진다.
여기에 눈에 잘 안 띄는 두 번째 문제가 겹쳐 있다. 텍스트 검색은 관계를 볼 수 없다. "이 함수를 누가 부르는가"를 알고 싶어도 함수 이름으로 검색하는 것 말고는 방법이 없고, 다른 이름으로 가져다 쓰는 경우는 놓친다. 반대로 이름만 같고 아무 상관 없는 것들이 딸려 온다. 코드의 의미는 글자가 얼마나 닮았는지가 아니라 무엇이 무엇을 부르는지에 있는데, 텍스트 검색은 그 축을 아예 보지 못한다.
이 두 문제를 정면으로 겨냥한 도구가 graphify다. 코드베이스를 함수·클래스 단위의 지식 그래프로 바꿔 코딩 에이전트에게 넘겨 주는 Apache-2.0 오픈소스로, 올해 4월에 첫 커밋이 올라와 넉 달 만에 깃허브 별 10만 개를 넘겼다. 그런데 흥미로운 건 기능 목록이 아니라 graphify가 쓰지 않기로 한 것들이었다. 요즘 이런 문제에 당연하게 동원되는 세 가지 — 대형 언어 모델, 임베딩 검색, 그리고 "답변 생성" — 을 전부 거부한다. 실제로 내 프로젝트에 v0.9.37을 돌려 보면서, 그 거부 하나하나가 무엇을 얻고 무엇을 포기하는 거래인지 따라가 봤다.
먼저, graphify가 만드는 것
명령 한 번을 실행하면 프로젝트 폴더에 파일 세 개가 생긴다. 코드 안의 함수·클래스·파일을 노드로, 그것들 사이의 호출·임포트(다른 파일의 코드를 가져다 쓰는 것)·상속 관계를 엣지로 만든 지도다. 하나는 브라우저에서 클릭해 돌아다니는 시각화, 하나는 사람이 읽는 요약 보고서, 하나는 기계가 읽는 지도 원본이다.
핵심은 이 파일들이 대화가 끝나도 디스크에 남는다는 데 있다. 모델의 기억이 아니라 산출물이라서, 내일도 모레도 그대로 있다. 앞서 말한 첫 번째 문제 — 매 세션 처음부터 다시 발견하는 것 — 은 여기서 해결된다.
다이어그램 설명. graphify의 전체 흐름이자, 앞으로 이야기할 세 가지 거부가 어디에 걸려 있는지 보여주는 지도다. 눈여겨볼 곳은 "파일 종류는?"에서 갈라지는 두 갈래다. 코드는 문법이 정해져 있으니 파서(구문 분석기)가 정확히 읽어낼 수 있고, 그래서 모델을 한 번도 부르지 않는다. 반대로 문서나 PDF는 "이 문단이 저 개념을 참조한다"는 관계가 문법으로 표현돼 있지 않으니 모델이 읽어 주는 수밖에 없다. 흔히 "AI가 코드를 이해해서 그래프를 만든다"고 오해하기 쉬운데, 코드 경로에서 AI는 아예 등장하지 않는다. 이 갈림길을 잡아 두면 나머지 설계가 전부 여기서 파생된다는 게 보인다.
거부 하나 — 코드에는 모델을 쓰지 않는다
코드 추출은 tree-sitter(소스 코드를 구문 트리로 바꿔 주는 파서 라이브러리, 40개 넘는 언어를 지원한다)가 전담한다. 파일을 파싱해 구문 트리를 걷다가, 함수 정의를 만나면 노드를 만들고 그 안에서 다른 함수를 부르는 지점을 만나면 엣지를 만든다. 실제로 뽑히는 모양은 이렇다.
노드: resolve_anchor() [src=scripts/validate_grounding.py, loc=L142] 노드: load_graph() [src=graph/store.py, loc=L31] 엣지: resolve_anchor() --calls--> load_graph() [EXTRACTED]예시 설명. 노드 하나에 "어느 파일 몇 번째 줄"이 반드시 따라붙고, 엣지에는 관계의 종류(여기서는 호출)와 신뢰도 딱지가 붙는다. 이게 왜 중요하냐면, 이 지도의 모든 주장이 원본 소스의 특정 줄로 되짚어질 수 있다는 뜻이기 때문이다. 모델이 개입하지 않았으니 지어낸 관계가 섞일 여지가 없고, 같은 코드를 열 번 돌리면 열 번 같은 지도가 나온다.
얻는 것은 명확하다. 비용이 0원이고, 인터넷이 없어도 되고, 소스가 기계 밖으로 나가지 않고, 결과가 매번 같다. 100만 줄짜리 저장소도 API 요금 없이 통째로 훑을 수 있다.
포기하는 것도 그만큼 명확하다. 파서는 문법에 적힌 것만 본다. 변수에 함수를 담아 두었다가 나중에 부르거나, 문자열로 이름을 만들어 호출하는 식의 동적인 코드는 구문 트리에 흔적이 남지 않는다. 파서는 그런 관계를 영영 볼 수 없다.
여기서 graphify가 택한 태도가 마음에 들었다. 모르는 걸 아는 척하지 않고 자료구조에 자백할 자리를 미리 파 두었다. 모든 엣지에 세 등급 중 하나가 붙는다 — 소스에 명시적으로 적혀 있으면
EXTRACTED, 파서가 추론해 이은 것이면INFERRED, 확신이 서지 않으면AMBIGUOUS. 내 프로젝트에서 실제로 나온 비율은 명시 83% · 추론 17% · 불확실 0%였다. 불확실 등급이 한 번도 쓰이지 않았다는 것도 그 자체로 정보다. 이 저장소에서는 파서가 애매하다고 판단한 관계가 없었다는 뜻이니까.보고서 맨 위에 이 비율을 그대로 노출한다는 점이 결정적이다. "이 지도의 17%는 추론입니다"라고 먼저 말해 주는 것과, 전부 사실인 척 늘어놓는 것은 도구를 대하는 사람의 태도를 바꾼다.
거부 둘 — 임베딩과 벡터 데이터베이스를 쓰지 않는다
공식 문서가 대놓고 강조하는 문장이 있다. "벡터 인덱스가 아니다(Not a vector index)." 임베딩(문장이나 코드를 숫자 배열로 바꿔 의미가 비슷한 것끼리 가깝게 놓는 기법)도, 그걸 저장하는 벡터 데이터베이스도 쓰지 않는다는 선언이다.
요즘 "코드베이스에 질문하기"를 만든다고 하면 거의 반사적으로 나오는 구성이 임베딩 검색이다. 그래서 이 거부는 유행을 거스르는 쪽에 가깝다. 대신 graphify가 쓰는 건 질문에서 뽑은 단어와 노드 이름을 맞춰 출발점을 잡고, 거기서부터 지도를 따라 이웃으로 퍼져 나가는 방식이다. 지하철 노선도에서 한 역을 찍고 두 정거장 이내를 훑는 것과 같은 그림이다.
얻는 것은 인덱싱 비용이 0원이고, 임베딩 모델을 내려받거나 서버를 띄울 필요가 없고, 같은 질문에 항상 같은 결과가 나온다는 점이다. 특히 마지막이 크다. 임베딩 검색은 모델을 바꾸면 결과가 조용히 달라지는데, 이쪽은 그런 흔들림이 없다.
포기하는 건 의미적 유사도다. "인증"이라고 물었을 때 이름에
auth가 안 들어간 관련 함수를 데려오는 능력이 없다. 지도를 따라 이어져 있으면 찾고, 안 이어져 있으면 못 찾는다. 구조적으로 가까운 것과 의미적으로 가까운 것은 다른 축인데, graphify는 앞의 축만 본다. 실제로 돌려 보니 이 대가가 눈에 보이는 지점이 있었는데, 그건 따로 다룰 만한 이야기라 여기서는 접어 둔다.거부 셋 — 답을 만들지 않는다
이게 가장 반직관적이면서, graphify의 정체를 결정하는 부분이다. 지도에 질문을 던지면 무엇이 돌아오는지 실제 출력을 보자. 내 프로젝트 하나에 지도를 만들고 "검증이 어떻게 동작하나"라고 물은 결과다.
Traversal: BFS depth=2 | Start: ['GroundingReport', 'grounding_ok()', ...] | 151 nodes found NODE GroundingReport [src=scripts/validate_grounding.py loc=L107 community=9] NODE grounding_ok() [src=scripts/generate_narrative_pages.py loc=L425 community=92] NODE scan_wiki_dir() [src=scripts/validate_grounding.py loc=L307 community=9] NODE .drift_rate() [src=scripts/validate_grounding.py loc=L119 community=9] ... [!] TRUNCATED: showing 26 of 151 nodes (~900-token budget).출력 설명. 문장이 하나도 없다. 관련 있어 보이는 심볼의 이름과, 각각이 어느 파일 몇 번째 줄에 있는지, 그리고 어느 뭉치에 속하는지가 전부다. 설명은커녕 요약도 하지 않는다. 마지막 줄도 정직한데, 예산을 넘겨서 151개 중 26개만 보여 주고 있다고 스스로 밝힌다. 검색 결과를 조용히 잘라 놓고 다 보여준 척하는 도구가 흔한 걸 생각하면 이건 꽤 드문 태도다.
그러니까 graphify가 파는 것은 요약이 아니라 "읽을 파일 목록을 좁혀 주는 것"이다. 답은 여전히 에이전트가 만든다. 다만 여덟 개 파일을 헤매는 대신, 지목된 서너 곳만 펼쳐 보고 만든다.
이 설계가 왜 좋은지는 환각(모델이 사실이 아닌 것을 그럴듯하게 만들어 내는 현상)의 관점에서 보면 분명해진다. 요약을 만들지 않으니 요약이 틀릴 일이 없다. 모든 출력이 파일과 줄 번호라서, 미심쩍으면 그 자리를 열어 보면 끝난다. 검증 비용이 거의 0에 가까운 형태로 답을 내놓는 것이다.
대가는 graphify 혼자서는 아무것도 설명해 주지 않는다는 점이다. 사람이 직접 쓰면 심볼 목록만 잔뜩 보게 된다. 이건 사람용 도구가 아니라 에이전트가 읽기 전에 거쳐 가는 계층이다.
두 워크플로를 나란히 놓으면
세 가지 거부가 합쳐지면 작업 흐름이 어떻게 달라지는지, 앞뒤를 나란히 두고 보는 게 가장 빠르다.
▲ 기존 방식 — 매번 처음부터
다이어그램 설명. 텍스트 검색으로 시작하는 익숙한 흐름이다. 눈여겨볼 곳은 두 군데다. 하나는 "파일 8개 읽기 — 6개는 헛다리" 단계로, 읽은 것의 대부분이 그대로 버려진다. 토큰과 시간이 여기서 새어 나간다. 다른 하나는 "대화 종료" 뒤에 점선으로 되돌아가는 화살표다. 이번에 알아낸 것이 다음 세션에 아무것도 남기지 못하기 때문에, 같은 질문을 하면 같은 40군데 검색이 처음부터 반복된다. 이 순환이 이 방식의 진짜 비용이다.
▲ 지도를 놓은 방식 — 한 번 만들고 계속 쓴다
다이어그램 설명. 같은 질문이 지나가는 경로인데 두 가지가 달라졌다. 첫째, "지도 구축" 단계가 흐름 바깥에 한 번만 있고 질문 때마다 반복되지 않는다. 둘째, 점선 화살표가 돌아가는 지점이 질문 단계이지 지도 구축 단계가 아니다. 즉 다음 세션은 지도가 이미 있는 상태에서 시작한다. 이 두 그림의 차이를 한 문장으로 줄이면, 발견 비용을 매 세션 치르느냐 한 번만 치르느냐다. 다만 오해하기 쉬운 지점이 있는데, 코드가 바뀌면 지도도 낡는다. 그래서 graphify는 커밋 훅(git이 커밋할 때 자동으로 실행하는 스크립트)을 걸어 백그라운드에서 지도를 갱신하는 경로를 따로 준비해 뒀다. 내 환경에서 재빌드는 5초 안팎이었으니 부담스러운 수준은 아니다.
절반은 파서, 절반은 프롬프트
마지막으로 짚고 싶은 게 있다. graphify를 설치하면 무슨 일이 일어나는지 보고 나서 생각이 좀 바뀌었다.
설치 명령은 파이썬 패키지만 깔지 않는다. 사용자가 쓰는 코딩 에이전트를 찾아내 각각의 설정 디렉토리에 지시문 파일을 심는다. 지원 목록이 16종에 이른다. 그리고 프로젝트의 규칙 파일에 이런 문단을 끼워 넣는다 — "코드베이스에 대한 질문이 오면 먼저 지도에 질의하라. 지도 요약본은 질의로 부족할 때만 읽어라. 코드를 고쳤으면 지도를 갱신하라."
여기서 문제의 성격이 드러난다. 아무리 잘 만든 지도를 놓아 둬도, 에이전트가 습관대로 텍스트 검색을 하면 그 지도는 없는 것과 같다. 좋은 인덱스를 만드는 것과 그 인덱스가 실제로 쓰이게 만드는 것은 서로 다른 일이고, 후자는 코드가 아니라 프롬프트의 영역이다. 지시문 본문을 읽어 보면 이 싸움의 흔적이 남아 있다. 예컨대 "API 키를 절대 묻지 마라, 키가 필요하다고 판단해 멈추려는 자신을 발견하면 그건 이 지시문을 잘못 읽은 것이다" 같은, 모델의 예상 오작동을 미리 막는 문장이 박혀 있다.
에이전트를 대상으로 도구를 만든다는 건 이런 것이구나 싶었다. 사람에게는 문서를 주면 되지만, 에이전트에게는 행동 규칙을 심어야 하고, 그 규칙이 모델의 기존 습관과 경쟁해야 한다.
정리 — 무엇을 안 쓸지가 그 도구의 정체다
기능 목록만 훑었다면 graphify를 "코드 지식 그래프 도구" 한 줄로 정리하고 넘어갔을 것이다. 그런데 안 쓰기로 한 것들을 따라가 보니 훨씬 또렷한 그림이 나왔다. 모델을 안 쓴 대가로 결정론과 0원을 얻고 동적인 코드를 잃었다. 임베딩을 안 쓴 대가로 흔들리지 않는 결과를 얻고 의미적 유사도를 잃었다. 답을 안 만든 대가로 환각이 낄 자리를 없애고 "혼자서는 아무것도 설명하지 못하는 도구"가 됐다.
세 거래가 전부 같은 방향을 가리킨다. graphify는 똑똑해지는 대신 검증 가능해지는 쪽을 골랐다. 요즘 이 영역의 기본값이 "모델을 하나 더 끼워 넣어 똑똑하게 만든다"인 걸 생각하면, 반대 방향으로 밀어붙여 10만 스타를 받은 사례가 있다는 것 자체가 신호로 읽힌다.
내 작업에 옮겨 적을 교훈은 이거다. 도구를 평가할 때 기능 목록보다 거부 목록을 먼저 본다. 무엇을 할 수 있는지는 대개 비슷비슷하지만, 무엇을 하지 않기로 했는지에는 만든 사람이 실제로 무엇을 중요하게 여겼는지가 드러난다.
참고한 공개 자료:
- graphify (Apache-2.0 오픈소스) — https://github.com/Graphify-Labs/graphify
- 본문의 실측 수치는 v0.9.37(2026-08-08 기준 커밋)을 내려받아 개인 프로젝트 하나(코드에서 문서를 생성하는 파이썬 도구, 코드 파일 127개)에 직접 돌려 얻은 결과다. 다른 저장소에서는 다른 숫자가 나올 수 있다.
- tree-sitter — https://tree-sitter.github.io/tree-sitter/
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
링크를 본문에 쓸까 메타데이터에 쓸까 — 발행되는 글이라면 답이 갈린다 (0) 2026.08.19 관계에 이름을 붙이려면 — 문서 종류를 먼저 선언해야 하는 이유 (0) 2026.08.18 개체는 사람만이 아니다 — 개념을 1급 개체로 올려야 그래프가 생긴다 (0) 2026.08.17 같은 문제를 푼 두 설계 — 남이 만든 에이전트 기억과 내가 만든 지식 금고 검색 (0) 2026.08.16 위키링크가 곧 엣지다 — LLM 호출 없이 지식 그래프를 짓는 방법 (0) 2026.08.16 스타 12만 개짜리 프롬프트 뭉치 — gstack 논쟁이 실제로 드러낸 것 (0) 2026.08.15 웹을 읽는 에이전트에 방어막을 씌운다는 것 — 프롬프트 인젝션 6겹 해부 (0) 2026.08.15 gstack을 석 달 방치했더니 — 1.26과 1.60 사이에 늘어난 스킬 9개 (0) 2026.08.15 책을 미리 증류할까, 그때그때 찾을까 — 벡터 DB를 직접 굴려 본 사람이 본 '스킬 컴파일'의 자리 (0) 2026.08.14 책을 스킬로 만드는 순간 PDF는 데이터가 아니라 지시문이 된다 — 문서에서 에이전트로 이어지는 새 공격면 (0) 2026.08.14