-
이 함수를 누가 부르는가 — jedi로 호출 관계를 의미 수준에서 풀기IT 2026. 7. 6. 21:00
배경: 함수 목록만으로는 "관계"를 말할 수 없다
나는 여러 코드 저장소(repo)를 읽어 설명 문서(코드 위키)를 자동 생성하는 개인 프로젝트를 만들고 있다. 첫 단계로 구문 파서를 써서 "이 저장소에 어떤 함수와 모듈이 어디에 있는가"라는 목록을 결정적으로 만들어 두었다. 함수 하나하나가 노드(node — 그래프의 마디)가 되고, "저장소가 모듈을, 모듈이 함수를 소유한다"는 포함 관계까지 그래프에 들어왔다.
그런데 위키가 정말로 유용해지려면 한 걸음 더 나아가야 한다. "이 주문 처리 핸들러가 GPU를 확보하는 함수를 호출한다" 같은 관계를 말할 수 있어야 한다. 함수가 어디 있는지 아는 것과, 그 함수가 무엇을 부르고 누구에게 불리는지를 아는 것은 다른 차원이다. 그리고 이 호출 관계가 있어야 "이 함수를 바꾸면 어디가 영향받지?" 같은 질문에 답할 수 있다.
다이어그램 설명. 구문 추출이 끝난 시점의 상태를 보여준다. 함수와 모듈의 목록은 손에 있지만, 그것들이 서로 어떻게 호출하는지를 잇는 선이 아직 없다. 그래서 "이 핸들러가 호출하는 함수들" 같은 가장 실용적인 질문에 답하지 못한다. 함정은 목록만 있어도 위키가 그럴듯해 보인다는 점인데, 정작 코드를 이해하는 데 가장 중요한 "무엇이 무엇을 부르는가"가 비어 있으면 그 위키는 절반짜리다.
문제: 같은 이름의 함수, 어느 정의를 가리키나
호출 관계를 만들려면 "이 자리의 함수 호출이 가리키는 진짜 정의"를 정확히 찾아야 한다. 이게 생각보다 어렵다. 코드에는 같은 이름의 함수가 수두룩하기 때문이다.
예를 들어 결제(payments) 모듈에도
process()가 있고 이미지(imaging) 모듈에도process()가 있다고 하자. 어느 코드에서process()를 호출하면, 그게 둘 중 어느 정의를 부르는 것인지는 이름만 봐서는 알 수 없다. 지금 이 자리에서 어떤 import가 일어났는지, 어떤 범위(스코프)가 보이는지를 따져야 비로소 정해진다. 이 판단을 컴파일러는 정확히 해내지만, 글자 매칭(grep)이나 구문 파싱만으로는 못 한다.다이어그램 설명. 이름의 모호함이 어떻게 해소되는지 보여준다. 같은
process()라는 호출이 두 정의 후보를 두고 있지만, 지금 이 파일에서 무엇을 import했고 어떤 이름이 보이는지를 따지면 둘 중 하나로 확정된다. 핵심은 이 확정이 "글자가 같은가"가 아니라 "의미상 무엇을 가리키는가"의 문제라는 점이다. 함정은, 추측성 휴리스틱(예: 같은 폴더 우선)으로 대충 고르면 까다로운 경우에서 틀리는데, 호출 관계는 하나만 어긋나도 "이 함수가 영향 주는 모든 곳"이라는 주장이 깨진다.해결: in-process 의미 분석으로 호출을 잇는다 (jedi)
왜 jedi인가 — 무거운 서버 대신 라이브러리
이 의미 해석을 위해 택한 도구가 jedi다. jedi는 순수 파이썬(pure Python)으로 된 정적 분석 라이브러리로, 많은 코드 에디터의 자동완성·정의 점프 기능을 떠받치는 엔진이기도 하다. 후보가 여럿이었지만 내 작업의 성격에 jedi가 가장 맞았다.
도구 특징 채택 jedi 순수 파이썬 라이브러리(~15MB). 별도 프로세스 없이 코드 안에서 직접 호출. 정확도 중상. 채택 풀 타입 검사기 정확도는 최상이지만 다른 런타임(Node.js) 의존 + 100MB대. 배치 추출엔 과함. 거절 언어 서버 데몬 별도 프로세스를 띄우고 통신해야 함. 관리 부담과 왕복 지연. 거절 구문 질의(tree-sitter) 가장 빠르지만 이름 해석이 없어 "어느 정의를 부르는지"를 못 짚는다. 거절 표 설명. 같은 일(호출 관계 추출)을 두고 네 가지 길을 비교한 것이다. 정확도만 보면 무거운 타입 검사기가 앞서지만, 사람 없이 밤마다 여러 저장소를 통째로 훑는 배치 작업에는 "다른 런타임을 깔고 100MB대 도구를 띄우는" 비용이 과했다. 별도 프로세스로 떠야 하는 언어 서버도 같은 이유로 부담이었다. 핵심은 "내 코드 안에서 함수처럼 바로 부를 수 있고(in-process), 의존성이 가볍고, 정확도가 충분한" 지점을 고른 것 — 그게 jedi였다. 함정은 가벼움을 택한 대가로 동적인 코드에서 일부 호출을 놓친다는 점인데, 이는 뒤에서 다룬다.
어떻게 호출 관계를 만드나
방법은 직관적이다. 구문 추출 단계가 찾아둔 함수 하나하나에 대해 jedi를 두 번 부른다. 첫째, 그 함수의
def위치를 가리켜 "이게 바로 이 함수의 정의다"라고 jedi 기준으로 확정받는다(정의 확정, goto). 이건 정의를 어딘가에서 새로 찾는 게 아니라, 우리가 이미 아는 그 자리를 jedi가 인정하는 정확한 기준점(줄·칸)으로 못 박는 단계다. 둘째, 그 기준점에서 "이 함수가 쓰이는 모든 곳을 찾아 달라"고 요청한다(참조 찾기, get_references). 그런 다음 각 참조가 어느 함수 안에서 일어났는지를 역으로 찾아, "부른 쪽(caller) → 불린 쪽(callee)"이라는 호출 관계(CALLS)를 그래프에 엣지로 추가한다.다이어그램 설명. 함수 하나로부터 호출 관계가 만들어지는 흐름이다. 정의를 확정하고, 그 이름이 쓰인 자리를 모두 모은 뒤, 각 자리가 어느 함수 안에 들어 있는지를 거슬러 올라가 "누가 이 함수를 불렀는지"를 알아낸다. 핵심은 "사용처 → 그 사용처를 감싼 함수"라는 역추적이다 — 이게 있어야 단순한 "여기서 쓰임"이 "이 함수가 저 함수를 호출함"이라는 방향 있는 관계로 승격된다. 함정은 참조 중에 정의 그 자체나 자기 자신 호출이 섞여 들 수 있다는 점인데, 그런 경우는 걸러내고 진짜 호출만 남긴다.
다이어그램 설명. 완성된 호출 관계가 그래프에서 어떤 모양인지 보여주는 작은 예시다. 주문 처리 핸들러가 GPU를 확보하는 함수와 응답을 직렬화하는 함수를 호출한다는 사실이, 방향 있는 두 개의 선으로 표현된다. 이런 선이 수천 개 모이면 "이 함수를 바꾸면 어디가 흔들리는가"를 따라갈 수 있는 지도가 된다. 핵심은 이 선들이 글자 매칭의 추측이 아니라 의미 해석으로 확정된 사실이라는 점이다.
일부러 보수적으로 — false positive 금지
중요한 원칙이 하나 있다. 파이썬은 매우 동적인 언어라, 실행 중에야 정해지는 호출(이름을 문자열로 조립해 부르거나, 데코레이터로 감싸거나 하는 경우)은 정적 분석이 다 잡지 못한다. 그래서 명시적으로 "놓치는 건 허용하되, 틀리게 잇는 건 금지"라는 정책을 택했다. jedi가 확신하지 못하면 차라리 엣지를 안 박는다.
다이어그램 설명. 정보의 신뢰도에 층위가 있다는 것을 보여준다. 구문 추출은 같은 코드면 같은 결과라 가장 믿을 수 있고, 의미 해석은 호출 관계까지 풀어내지만 동적인 코드 앞에서는 일부를 놓칠 수 있어 한 단계 낮다. LLM 서술은 가장 유연하지만 환각이 섞일 수 있어 별도 검증이 필요하다. 핵심은 "틀리게 잇느니 차라리 빠뜨린다"는 보수적 태도다 — 빠진 호출은 나중에 보강하면 되지만, 거짓 호출은 위키의 신뢰 자체를 무너뜨리기 때문이다.
같은 저장소 안의 호출만
의미 해석으로 잇는 호출 관계는 같은 저장소 안으로 한정했다. 저장소 경계를 넘나드는 연결(다른 서비스의 포트를 호출하거나 다른 프로젝트의 스크립트를 부르는 경우)은 의미 해석이 아니라 구문 단계에서 잡은 신호로 따로 다룬다. 함수 단위의 정밀한 의미 해석을 저장소 경계 너머까지 밀어붙이는 건 비용 대비 이득이 적다고 보고 범위를 좁혔다.
효과: 무엇이 가능해졌나
- 호출 그래프: "어느 함수가 어느 함수를 부르는가"가 의미 수준에서 그래프에 들어왔다. 스무 개 남짓의 저장소에서 약 3천 개의 호출 관계가 단 몇십 초 만에 추출됐다.
- 영향 범위 추적: "이 함수를 바꾸면 어디가 영향받지?"를 호출 관계를 따라 답할 수 있다.
- 가벼움: 별도 프로세스(데몬)도, 다른 런타임도 필요 없다. 내 파이썬 코드 안에서 함수처럼 바로 부른다. 구문 단계가 만들어 둔 함수 목록을 받아 참조만 조회하므로, 파일을 새로 훑는 비용도 없다.
- 신뢰할 수 있는 엣지: 보수적 정책 덕분에, 그래프에 박힌 호출 관계는 "확실한 것만"이다. 라인 앵커와 결합하면 호출이 일어난 자리까지 검증 가능해진다.
정직한 한계
- 파이썬 전용: 구문 추출은 여러 언어를 다루지만, 호출 관계까지 푸는 의미 해석은 현재 파이썬에 한정된다. 다른 언어는 함수·모듈 골격까지만 잡힌다.
- 동적 호출 누락: 실행해야 정해지는 호출은 놓칠 수 있다. 이는 정적 분석의 본질적 한계이고, 보수적으로 "확실한 것만" 남기는 대가다.
- 같은 저장소 안만: 저장소 경계를 넘는 정밀한 호출 관계는 의미 단계가 아니라 구문 단계의 신호로 근사한다.
내 코드 위키 자동생성 시스템과의 연결
코드를 읽어 문서를 자동으로 써내는 시스템에서, "이 함수가 저 함수를 호출한다"는 한 줄의 서술조차 글자 매칭의 어림짐작이 아니라 의미에 근거해야 한다. 그래야 위키가 코드의 실제 동작을 반영한다고 말할 수 있다.
그래서 내 시스템은 구문 추출(tree-sitter) 위에 의미 해석(jedi)을 한 층 더 얹었다. 구문 단계가 "어떤 함수가 어디 있는가"를 결정적으로 깔아 두면, jedi가 그 위에서 "무엇이 무엇을 부르는가"를 의미 수준으로 풀어내 호출 관계 엣지를 더한다. 무거운 언어 서버를 띄우는 대신 가벼운 라이브러리를 코드 안에서 직접 부르는 길을 택해, 사람 없이 도는 밤마다의 추출 작업에 딱 맞췄다. 구문이 코드의 뼈대라면, 의미 해석은 그 뼈대에 흐르는 핏줄을 그려 넣는 일이다 — 이 둘이 합쳐져야 비로소 "코드에 단단히 묶인, 따라가며 읽을 수 있는 지도"가 완성된다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
근거가 이 문장을 정말 뒷받침하나 — 로컬 NLI로 매일 $0 사실 검증 (0) 2026.07.08 문장을 잘게 쪼개 검증한다 — Claim 분해와 앵커 없는 주장 걸러내기 (0) 2026.07.07 AI가 '지어낸다'는 말을 정확히 나누기 — 할루시네이션의 분류학 (0) 2026.07.07 모듈이 바깥에 한 약속 — 공개 인터페이스를 코드에서 뽑아 표로 만들기 (0) 2026.07.07 이 이름이 가리키는 진짜 그곳 — 코드 심볼을 구문에서 의미로 풀어내기 (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 한 문장이 다른 문장에 기대는 두 가지 방식 — 프로그램 의존 그래프(PDG) (0) 2026.07.03