-
코드를 그래프로 본다는 것 — 호출 그래프부터 코드 속성 그래프까지IT 2026. 7. 4. 22:00
집에 작은 AI 서버를 한 대 두고, 내 코드들을 읽어 설명 문서(위키)를 자동으로 써 주는 도구를 만들며 놀고 있다. 그러다 한 가지를 절실히 깨달았다. 코드를 "그림(그래프)"으로 바꿔 보면 평소 안 보이던 것이 보인다. 그런데 같은 코드라도 "어떤 그림으로 그리느냐"에 따라 보이는 게 완전히 달라진다.
어떤 그림은 "누가 누구를 부르는가"만 보여주고, 어떤 그림은 "이 값이 어디로 흘러가 무슨 일을 일으키는가"까지 보여준다. 이 글은 그 그림들의 스펙트럼을 얕은 것부터 깊은 것까지 차근차근 풀어 본다.
왜 코드를 "그래프"로 보게 됐나
프로그램은 결국 글자의 나열이다. 하지만 도구가 코드를 분석하려면 글자 그대로 보는 것으로는 부족하다. "이 함수가 저 함수를 부른다", "이 변수의 값이 저기로 흘러간다" 같은 관계를 알아야 하기 때문이다.
비유하자면, 도시를 이해하는 지도는 여러 가지다. 어떤 지도는 도로망만, 어떤 지도는 지하철 노선만, 어떤 지도는 상하수도 배관만 그린다. 같은 도시지만 무엇을 그리느냐에 따라 답할 수 있는 질문이 다르다. 도로 지도로는 "여기서 저기까지 차로 갈 수 있나"를 답하지만, "여기서 물을 잠그면 어느 집이 단수되나"는 배관 지도가 있어야 답한다.
코드도 똑같다. 프로그램을 그래프(노드와 엣지로 이뤄진 그림 — 노드(node)는 '점', 엣지(edge)는 '점과 점을 잇는 선')로 표현하는 방법은 여러 가지이고, 각각이 답할 수 있는 질문이 다르다.
다이어그램 설명. 위 그림은 같은 소스 코드를 두고도 우리가 던지는 질문이 여러 종류라는 것을 보여준다. "누가 이 함수를 부르나" 같은 단순 연결 질문부터 "이 값이 어디로 흘러가나" 같은 속뜻(의미) 질문까지 폭이 넓다. 핵심은 이 질문들이 하나의 만능 그래프로 다 풀리지 않는다는 점이다 — 질문마다 그에 맞는 그래프 표현이 따로 있다. 흔히 "콜그래프 하나면 되지 않나" 생각하기 쉬운데, 맨 아래 값 흐름 질문은 콜그래프로는 손도 못 댄다.
얕은 그래프만으로는 답할 수 없는 질문이 있다
코드 분석에서 가장 흔하고 만들기 쉬운 두 그래프는 호출 그래프와 의존 그래프다. 둘 다 "구조"와 "연결" — 즉 누가 누구와 이어져 있는가를 답한다.
- 호출 그래프(Call Graph): 노드는 함수, 엣지는 호출 관계. "A 함수가 B 함수를 부른다"를 화살표로 그린다.
- 의존 그래프(Dependency Graph): 노드는 모듈·패키지·빌드 단위, 엣지는 의존 관계. "이 모듈이 저 모듈을 가져다(import) 쓴다"를 그린다.
이 둘은 분명 유용하다. "이 함수를 고치면 어떤 함수들이 영향받을까?"를 대략 짚어 준다. 하지만 한계가 분명하다. 이들은 연결의 존재 여부만 알지, 그 연결 안에서 무슨 일이 벌어지는지는 모른다.
예를 들어 어떤 함수가 사용자 입력을 받아 여러 단계를 거친 뒤 위험한 명령 실행 함수로 그 값을 넘긴다고 하자. 호출 그래프는 "입력 받는 함수 → … → 명령 실행 함수"로 화살표가 이어진다는 것만 보여준다. 하지만 그 입력값이 실제로 명령 실행까지 그대로 흘러가는지, 아니면 중간에 안전하게 검증·정제되는지는 알 수 없다. 화살표는 "통로가 있다"만 말하지, "그 통로로 무엇이 흐르는가"는 말하지 않기 때문이다.
다이어그램 설명. 위 그림은 호출 그래프의 한계를 그대로 드러낸다. 입력을 받는 함수, 중간 처리 함수, 명령을 실행하는 함수가 화살표로 이어져 있다는 사실은 알 수 있다. 그러나 그 사이로 위험한 값이 정말 끝까지 흘러가는지, 아니면 중간 처리 단계에서 안전하게 걸러지는지는 이 그래프만으로는 판단할 수 없다. 즉 "연결됨"은 알아도 "흐름"은 모른다는 게 얕은 그래프의 근본적 한계다. 보안 점검에서 특히 치명적인데, "통로가 있다"는 사실만으로는 그 통로가 실제 공격 경로인지 가려낼 수 없기 때문이다.
점점 깊어지는 그래프들
핵심 아이디어를 직관으로 먼저 말하면 이렇다. 그래프를 깊게 만들수록, "연결" 질문에서 "의미(흐름)" 질문으로 답할 수 있는 범위가 올라간다. 얕은 그래프는 도시의 도로 지도, 깊은 그래프는 "이 수도꼭지를 틀면 물이 어느 경로로 어느 집까지 가는가"를 그린 배관 흐름도라고 보면 된다. 얕음에서 깊음 순서로 보자.
(1) 제어흐름 그래프 (CFG, Control-Flow Graph)
한 함수 내부의 실행 순서와 분기를 그린다. if 문에서 갈라지고 반복문에서 되돌아오는 경로를 노드와 엣지로 표현한다. 호출 그래프가 "함수들 사이"를 본다면, 제어흐름 그래프는 "함수 한 개 안쪽"을 현미경으로 들여다본다. "이 코드에서 어떤 실행 경로가 가능한가"를 답한다.
(2) 데이터흐름 그래프 (DDG, Data-Flow / Data Dependence Graph)
"한 곳에서 만든 값이 다른 곳의 변수에 영향을 준다"는 관계를 그린다. 즉 이 값이 어디서 와서 어디로 흘러가나를 추적한다. 앞서 본 "사용자 입력이 명령 실행까지 정말 흘러가는가" 질문에 답할 수 있는 게 바로 이 그래프다.
(3) 프로그램 의존 그래프 (PDG, Program Dependence Graph)
데이터 의존(값의 흐름)과 제어 의존(실행이 어떤 조건 아래 일어나는가)을 하나로 합친 그래프. "이 줄을 바꾸면 영향받는 부분만 골라낸다" 같은 작업(프로그램 슬라이싱, 영향 분석)의 기반이 된다.
(4) 코드 속성 그래프 (CPG, Code Property Graph)
구문 트리(AST, Abstract Syntax Tree — 코드의 문법 구조를 나무 모양으로 표현한 것)에 더해 제어흐름과 프로그램 의존을 하나의 거대한 그래프로 통합해, 그 위에서 질의(쿼리)할 수 있게 만든 것. 표현력이 가장 높다. 이 개념은 Yamaguchi 등의 논문 "Modeling and Discovering Vulnerabilities with Code Property Graphs"(IEEE S&P 2014)에서 제안됐는데, 출발 동기 자체가 보안 취약점을 그래프 질의로 찾자는 것이었다.
다이어그램 설명. 위 그림은 그래프들을 얕음에서 깊음으로 층층이 쌓은 스펙트럼이다. 맨 위의 호출 그래프와 의존 그래프는 "누가 누구와 이어지는가"라는 구조·연결 질문에 답한다. 아래로 내려가 제어흐름과 데이터흐름, 프로그램 의존을 거쳐 맨 아래 코드 속성 그래프에 이르면 "이 변경이 어떤 값과 경로로 파급되는가"라는 의미·흐름 질문에 답할 수 있게 된다. 놓치기 쉬운 함정은 "아래로 갈수록 무조건 좋다"는 생각이다 — 아래로 갈수록 표현력은 커지지만 그래프를 만드는 비용도 함께 커진다. 그래서 늘 맨 아래층을 택하는 게 답은 아니다.
같은 코드, 다른 그래프
같은 코드 한 조각이 각 그래프에서 무엇을 노드와 엣지로 보는지 표로 나란히 놓아 보자.
그래프 노드(점) 엣지(선) 답하는 질문 호출 그래프 함수 "부른다" 누가 이 함수를 부르나 의존 그래프 모듈·패키지 "가져다 쓴다(import)" 무엇이 무엇에 의존하나 제어흐름 그래프 문장 블록(코드 한 덩어리) "다음에 실행될 수 있다" 한 함수 안 실행 경로는 데이터흐름 그래프 값을 만들거나 쓰는 지점 "이 값이 저기에 영향 준다" 이 값이 어디로 흐르나 코드 속성 그래프 위 모두를 통합한 노드 종류 표시된 선(문법·실행·값흐름) 골라가며 무엇이든 표 설명. 이 표는 똑같은 코드를 다섯 가지 그래프 렌즈로 들여다봤을 때 무엇이 점이 되고 무엇이 선이 되는지를 나란히 보여준다. 호출 그래프에서는 함수가 점이고 "부른다"가 선이지만, 데이터흐름 그래프에서는 값을 만들거나 쓰는 지점이 점이고 "이 값이 저기에 영향을 준다"가 선이 된다. 같은 코드라도 무엇을 점과 선으로 삼느냐에 따라 보이는 세계가 완전히 달라진다는 것이 핵심이다. 맨 아래 코드 속성 그래프는 이 모든 렌즈를 한 그래프에 합쳐, 필요할 때 원하는 종류의 선만 골라 따라가며 질문할 수 있게 만든 것이다.
이걸 실제로 해 주는 도구들
- Joern: 오픈소스 코드 속성 그래프 엔진. C/C++/Java/Python 등을 파싱해 그래프 데이터베이스로 올린 뒤 질의한다.
- CodeQL(GitHub): 소스 코드를 관계형 데이터베이스로 "컴파일"한다. 구문 트리·데이터흐름·제어흐름·타입 계층·호출 그래프를 모두 담아, 오염 추적(taint — 위험한 입력이 어디까지 번지는지 따라가는 분석) 같은 데이터흐름 질의를 한다.
- Semgrep: 파일·함수 경계를 넘는 데이터흐름과 오염 추적을 지원한다.
무엇이 좋아졌고, 무엇이 한계인가
깊은 그래프가 생기면서 답할 수 있는 질문의 차원이 달라졌다. 대표적으로 "함수의 인자(매개변수) 형태를 바꿨을 때 무슨 일이 벌어지나"를 보자.
다이어그램 설명. 위 그림은 "함수의 인자 형태를 바꾼다"는 하나의 변경이 두 그래프에서 얼마나 다르게 보이는지를 비교한다. 호출 그래프만 있으면 그 함수를 부르는 쪽의 목록까지는 알 수 있지만, 바뀐 값이 각 호출처 안에서 어디로 흘러 무엇을 깨뜨리는지는 알 수 없어 "여기 영향이 있을 수도 있다" 수준에 머문다. 반면 데이터흐름 그래프가 있으면 바뀐 값이 어떤 변수로 받아져 어떤 계산과 분기를 거쳐 최종적으로 어디까지 흘러가는지를 지점 단위로 짚어낸다. 단순 연결 정보와 흐름 정보의 차이가 곧 분석 정밀도의 차이다.
한계와 트레이드오프도 분명하다.
- 구축 비용: 코드 속성 그래프는 표현력이 가장 높은 대신 만드는 작업이 무겁다. 깊이 파싱하고 여러 그래프를 통합해야 하므로 시간·자원이 많이 든다.
- 동적 언어의 정밀도 한계: 파이썬(Python)처럼 실행 중 타입·동작이 유연하게 바뀌는 동적 언어에서는, 미리 코드만 보고 데이터흐름을 정확히 추적하기 어렵다.
- 현실적 절충: 그래서 실무에서는 풀(full) 코드 속성 그래프를 다 만들기보다, "함수 시그니처(이름·인자·반환 형태) + 한 단계(1-hop) 데이터흐름" 정도만 가볍게 뽑아 쓰는 경량 추출이 흔하다.
코드 위키를 자동 생성하는 일에 왜 핵심인가
코드를 읽고 사람이 이해할 문서를 자동으로 써 주는 일에서, "코드를 어떤 그래프로 보느냐"는 결과물의 깊이를 직접 좌우한다. 얕은 그래프만 가진 시스템은 "이 모듈이 저 모듈을 쓴다", "이 함수가 저 함수를 부른다" 수준의 구조 설명까지만 자신 있게 쓸 수 있다. 반면 "이 설정값을 바꾸면 어떤 동작이 어디까지 달라지는가" 같은 의미·파급 질문에 답하려면 더 깊은 흐름 그래프가 필요하다.
현실적으로는 표현력 최고의 통합 그래프를 모든 코드에 무겁게 구축하는 것이 비용 대비 효율이 낮고, 동적 언어에서는 정밀도까지 떨어진다. 그래서 자동 문서화 시스템은 보통 가장 얕은 두 층(연결·구조)을 안정적으로 확보하고, 필요한 곳에만 가벼운 흐름 정보를 덧대는 절충을 택한다. 이 스펙트럼을 이해하면 자동 생성 문서가 "여기까지는 자신 있게 말할 수 있고, 그 이상은 흐름 분석이 있어야 한다"는 경계를 의식적으로 그을 수 있다. 그 경계 인식이 곧 신뢰할 수 있는 문서와 과장된 문서를 가르는 기준이 된다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
모듈이 바깥에 한 약속 — 공개 인터페이스를 코드에서 뽑아 표로 만들기 (0) 2026.07.07 이 이름이 가리키는 진짜 그곳 — 코드 심볼을 구문에서 의미로 풀어내기 (0) 2026.07.06 이 함수를 누가 부르는가 — jedi로 호출 관계를 의미 수준에서 풀기 (0) 2026.07.06 언어가 하나 늘 때 코드를 고치지 않으려면 — 표준 tags.scm 쿼리로 다언어 추출하기 (0) 2026.07.05 코드를 글자가 아니라 구조로 읽기 — tree-sitter로 함수와 모듈을 뽑아내기 (0) 2026.07.05 코드를 그래프로 질문하기 — 코드 속성 그래프(CPG) 한 번에 이해하기 (0) 2026.07.04 한 문장이 다른 문장에 기대는 두 가지 방식 — 프로그램 의존 그래프(PDG) (0) 2026.07.03 값은 어디서 와서 어디로 가는가 — 데이터흐름 그래프(DDG)로 코드 읽는 법 배우기 (0) 2026.07.03 코드는 일직선이 아니다: 제어흐름 그래프(CFG)로 함수 속 길을 그려보기 (0) 2026.07.03 스킬의 끝은 스크립트인가 — 하네스 엔지니어링에서 스크립트는 '지능을 파낸 빈자리'다 (0) 2026.07.02