-
코드를 그래프로 질문하기 — 코드 속성 그래프(CPG) 한 번에 이해하기IT 2026. 7. 4. 21:00
들어가며 — 코드에게 "질문"을 던지고 싶었다
나는 정적 분석(static analysis: 프로그램을 실행하지 않고 소스 코드만 들여다보며 성질을 알아내는 기법)을 공부하면서 한 가지 욕심이 생겼다. 코드를 그냥 "읽는" 게 아니라, 검색 엔진에 검색어를 넣듯이 "사용자 입력이 검증 없이 위험한 함수까지 흘러가는 곳을 다 찾아줘" 같은 질문을 던지고 싶었다. 마치 도서관에서 "위험물 보관법을 어긴 책 다 가져와"라고 사서에게 부탁하는 느낌으로 말이다.
그런데 막상 해보니, 코드를 분석하는 도구들은 저마다 한 가지 측면만 답해 줬다. 문법을 묻는 도구는 실행 흐름을 모르고, 실행 흐름을 아는 도구는 값이 어디로 흐르는지 몰랐다. 이 글은 그 답답함을 풀어준 코드 속성 그래프(CPG, Code Property Graph)를 내가 이해한 대로 정리한 학습 노트다. 이 개념은 제어 흐름 → 데이터 흐름 → 프로그램 의존으로 점점 깊어지던 스펙트럼의 통합 종착점이다. 앞의 세 가지를 다 합친 것이 바로 CPG다.
CPG가 무엇인가 — 세 그래프를 하나로 겹친 지도
먼저 용어부터 한 줄씩 풀어두자.
약어 이름 한 줄 풀이 AST 구문 트리(Abstract Syntax Tree) 코드의 문법 구조를 나무 모양으로 표현한 것. "이 줄은 함수 호출이고, 그 안에 변수가 들어 있다" 같은 골격. CFG 제어흐름 그래프(Control Flow Graph) 프로그램이 실제로 실행될 때 어떤 순서·갈래로 문장이 이어지는지를 그린 그래프. PDG 프로그램 의존 그래프(Program Dependence Graph) "이 값이 저 계산에 영향을 준다" 같은 의존 관계만 추려 그린 그래프. CPG 코드 속성 그래프(Code Property Graph) 위 셋을 하나의 속성 그래프로 통합해, 그 위에서 질의할 수 있게 만든 것. 여기서 속성 그래프(property graph)란, 노드(점)와 엣지(선)에 종류와 속성을 붙일 수 있는 그래프를 말한다. 단순히 "A와 B가 연결됨"이 아니라, "A(함수 호출)와 B(변수)가 문법 관계로 연결됨" 또는 "값 흐름 관계로 연결됨"처럼 선마다 꼬리표를 단다.
CPG의 핵심 아이디어는 이렇다. 세 그래프가 공통의 노드를 공유하게 만든 다음, 그 위에 각 그래프의 엣지를 겹쳐(overlay) 올린다. 그리고 각 엣지에 "이건 문법 엣지", "이건 실행 흐름 엣지", "이건 값 흐름 엣지"라는 종류 라벨을 붙인다. 그러면 하나의 지도 위에서 원하는 종류의 선만 골라 따라갈 수 있다.
다이어그램 설명. 위쪽은 문법·실행 순서·값 의존을 각각 따로 답하던 세 그래프이고, 아래쪽은 이들을 공통 노드 위에 겹쳐 하나로 통합한 코드 속성 그래프다. 핵심 흐름은 "세 갈래의 정보가 한 지도로 모인다"는 것. 왜 합치냐면, 실제로 알고 싶은 질문이 한 그래프만으로는 답이 안 나오기 때문이다. 함정은 합쳤다고 정보가 사라지는 게 아니라 엣지 종류 라벨로 보존된다는 점 — 원할 때 다시 한 종류만 떼어 볼 수 있다.
배경 — "보안 취약점을 그래프 질의로 찾자"
CPG는 Yamaguchi, Golde, Arp, Rieck이 발표한 "Modeling and Discovering Vulnerabilities with Code Property Graphs"(IEEE Symposium on Security and Privacy, 2014)에서 제안됐다. 출발 동기 자체가 보안이었다. 취약점을 사람이 일일이 코드를 읽어 찾는 대신, 그래프 질의 한 줄로 패턴을 찾아내자는 발상이었다.
비유하자면, 예전에는 탐정이 사건 현장을 직접 발로 뛰며 단서를 모았다면, CPG는 모든 단서를 하나의 데이터베이스에 넣어 두고 "흉기가 부엌에서 거실로 옮겨진 사건만 보여줘"라고 한 줄로 검색하는 것에 가깝다. 단서가 따로 흩어져 있으면 이런 교차 질문이 불가능하다.
어떤 문제였나 — 취약점은 세 그래프에 "걸쳐" 있다
왜 굳이 합쳐야 했을까. 세 그래프는 각자 답할 수 있는 질문이 정해져 있다.
그래프 답할 수 있는 질문 답하지 못하는 질문 구문 트리(AST) 어디가 사용자 입력이고 어디가 위험한 호출인가? (문법) 그 둘이 실제 실행에서 이어지는가? 제어흐름 그래프(CFG) 이 문장에서 저 문장으로 실행이 도달하는가? (경로) 중간에 값이 정말 그쪽으로 흘러가는가? 프로그램 의존 그래프(PDG) 이 값이 저 계산에 영향을 주는가? (의존) 그게 문법적으로 어떤 호출이고 위험한가? 문제는 실제 취약점이 이 셋에 동시에 걸쳐 있다는 점이다. 대표적인 예가 이런 시나리오다 — "사용자가 입력한 값이, 정제(검증/무해화) 단계를 거치지 않고, 위험한 명령 실행 함수까지 흘러간다." 이 한 문장을 검증하려면 세 가지를 한꺼번에 봐야 한다.
다이어그램 설명. 하나의 취약점 질문이 문법·실행흐름·값흐름이라는 세 갈래의 하위 질문으로 갈라지고, 셋 모두 충족될 때만 진짜 취약점으로 판정되는 흐름을 보여준다. 왜 중요하냐면, 세 갈래 중 하나라도 따로 보면 가짜 경보가 쏟아진다 — 위험한 함수가 있어도 입력이 안 닿으면 무해하고, 경로가 있어도 값이 중간에 검증되면 안전하다. 함정은 셋을 따로 가진 도구로는 이 교차 조건을 한 번에 질의할 방법이 없었다는 것이다.
2014년 이전의 도구들은 이 세 그래프를 각각 따로 만들고 따로 분석했다. 그래서 "문법은 맞는데 실행 경로가 닿는지", "경로는 닿는데 값이 흐르는지"를 사람이 머릿속에서 짜맞춰야 했다. 교차 질문을 자동화할 단일한 데이터 구조가 없었던 것이다.
어떻게 해결했나 — 겹치고(overlay), 라벨 붙이고, 질의한다
CPG의 해결책은 세 단계로 요약된다.
1단계 — 공통 노드 위에 세 그래프를 겹친다
세 그래프가 사실 같은 코드를 다르게 본 것이므로, 같은 코드 요소(예: 한 변수, 한 함수 호출)는 하나의 노드로 공유한다. 그 공유 노드 위에 AST 엣지, CFG 엣지, PDG 엣지를 모두 올린다. 투명 필름 세 장을 같은 지도 위에 포개는 것과 같다.
2단계 — 엣지마다 종류 라벨을 붙인다
포개고 나면 선이 뒤섞이므로, 각 엣지에 "문법", "실행흐름", "값흐름" 같은 종류 라벨을 단다. 이게 속성 그래프의 힘이다. 질의할 때 원하는 종류의 선만 골라 따라갈 수 있다.
다이어그램 설명. 같은 세 노드 사이를 종류가 다른 엣지(값흐름·실행흐름·문법)가 동시에 잇는 모습으로, 한 지도 위에 세 겹의 정보가 라벨로 구분되어 공존함을 보여준다. 흐름은 입력을 읽어 변수에 담고 위험 호출로 이어지는 경로다. 왜 라벨이 핵심이냐면, 오염 추적(taint tracking)을 할 때는 값흐름 라벨만 따라가면 되고, 도달 가능성을 볼 때는 실행흐름 라벨만 따라가면 되기 때문이다. 함정은 라벨 없이 선만 합치면 어느 게 어떤 의미인지 구분할 수 없게 된다는 점.
3단계 — 그래프 데이터베이스에 올려 순회 질의한다
이렇게 만든 CPG를 그래프 데이터베이스(graph database: 데이터를 표 대신 노드·엣지 형태로 저장하는 DB)에 올린다. 그러면 순회(traversal) 질의 언어로 패턴을 찾을 수 있다. 순회 질의란 "이 노드에서 출발해 값흐름 엣지를 따라가다가, 중간에 검증 노드를 거치지 않고 위험 호출에 도달하는 경로"처럼 그래프를 걸어 다니며 조건을 거는 검색이다.
다이어그램 설명. 입력 노드에서 출발해 값흐름 엣지를 한 걸음씩 따라가며, 검증을 거쳤는지와 위험 호출에 도달했는지를 차례로 판단해 취약점 후보만 걸러내는 순회 질의의 흐름이다. 이것이 흔히 말하는 오염 추적(taint tracking) 스타일 — 신뢰할 수 없는 "오염된" 값이 어디까지 번지는지 따라가는 방식이다. 왜 좋냐면 사람이 머릿속으로 짜맞추던 교차 조건을 질의 한 줄로 표현하기 때문. 함정은 경로가 너무 많으면 질의가 느려질 수 있다는 점이라, 출발 노드를 잘 좁히는 게 중요하다.
효과 — 표현력은 최고, 비용도 최고
CPG의 가장 큰 효과는 취약점 탐지를 "그래프 질의"로 표현할 수 있다는 것이다. 앞서 본 오염 추적 스타일의 질문을 코드로 적어 두면, 같은 패턴의 취약점을 코드베이스 전체에서 자동으로 찾아낸다. 한 번 질의를 잘 써 두면 사람마다 다른 직관에 기대지 않고 일관되게 적용된다.
또 하나의 장점은 여러 언어에 적용 가능하다는 점이다. AST·CFG·PDG라는 개념 자체는 특정 언어에 묶이지 않으므로, 언어별 파서만 갖추면 같은 질의 모델을 재사용할 수 있다. 대표적인 오픈소스 도구로
Joern이 있다.다만 공짜는 아니다. 아래 표처럼 분석 모델이 깊어질수록 표현력은 커지지만 구축 비용도 함께 커진다. CPG는 그 스펙트럼의 끝에 있다 — 표현력은 최고지만 구축 비용도 가장 크다.
분석 모델 보는 것 표현력 구축 비용 구문 트리(AST) 문법 구조 낮음 낮음 제어흐름 그래프(CFG) 실행 순서 중간 중간 프로그램 의존 그래프(PDG) 값·제어 의존 높음 높음 코드 속성 그래프(CPG) 위 셋의 통합 최고 최고 다이어그램 설명. 문법만 보는 가벼운 모델에서 출발해, 실행 경로와 값 의존을 차례로 더해 마침내 셋을 통합한 모델에 이르는 점층적 스펙트럼을 보여준다. 흐름은 "정보가 한 겹씩 쌓이며 깊어진다"는 것. 왜 이 순서가 중요하냐면, 각 단계가 앞 단계 위에 정보를 더하는 구조라 통합 모델은 진짜로 앞의 셋을 모두 포함하기 때문이다. 함정은 끝으로 갈수록 빌드 시간·메모리 비용이 가파르게 오른다는 점 — 모든 코드에 무조건 최상위 모델을 쓰는 게 능사는 아니다.
코드 위키 자동 생성에 왜 핵심인가
마지막으로, 내가 이 개념에 끌린 진짜 이유. CPG는 보안 취약점 탐지에서 출발했지만, 그 본질은 "코드를 질의 가능한 그래프로 만든다"는 것이다. 이건 코드를 자동으로 설명·문서화하는 작업, 즉 코드 위키 자동 생성에 그대로 쓰인다.
코드 위키를 자동으로 쓰려면, 글쓰기 도구가 코드의 구조를 정확히 "알아야" 한다. 단순히 텍스트로 읽는 것과, "이 함수가 어떤 함수들을 부르고, 어떤 값이 어디서 와서 어디로 가는가"를 그래프로 질의하는 것은 정확도가 다르다. CPG가 있으면 다음 질문들에 사실 기반으로 답할 수 있다.
위키가 답해야 할 질문 CPG의 어떤 엣지로 답하나 이 모듈은 어떤 함수들로 이뤄져 있나? (구성) 문법 엣지(AST) 이 기능을 실행하면 어떤 경로로 흘러가나? (동작) 실행흐름 엣지(CFG) 이 설정값은 어디서 와서 어디에 쓰이나? (데이터 파이프라인) 값흐름 엣지(PDG) 다이어그램 설명. 진실의 원천인 소스 코드에서 통합 그래프를 짓고, 순회 질의로 검증 가능한 사실을 뽑아, 그 근거 위에서 글쓰기 도구가 서술을 작성하고 다시 코드 위치로 grounding하는 전체 파이프라인이다. 흐름의 핵심은 "코드 → 그래프 → 사실 → 서술"이라는 단방향 의존. 왜 핵심이냐면, 서술이 코드에서 직접 파생되므로 코드가 바뀌면 같은 파이프라인으로 다시 생성하면 되고, 모든 문장이 근거를 동반해 환각(hallucination: 사실이 아닌 내용을 그럴듯하게 지어내는 현상)을 줄인다. 함정은 그래프 구축 비용이라, 변경된 부분만 다시 인덱싱하는 식의 최적화가 현실적으로 필요하다.
정리하면, CPG는 "코드에게 구조화된 질문을 던지는 공통 기반"이다. 보안에서는 그 질문이 "취약점이 어디 있나"였고, 코드 위키에서는 "이 코드가 무엇을 어떻게 하나"가 된다. 질문만 다를 뿐, 코드를 질의 가능한 그래프로 만들어 두면 둘 다 같은 토대 위에서 풀린다는 점이 내게는 가장 큰 배움이었다.
마치며
처음의 욕심 — "코드에게 질문을 던지고 싶다" — 으로 돌아가 보면, CPG는 그 욕심에 대한 가장 완성된 답이다. 문법·실행흐름·값흐름을 한 지도에 겹쳐 두고 엣지 라벨로 구분하니, 흩어져 있을 때는 불가능했던 교차 질문이 질의 한 줄로 가능해졌다. 표현력의 끝인 만큼 구축 비용도 끝이라는 트레이드오프는 있지만, 코드를 "읽는 대상"이 아니라 "질의하는 데이터"로 바꿔 놓았다는 점에서, 정적 분석과 코드 인텔리전스(code intelligence: 코드의 구조·의미를 자동으로 이해해 활용하는 기술 전반)를 공부하는 사람이라면 한 번은 깊이 들여다볼 가치가 있는 개념이다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
이 이름이 가리키는 진짜 그곳 — 코드 심볼을 구문에서 의미로 풀어내기 (0) 2026.07.06 이 함수를 누가 부르는가 — jedi로 호출 관계를 의미 수준에서 풀기 (0) 2026.07.06 언어가 하나 늘 때 코드를 고치지 않으려면 — 표준 tags.scm 쿼리로 다언어 추출하기 (0) 2026.07.05 코드를 글자가 아니라 구조로 읽기 — tree-sitter로 함수와 모듈을 뽑아내기 (0) 2026.07.05 코드를 그래프로 본다는 것 — 호출 그래프부터 코드 속성 그래프까지 (0) 2026.07.04 한 문장이 다른 문장에 기대는 두 가지 방식 — 프로그램 의존 그래프(PDG) (0) 2026.07.03 값은 어디서 와서 어디로 가는가 — 데이터흐름 그래프(DDG)로 코드 읽는 법 배우기 (0) 2026.07.03 코드는 일직선이 아니다: 제어흐름 그래프(CFG)로 함수 속 길을 그려보기 (0) 2026.07.03 스킬의 끝은 스크립트인가 — 하네스 엔지니어링에서 스크립트는 '지능을 파낸 빈자리'다 (0) 2026.07.02 MCP는 왜 Streamable HTTP로 갈아탔나 — 엔드포인트 하나로 스트림을 다스리는 법 (0) 2026.07.01