1 질문: 범용 코드 분석 Agent는 가능한가
이전 글에서 GraphRAG 기반 코드 분석기의 설계 원칙을 다루었다. AST Fact Graph를 불변 전제로 고정하고, LLM Semantic Edge를 제한된 집합으로 통제하면 신뢰할 수 있는 코드 분석 Agent를 만들 수 있다는 것이 핵심이었다.
이 글에서는 한 단계 더 나아가, 이 아키텍처를 모든 언어·모든 레포로 확장할 수 있는가 를 검토한다.
2 파이프라인: 각 단계의 성숙도
코드 → AST 파싱 → 트리플 추출 → 지식 그래프 → Cypher 쿼리 → LLM 설명
| 단계 | 기술 | 성숙도 | 범용화 난이도 |
|---|---|---|---|
| 코드 → AST | AST 파서, Tree-sitter | 높음 | 중간 (언어별 파서 필요) |
| AST → 트리플 | CPG(Code Property Graph) 변환 | 높음 | 중간 (언어별 매핑 규칙) |
| 온톨로지 설계 | Module/Function/Class + imports/calls | 정립됨 | 낮음 (언어 공통 추상화 가능) |
| 그래프 저장/쿼리 | Neo4j + Cypher | 높음 | 낮음 (언어 무관) |
| LLM 연동 | 쿼리 결과 → 자연어 설명 | 검증됨 | 낮음 (언어 무관) |
각 단계가 개별적으로는 이미 검증된 기술이다. 문제는 이것들을 end-to-end로 통합 하고, 범용 으로 확장하는 순간 발생한다.
3 범용화의 세 가지 난제
3.1 난제 1: 동적 호출 누락
정적 분석은 코드에 명시적으로 작성된 관계만 추출한다. 동적 언어의 런타임 행동은 포착하지 못한다.
| 언어 | 정적 분석 커버리지 | 누락되는 패턴 |
|---|---|---|
| Python | ~70-80% | getattr(), 동적 import, 데코레이터 기반 라우팅 |
| Java | ~85-90% | 리플렉션, DI 컨테이너(Spring), 프록시 패턴 |
| JavaScript/TS | ~65-75% | 콜백 체인, 동적 require(), 이벤트 리스너 |
| Go | ~90-95% | 인터페이스 기반 다형성 (구현체 식별 어려움) |
영향도 분석에서 “모듈 A를 바꾸면 어디에 영향이 있는가?”라는 질문에 답할 때, 누락된 호출 관계 하나가 전체 분석의 신뢰도 를 훼손한다. 90%의 관계를 포착해도 나머지 10%가 핵심 경로에 있으면 분석이 무용지물이 된다.
이것은 개방 세계 가정(OWA)과 폐쇄 세계 가정(CWA)의 문제이기도 하다. 정적 분석은 CWA(“추출되지 않은 관계는 없다”)로 동작하지만, 실제 코드는 OWA(“아직 모르는 관계가 있을 수 있다”)이다. 이 간극을 인지하고 사용자에게 명시하는 것이 중요하다.
완화 전략:
| 전략 | 방법 | 추가 비용 |
|---|---|---|
| 런타임 트레이스 | 테스트 실행 시 실제 호출 관계 수집 | 테스트 커버리지에 의존 |
| 타입 추론 | mypy, TypeScript 타입 정보 활용 | 타입 어노테이션 필요 |
| 패턴 매칭 | DI 컨테이너 설정 파일 파싱 | 프레임워크별 파서 필요 |
| LLM 보조 추론 | 코드 맥락에서 동적 호출 추정 | 환각 위험, confidence 관리 필요 |
3.2 난제 2: 언어별 AST 차이
Python AST: ast.parse() → Module, FunctionDef, ClassDef, Import
Java AST: javac → CompilationUnit, MethodDeclaration, ClassDeclaration
JS AST: Babel/Acorn → Program, FunctionDeclaration, ArrowFunctionExpression
같은 “함수”라는 개념이 언어마다 다른 AST 노드로 표현된다. 이를 공통 온톨로지에 매핑하려면 언어별 매핑 레이어 가 필요하다.
Tree-sitter를 통한 부분적 해결:
Tree-sitter는 다수 언어를 지원하는 범용 파서이다. 동일한 API로 여러 언어의 AST를 생성할 수 있다. 그러나 AST 구조 자체는 언어마다 다르므로, 공통 온톨로지로의 매핑 규칙은 언어별로 작성해야 한다.
Tree-sitter (범용 파서)
↓
언어별 AST (구조가 다름)
↓
언어별 매핑 규칙 (수동 작성 필요)
↓
공통 온톨로지 (Module, Function, Class, imports, calls)
CPG(Code Property Graph)의 접근:
Joern 프로젝트의 CPG는 AST + CFG + PDG를 하나의 그래프로 통합한 언어 무관 코드 표현 모델이다. 이미 C, Java, Python, JavaScript 등을 지원하며, 공통 스키마로 정규화한다. GraphRAG의 온톨로지 레이어로 CPG를 채택하면 언어별 매핑 문제를 상당 부분 해결할 수 있다.
3.3 난제 3: 온톨로지 유지보수 비용
코드는 매일 바뀐다. 그래프가 코드 변경을 따라가지 못하면 가치가 급락 한다.
| 갱신 전략 | 방법 | 비용 | 정확도 |
|---|---|---|---|
| 전체 재빌드 | 매 커밋마다 전체 그래프 재생성 | 높음 (대규모 레포에서 수분~수십분) | 높음 |
| 증분 업데이트 | Git diff → 변경 파일만 재파싱 → 트리플 diff | 낮음 | 높음 (구현 복잡) |
| 지연 갱신 | 쿼리 시점에 해당 모듈만 갱신 | 최저 | 중간 (최신성 보장 안 됨) |
증분 업데이트가 이상적이지만 구현이 복잡하다:
Git diff 분석
↓
변경된 파일 목록 추출
↓
해당 파일의 기존 트리플 삭제
↓
변경된 파일 재파싱 → 새 트리플 생성
↓
그래프 DB에 새 트리플 삽입
↓
전이적 관계 재계산 (영향받는 부분만)
스케일 문제:
| 레포 규모 | 파일 수 | 예상 노드 수 | 예상 엣지 수 | 전체 재빌드 시간 |
|---|---|---|---|---|
| 소규모 | ~100 | ~5,000 | ~20,000 | 수초 |
| 중규모 | ~1,000 | ~50,000 | ~200,000 | 수분 |
| 대규모 모노레포 | ~10,000+ | ~500,000+ | ~2,000,000+ | 수십분 이상 |
대규모 모노레포에서는 전체 재빌드가 비현실적이므로 증분 업데이트가 필수이다.
4 기존 도구와의 비교
4.1 CodeQL (GitHub)
| 항목 | CodeQL | GraphRAG 코드 분석기 |
|---|---|---|
| 코드 표현 | 관계형 DB (테이블) | 지식 그래프 (노드 + 엣지) |
| 쿼리 언어 | QL (SQL-like) | Cypher (그래프 패턴 매칭) |
| 관계 탐색 | JOIN (깊어지면 느림) | 그래프 순회 (\(O(1)\) 홉) |
| 추론 | 규칙 기반 (QL 쿼리) | 그래프 알고리즘 + LLM |
| 자연어 인터페이스 | 없음 | LLM이 자연어 → Cypher 변환 |
| 주요 용도 | 보안 취약점 탐지 | 구조 이해, 영향도 분석 |
| 성숙도 | 매우 높음 (프로덕션) | 초기 (PoC~MVP) |
4.2 Joern (CPG)
| 항목 | Joern | GraphRAG 코드 분석기 |
|---|---|---|
| 코드 표현 | CPG (AST + CFG + PDG) | 온톨로지 기반 지식 그래프 |
| 분석 깊이 | 함수 내부 데이터 흐름까지 | 모듈/함수 간 의존 관계 중심 |
| 강점 | 취약점 패턴 탐지, 데이터 흐름 | 크로스 레포 의존성, 자연어 질의 |
| 약점 | 자연어 인터페이스 없음 | 함수 내부 분석 약함 |
CodeQL과 Joern은 코드를 데이터로 변환하여 쿼리 하는 도구이다. GraphRAG 코드 분석기는 여기에 두 가지를 추가한다:
- 자연어 인터페이스: “이 모듈 바꾸면 어디 영향 있어?”라고 물으면 답한다
- 의미적 관계: AST에서 추출할 수 없는 도메인 수준의 관계(예:
validates_input_for,shares_state_with)를 LLM이 추론하여 그래프에 추가한다
즉 CodeQL/Joern은 구조적 분석 에 최적화되어 있고, GraphRAG는 구조 + 의미 + 자연어 를 통합한다.
5 현실적 타협: 80% 범용
“모든 언어 × 모든 레포 × 모든 패턴”을 100% 커버하는 범용은 비현실적이다. 현실적 목표는 80% 커버리지의 범용 이다.
5.1 단계적 확장 전략
Phase 1: 단일 언어 + 단일 레포
→ Python 모노레포, 정적 분석만
→ 이 단계에서 온톨로지 설계와 파이프라인을 검증
Phase 2: 단일 언어 + 멀티 레포
→ 크로스 레포 의존성, 엔티티 해소(Entity Resolution) 추가
→ URI 설계: code:{repo}/{module_path}/{entity_type}/{entity_name}
Phase 3: 멀티 언어 + 멀티 레포
→ Tree-sitter 또는 CPG로 언어 추상화
→ 언어별 매핑 규칙 추가
Phase 4: 동적 분석 통합
→ 런타임 트레이스로 정적 분석 누락 보완
→ confidence 기반 엣지 관리
5.2 각 Phase의 핵심 판단 기준
| Phase | 진입 조건 | 성공 기준 |
|---|---|---|
| 1 → 2 | Phase 1에서 CQ 기반 평가 통과 | 단일 레포에서 영향도 분석이 수작업보다 빠름 |
| 2 → 3 | 크로스 레포 의존성 추적 성공 | 엔티티 해소 정확도 > 95% |
| 3 → 4 | 멀티 언어 AST 매핑 안정화 | 정적 분석 커버리지 한계가 실무에서 문제로 보고됨 |
“처음부터 모든 언어를 지원하겠다”는 이전 글에서 정의한 실패 패턴 중 하나이다. Phase 1에서 단일 언어로 온톨로지 설계, 그래프 갱신 파이프라인, 쿼리 패턴을 충분히 검증한 뒤에 확장해야 한다.
6 특수 케이스: Python 단일 언어는 실현 가능한가
범용이 어렵다면, Python 하나로 한정하면 어떨까? Python은 다른 언어 대비 확실히 유리한 조건을 갖추고 있다.
6.1 Python이 유리한 이유
| 조건 | 설명 |
|---|---|
| AST 파서 100% 커버리지 | ast.parse()가 표준 라이브러리에 내장, 모든 Python 소스를 완벽하게 파싱 |
| 단일 파서 | Tree-sitter, Joern 없이 ast 모듈 하나로 충분 |
| 타입 힌트 생태계 | mypy, pyright 등 정적 타입 분석 도구가 성숙 |
| 메타데이터 자동화 | ast.NodeVisitor로 클래스/함수/임포트 관계를 자동 추출하는 파이프라인 구축이 용이 |
6.2 AST 파싱 100% ≠ 호출 그래프 100%
여기서 간과하기 쉬운 핵심이 있다. ast.parse()는 구문(syntax)을 100% 파싱하지만, GraphRAG에 필요한 호출 관계(call graph)는 구문만으로 완성되지 않는다.
6.2.1 동적 호출 — AST로 추적 불가
# AST가 보는 것: getattr(obj, name) 호출 1건
# 실제 일어나는 것: 런타임에 수백 개의 다른 메서드 호출
handler = getattr(obj, f"handle_{event_type}")
handler()
# importlib 동적 임포트
module = importlib.import_module(f"plugins.{plugin_name}")
# eval/exec
func = eval(f"process_{data_type}")AST는 getattr이 호출된 건 알지만, 실제로 어떤 메서드가 호출되는지는 런타임 정보 없이 알 수 없다.
6.2.2 프레임워크 매직 — 의미적 연결 누락
# Flask/FastAPI — 데코레이터 기반 라우팅
@app.route("/users/<id>")
def get_user(id): ...
# Celery — 태스크 등록 및 비동기 호출
@celery.task
def process_data(): ...
process_data.delay() # 이 호출이 process_data()로 연결되는 건 Celery 규칙
# SQLAlchemy — 메타클래스 기반 ORM
class User(Base):
__tablename__ = "users"AST로 파싱은 되지만, “이 함수가 HTTP GET /users/<id>에 바인딩된다”는 의미는 프레임워크별 규칙을 알아야 추출 가능하다.
6.2.3 덕타이핑 — 호출 대상 불특정
타입 힌트가 있으면 추론이 가능하지만, 없으면 호출 대상을 특정할 수 없다.
6.2.4 monkey patching — 런타임 치환
6.3 Python 정적 분석 커버리지 실측 추정
| 분석 대상 | 커버리지 | 비고 |
|---|---|---|
| 파일/클래스/함수 구조 | ~100% | ast로 완벽 |
| 정적 호출 관계 (직접 호출) | ~70-80% | func(), module.func() 형태 |
| 동적 호출 관계 | ~20-30% | getattr, importlib, 콜백 등 |
| 프레임워크 의미 | 0% (기본) → 높음 (플러그인) | 프레임워크별 규칙 추가 시 |
6.4 현실적 전략: Python + 프레임워크 플러그인
Python 단일 언어에서, 사용하는 프레임워크를 1-2개로 한정하면 실용적 수준의 GraphRAG 코드 분석기는 충분히 실현 가능하다.
Phase 1 구체화 (Python 전용):
ast.parse() → 구조 메타데이터 100% 추출
↓
직접 호출 관계 추출 (정적 분석 70-80%)
↓
프레임워크 플러그인 추가 (FastAPI, SQLAlchemy 등)
↓
mypy/pyright 타입 정보로 덕타이핑 보완
↓
CI/CD 훅으로 증분 업데이트 자동화
| 요소 | 비용 | 효과 |
|---|---|---|
ast 기반 파이프라인 |
낮음 (표준 라이브러리) | 구조 메타데이터 완벽 |
| 프레임워크 플러그인 1-2개 | 중간 (프레임워크당 1-2주) | 커버리지 80% → 90%+ |
| 타입 힌트 연동 | 낮음 (mypy 결과 활용) |
덕타이핑 보완 |
| 증분 업데이트 | 중간 (Git diff 기반) | 유지보수 비용 최소화 |
Python은 AST 파서의 완전성, 타입 힌트 생태계, 프레임워크의 상대적 규칙성 덕분에 Phase 1의 최적 후보이다. “범용 코드 분석기”가 아닌 “Python 코드 분석기”로 시작하면, 온톨로지 설계와 파이프라인 검증을 가장 낮은 비용으로 수행할 수 있다. 단, AST 파싱 100%를 호출 그래프 100%와 혼동하지 않는 것이 중요하다. 동적 패턴과 프레임워크 매직은 별도 전략으로 보완해야 한다.
7 핵심은 “만들 수 있느냐”가 아니다
기술적으로 범용 코드 분석 Agent는 이론적으로 가능 하다. 파이프라인의 각 단계가 이미 검증된 기술이고, CPG와 Tree-sitter가 언어 추상화를 제공한다.
그러나 진짜 질문은 “유지보수할 수 있느냐” 이다.
| 질문 | 핵심 |
|---|---|
| 만들 수 있는가? | 가능하다 (각 단계가 검증됨) |
| 범용으로 확장할 수 있는가? | 80% 커버리지는 가능, 100%는 비현실적 |
| 유지보수할 수 있는가? | 이것이 진짜 병목 — 코드는 매일 바뀌는데 그래프가 따라가야 한다 |
| ROI가 나오는가? | Phase 1~2에서 빠르게 검증해야 한다 |
코드는 살아있는 유기체이다. 그래프가 코드의 변화를 실시간으로 반영하지 못하면, 아무리 정교한 온톨로지도 “어제의 지도”가 된다. CI/CD 파이프라인에 증분 업데이트를 통합하는 것이 기술적 핵심이고, 이것의 안정적 운영이 프로젝트의 성패를 결정한다.
8 관련 주제
시리즈 내 연결
- GraphRAG 기반 Blackbox 코드 분석기 아키텍처 — 설계 원칙과 5가지 성공 조건
- GraphRAG 개념과 최근 트렌드 — 온톨로지와 지식 그래프 기초
- GraphRAG 메타데이터 접근법 — 메타데이터 기반 그래프 구축
다른 카테고리 연결
- 그래프 이론 기초 — BFS, DFS, 중심성 등 수학적 기반
- 온톨로지 개론 — 온톨로지 설계 방법론과 평가
- 온톨로지와 메타데이터 저장소 설계 — RDB vs GraphDB 구현 비교