-
이 이름이 가리키는 진짜 그곳 — 코드 심볼을 구문에서 의미로 풀어내기IT 2026. 7. 6. 23:00
배경: "이름"은 생각보다 모호하다
코드를 읽다가
process라는 함수를 만났다고 하자. 나는 자연스럽게 묻게 된다. "이 process는 도대체 어디에 정의돼 있지? 그리고 누가 이걸 호출하지?"IDE에서는 함수 이름을 Ctrl 누르고 클릭하면 정의로 점프(go-to-definition)하고, 오른쪽 클릭으로 "모든 참조 찾기(find-references)"를 누른다. 너무 당연해서 마법처럼 느껴지지도 않는다. 그런데 이 마법을 여러 저장소(repo)에 걸쳐, 수많은 파일에서, 사람 없이 자동으로 해내려고 하면 갑자기 매우 어려운 문제가 된다.
핵심은 이렇다. 소스 코드에서
process라는 글자(텍스트)는 그냥 일곱 글자일 뿐이다. 이 글자가 가리키는 진짜 대상(심볼, symbol — 코드 안에서 이름이 붙은 정의 단위)이 무엇인지는 글자만 봐서는 알 수 없다. 같은 이름이 프로젝트 안에 수십 개 있을 수 있고, 그중 단 하나만이 "지금 이 자리의 process"가 가리키는 진짜 정의다. 이 글은 그 "글자 → 진짜 대상" 연결을, 그리고 몇 번째 줄에 있는지 라인 단위로 찾아내는 길을 따라가 본 학습 정리다.문제: 글자만 봐서는 진짜를 못 짚는다
가장 소박한 방법 — grep
"process가 어디 있나 찾아줘"라고 하면 가장 먼저 떠오르는 건
grep이다. 파일 전체에서process라는 글자를 모조리 찾아 나열한다. 빠르고 단순하다.문제는 grep이 글자만 본다는 것이다. 의미를 모른다. 비유하자면, 전화번호부에서 "김민수"를 검색했더니 동명이인이 40명 나오는 상황이다. 내가 찾는 김민수가 누구인지 전화번호부는 모른다. 코드에서도 똑같은 일이 벌어진다.
- 동명 함수(같은 이름, 다른 정의): 결제(payments) 모듈의
process()와 이미지(imaging) 모듈의process()는 완전히 다른 함수인데 grep은 둘 다 토해낸다. - 재export(re-export — A 파일이 B의 함수를 가져와 다시 내보내는 것): 진짜 정의는 한쪽에 있는데 import 줄에도 같은 이름이 등장한다. grep은 어느 게 "진짜 집"인지 모른다.
- 주석·문자열 안의 단어: 주석에 적힌
process나 로그 메시지"process failed"속 글자까지 잡아온다.
다이어그램 설명. 같은 질문에 두 방법이 어떻게 다르게 답하는지를 보여준다. 글자 매칭 방식은 이름만 같으면 결제·이미지 모듈, 재export, 주석 속 단어까지 전부 후보로 올려놓고 "이 중 하나일 거예요"라며 손을 든다. 반면 의미 해석 방식은 "지금 이 자리에서 보이는 범위"가 어디까지인지, 즉 스코프(scope — 이름이 유효한 영역)를 추적하고 import 경로를 끝까지 따라가서 딱 하나의 진짜 정의와 그 정확한 줄 번호를 짚어준다. 함정은, 글자 매칭은 빠른 대신 "이 중 진짜가 무엇인지"라는 가장 중요한 질문에 끝내 답하지 못한다는 점이다.
조금 똑똑한 휴리스틱도 한계
"같은 폴더 우선", "import 줄을 대충 파싱" 같은 휴리스틱(heuristic — 경험적 어림짐작)을 얹으면 grep보다는 낫다. 하지만 이건 결국 추측이다. 언어의 진짜 규칙(스코프 규칙, 모듈 해석 규칙)을 흉내 낼 뿐이라 까다로운 경우에서 틀린다. 그리고 무엇보다 "틀릴 수 있다"는 사실 자체가 문제다. "이 함수가 호출되는 모든 곳"을 안다고 주장하려면 단 하나도 놓치거나 더하면 안 되기 때문이다.
정리하면, 자동화된 코드 이해 시스템에 필요한 건 두 가지다. 첫째 정확성 — 글자가 아니라 의미로 "이 이름 = 저 정의"를 안다. 둘째 라인 앵커(line anchor) — 단순히 "그 파일에 있어요"가 아니라 "그 파일 142번째 줄, process 심볼"이라고 검증 가능한 위치를 준다. 이 두 가지가 지금부터 풀어갈 목표다.
해결: 글자 → 구문 → 의미로 한 층씩 내려간다
핵심 직관 — 깊이가 곧 정확도다
코드를 이해하는 데에는 깊이의 층위가 있다. 가장 얕은 곳이 글자다 — grep이 머무는 자리, 의미를 모른다. 그 위 한 층이 구문(syntax)이다 — "이건 함수 정의이고, 이름은 process, 여기서 import가 일어난다"까지는 안다. 가장 깊은 곳이 의미(semantics)다 — "지금 이 자리의 process가 가리키는 정의는 저기, 그리고 그것을 호출하는 곳은 여기들"이라고 안다.
내가 만들고 있는 코드 위키 자동생성 시스템은 이 깊이를 단계적으로 내려가도록 짜여 있다. 먼저 구문을 다언어 구문 파서로 뽑고, 그다음 파이썬에 한해 의미까지 풀어낸 뒤, 그 결과를 검증 가능한 그래프로 굳힌다. 핵심 발상은 질문할 때마다 즉석에서 분석하지 말고, 한 번 결정적으로 분석해 그래프 DB로 구워두자는 것이다.
다이어그램 설명. 이 글 전체이자 내 시스템의 파이프라인을 한 줄로 요약한다. 글자 매칭은 의미를 모르고 동명 심볼을 혼동한다. 구문 파싱 단계가 함수·모듈·import를 구조로 뽑아내고, 다시 의미 해석 단계가 "이 함수를 누가 부르는가"라는 호출 관계를 풀어낸다. 마지막으로 그 결과를 라인 앵커가 달린 그래프 DB로 굳혀, 누가 언제 물어도 검증 가능한 답을 즉시 내준다. "글자"에서 "검증된 의미"로 나아가며, 함정은 구문을 안다고 해서 의미까지 아는 것은 아니라는 점이다 — 구조를 아는 것과 이름이 무엇을 가리키는지 아는 것은 다른 층위다.
Stage A — 구문 파서로 함수·모듈·import를 뽑는다 (tree-sitter)
첫 단계는 코드의 구문 구조를 뽑는 것이다. 여기서 쓰는 도구가 tree-sitter다. tree-sitter는 소스 코드를 빠르게 파싱해 문법 구조(구문 트리)를 만들어주는 다언어 파서다. 내 시스템은 이걸로 각 소스 파일을 훑어 함수 정의, 모듈, 그리고 모듈 사이의 import 의존(DEPENDS_ON) 관계를 추출한다.
tree-sitter를 고른 이유는 여러 언어를 같은 방식으로 다룰 수 있어서다. 내 시스템은 파이썬뿐 아니라 타입스크립트(TypeScript), 자바스크립트(JavaScript), C#, PHP, C용 문법까지 같은 추출기로 처리한다. 언어마다 "무엇을 함수로 볼 것인가"만 따로 지정해주면(예: 파이썬은 함수 정의, 타입스크립트는 함수 선언과 메서드 정의), 나머지 골격은 공유된다. 단, tree-sitter는 여기까지만 한다 — "이 process 호출이 어느 정의를 가리키는지"라는 의미 해석은 tree-sitter의 일이 아니다. 그건 다음 단계의 몫이다.
Stage B — 파이썬 의미를 풀어 호출 관계를 만든다 (jedi)
두 번째 단계는 파이썬 코드에 한해 의미까지 내려간다. 여기서 쓰는 도구가 jedi다. jedi는 파이썬 전용 정적 분석 라이브러리로, IDE 자동완성·정의 점프의 엔진으로 널리 쓰인다. 내 시스템은 Stage A가 찾아둔 함수 하나하나에 대해, jedi에게 "이 함수의 정의가 여기 있다, 그리고 이 이름이 쓰이는 모든 곳을 찾아줘"라고 물어본다.
구체적으로는 jedi의 정의 점프(goto)로 함수 정의 위치를 확정한 뒤, 참조 찾기(get_references)로 그 함수가 호출·사용되는 모든 지점을 받아온다. 그리고 각 참조 지점이 어느 함수 안에 들어 있는지를 역으로 찾아, "부른 쪽(caller) → 불린 쪽(callee)"이라는 호출 관계(CALLS)를 그래프에 엣지로 추가한다. 글자 매칭이 결코 줄 수 없던, 의미에 근거한 호출 그래프가 이렇게 만들어진다.
다만 정직하게 짚어둘 한계가 있다. 이 의미 해석은 파이썬에 한정되고, 호출 관계는 같은 저장소 안에서만 잇는다. 저장소 경계를 넘나드는 연결은 다른 방식(서브프로세스 호출, 알려진 포트로의 HTTP 호출 같은 신호를 구문 단계에서 잡는 식)으로 따로 다룬다. jedi는 컴파일러가 아니므로 동적 파이썬의 까다로운 부분에서는 놓치는 경우도 있고, 그래서 분석에 시간 제한을 두고 부분 실패를 허용하도록 설계했다.
그래프 DB로 결정적으로 굽는다 — 라인 앵커가 핵심
Stage A·B가 만든 노드(함수·모듈·심볼)와 엣지(호출·의존 관계)는 그래프 자료구조로 모인 뒤 디스크에 영속화된다. 내 시스템은 그래프 라이브러리(networkx)로 메모리에서 그래프를 다루고, 그걸 SQLite 파일로 저장한다. 무거운 외부 그래프 데이터베이스를 도입하는 대신, 저장소 규모(노드 수만 개)에 충분하고 다루기 쉬운 이 조합을 선택했다.
여기서 가장 중요한 두 가지 설계가 노드 ID와 라인 앵커다.
모든 노드는
저장소@커밋해시::언어::심볼경로형태의 ID를 갖는다. 저장소가 다르거나, 같은 저장소라도 커밋이 다르거나, 심볼 경로가 다르면 ID가 달라진다. 이 ID는 그래프 DB와 벡터 검색, 그리고 위키 출력 경로를 한데 묶는 공통 정합 키 역할을 한다 — 같은 함수면 어디서든 같은 ID로 가리킬 수 있다.그리고 위키의 모든 사실 주장에는
경로/파일.py:줄번호, 더 나아가경로/파일.py:줄번호:심볼이름형태의 라인 앵커가 달린다. "이 함수가 저 모듈을 호출한다"라고 문서가 주장하면, 그 주장 옆에는 "그 근거는 이 파일 이 줄, 이 심볼"이라는 검증 가능한 좌표가 따라붙는다. 검증기는 이 앵커가 실제로 그 줄에 진짜 코드(심볼)를 가리키는지 기계적으로 확인하고, 코드가 움직여 줄 번호가 어긋나면(앵커가 빈 줄이나 주석을 가리키면) 드리프트로 잡아낸다. 이 게이트가 없으면, 한때 맞았던 줄 번호가 코드 이동 뒤 거짓이 되어 미래의 분석을 오염시킨다.효과와 한계
무엇이 가능해지나
이 파이프라인의 결과물은 한마디로 의미에 근거하고 라인 단위로 검증되는 코드 지도다. 같은 코드에서 항상 같은 그래프가 나오므로(결정적), 추출 단계에서는 추측도 환각도 끼어들 여지가 없다. 동명 함수를 ID로 구별하고, 호출 관계를 의미로 잇고, 모든 주장에 검증 가능한 좌표를 단다.
한계
- 의미 해석은 파이썬 전용: 구문 추출은 여러 언어를 다루지만, 호출 관계까지 푸는 의미 해석은 현재 파이썬에 한정된다. 다른 언어는 함수·모듈 골격까지만 잡힌다.
- 같은 저장소 안의 호출만: 저장소 경계를 넘는 정밀한 호출 관계는 의미 단계가 아니라 구문 단계의 신호로 근사한다.
- 정적 분석의 본질적 한계: 실행해야만 정해지는 동적 코드는 정적 분석이 일부 놓칠 수 있다. 그래서 시간 제한과 부분 실패 허용을 전제로 운영한다.
- 인덱스 신선도: 그래프는 "한 시점의 사진"이라, 코드가 바뀌면 언제 다시 구울지가 운영상 과제다. 변경된 저장소만 골라 다시 처리하는 방식으로 비용을 줄인다.
내 코드 위키 자동생성 시스템과의 연결
코드를 읽어 설명 문서를 자동으로 써내는 이 시스템에서, 모든 서술은 "실제로 그 코드 어디에 있는가"로 검증 가능해야 한다. "이 함수가 저 모듈을 호출한다"라고 문서가 주장하려면, 글자 매칭의 어림짐작이 아니라 의미에 근거한 정의·참조 관계가 뒷받침되어야 한다.
그래서 내 시스템은 즉석 질의 방식을 택하지 않고, 결정적으로 그래프 DB를 구워두는 길을 택했다. 구문 파서로 함수·모듈·import를 뽑고(tree-sitter), 파이썬 의미 분석으로 호출 관계를 풀고(jedi), 공통 정합 키가 달린 노드와 라인 앵커가 달린 그래프로 굳힌다. 이렇게 굳혀두면 같은 코드에서 같은 지도가 나오고, 위키의 모든 주장이 라인 앵커로 검증되며, 사실과 다른 설명을 기계적으로 걸러내는 게이트를 세울 수 있다. 이 토대가 없으면 자동 생성 문서는 "그럴듯하지만 검증 불가능한 글"에 그치고, 있으면 "코드에 단단히 묶인 신뢰할 수 있는 지도"가 된다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
에이전트가 지금 살아 있나 — OpenClaw가 흩어진 상태를 한 화면(Overview)에 모은 방법 (0) 2026.07.09 근거가 이 문장을 정말 뒷받침하나 — 로컬 NLI로 매일 $0 사실 검증 (0) 2026.07.08 문장을 잘게 쪼개 검증한다 — Claim 분해와 앵커 없는 주장 걸러내기 (0) 2026.07.07 AI가 '지어낸다'는 말을 정확히 나누기 — 할루시네이션의 분류학 (0) 2026.07.07 모듈이 바깥에 한 약속 — 공개 인터페이스를 코드에서 뽑아 표로 만들기 (0) 2026.07.07 이 함수를 누가 부르는가 — jedi로 호출 관계를 의미 수준에서 풀기 (0) 2026.07.06 언어가 하나 늘 때 코드를 고치지 않으려면 — 표준 tags.scm 쿼리로 다언어 추출하기 (0) 2026.07.05 코드를 글자가 아니라 구조로 읽기 — tree-sitter로 함수와 모듈을 뽑아내기 (0) 2026.07.05 코드를 그래프로 본다는 것 — 호출 그래프부터 코드 속성 그래프까지 (0) 2026.07.04 코드를 그래프로 질문하기 — 코드 속성 그래프(CPG) 한 번에 이해하기 (0) 2026.07.04 - 동명 함수(같은 이름, 다른 정의): 결제(payments) 모듈의