본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 22. 10:32

graphlens: Python, TypeScript, Go, Rust를 아우르는 저장소를 하나의 타입형 그래프(typed graph)로

요약

GraphLens는 다국어(Python, TypeScript, Go, Rust 등) 저장소를 하나의 타입형 그래프(typed graph)로 변환하는 오픈 소스 프레임워크입니다. 기존 도구의 한계인 단순 검색 방식과 단일 언어 제약을 극복하여, 정규화된 그래프 IR을 통해 정밀한 의존성 분석과 LLM 에이전트 지원을 제공합니다.

핵심 포인트

  • 다국어(Polyglot) 환경을 지원하는 통합 그래프 IR 제공
  • Tree-sitter 기반의 정밀한 구문 분석으로 정확한 참조 해결
  • 의존성 분석, 데드 코드 탐지, LLM 에이전트 활용 가능
  • 어댑터 중심 설계로 테스트와 캐싱이 용이한 구조

graphlens: Python, TypeScript, Go, Rust를 아우르는 모든 저장소를 하나의 타입형 그래프(typed graph)로 변환하기

제가 지금까지 사용해 본 모든 코드 인텔리전스(code-intelligence) 도구들은 두 가지 함정 중 하나에 빠집니다.

첫 번째는 **grep-and-read 루프(grep-and-read loop)**입니다. 사용자(또는 AI 에이전트)가 이름을 검색하고, 10개의 파일을 열고, 매칭되는 부분 주변을 읽고, import를 따라가고, 다시 검색하는 방식입니다. 작동은 하지만 느리고, 토큰을 낭비하며, services.py에서 찾은 process_orderapi.py에서 호출되는 동일한 process_order인지, 아니면 tests/에 있는 관련 없는 것인지 구분하지 못합니다.

두 번째는 **단일 언어 사일로(single-language silo)**입니다. Python은 완벽하게 이해하지만, TypeScript 프론트엔드가 Python FastAPI 라우트를 호출하는 순간 눈이 멀어버리는 도구들입니다. 실제 시스템은 다국어(polyglot)로 구성되어 있습니다. 하지만 여러분의 툴링은 대개 그렇지 못합니다.

graphlens는 이 두 가지 함정에서 벗어나기 위해 구축된 오픈 소스 (MIT) 프레임워크입니다. 이 프레임워크는 소스 프로젝트를 파싱하고, 그 구조를 공유된 **그래프 IR (graph IR)**로 정규화하며, 의존성 분석(dependency analysis), 탐색(navigation), 데드 코드 탐지(dead-code detection), 또는 LLM 에이전트에게 파일 덤프 대신 정확한 답변을 제공하는 용도 등 여러분이 원하는 무엇이든 할 수 있도록 해당 그래프를 제공합니다.

Repository → Language Adapter → GraphLens (IR) → Graph Backend
계층 (Layer)책임 (Responsibility)
Language Adapter소스 파일을 파싱하여 GraphLens를 생성함
...

핵심 설계 결정 사항: 어댑터(adapters)는 순수 데이터 생성자(pure data producers)입니다. 어댑터는 데이터베이스에 쓰지 않으며, 읽은 후에는 파일 시스템을 건드리지 않고, 서버를 실행하지도 않습니다. 그래프가 유일한 출력물입니다. 덕분에 전체 파이프라인을 매우 쉽게 테스트하고, 캐싱하고, 직렬화(serializable)할 수 있습니다.

첫 번째 그래프를 만들기까지 30초

pip install "graphlens-cli[python]"
graphlens analyze ./my-project
graphlens · my-project
  nodes:      1240
  relations:  3981
...

또는 Python에서:

from pathlib import Path
from graphlens import adapter_registry

...

무엇이 엣지(edges)를 실제로 만드는가 (이름 매칭 추측이 아닌)

대부분의 경량 코드 그래프 (code-graph) 도구들은 이름을 통해 참조를 해결합니다. 예를 들어 save() 호출을 발견하면, save라고 불리는 모든 것에 엣지 (edge)를 그리는 식입니다. 이는 빠르지만 틀린 방식입니다. 코드베이스에는 보통 수십 개의 save가 존재하기 때문입니다.

graphlens은 이 작업을 두 단계로 나눕니다:

  1. Tree-sitter가 모든 파일을 구체 구문 트리 (concrete syntax tree)로 파싱하여, 정확한 구조와 1부터 시작하는 범위 (span) 위치를 제공합니다. 이는 모든 _사용 지점 (use-site)_을 역할 (call / read / write / annotation / base)을 가진 하나의 **발생 (occurrence)**으로 기록합니다.
  2. 그 다음, 언어별 **타입 인식 리졸버 (type-aware resolver)**가 각 발생에 대해 definition_at(file, line, col)에 대한 답을 내놓습니다. 해결된 정의는 실제 선언 노드(declaration node)로 향하는 진짜 엣지가 됩니다.
언어리졸버 (Resolver)엔진 (Engine)
PythonTyResolverLSP를 통한 ty (Astral, Rust 기반)
...

따라서 CALLS 엣지는 실제 함수를 가리키고, HAS_TYPE 엣지는 실제 클래스를, INHERITS_FROM 엣지는 실제 부모(base)를 가리킵니다. 이것이 "아마 관련이 있을 것"과 "관련이 있음"의 차이입니다.

부분적 실패에 대한 정직함

타입 분석 (Type analysis)은 저하될 수 있습니다. 툴체인이 누락되었거나, 파일이 타입 체크를 통과하지 못할 수도 있습니다. graphlens은 절반만 해결된 그래프를 조용히 생성하는 대신, 그 결과를 기록합니다:

from graphlens import RESOLVER_STATUS_KEY
graph.metadata[RESOLVER_STATUS_KEY]   # 'ok' | 'degraded' | 'unavailable'

CI 환경에서 --strict 옵션을 켜면 ok가 아닌 상태는 빌드를 실패하게 만듭니다. 이를 통해 에이전트나 대시보드가 조용히 불완전해진 그래프를 소비하는 일을 방지할 수 있습니다.

그래프 모델 (The graph model)

노드 (Nodes) (PROJECT, MODULE, FILE, CLASS, METHOD, FUNCTION, PARAMETER, VARIABLE, ATTRIBUTE, TYPE_ALIAS, IMPORT, DEPENDENCY, EXTERNAL_SYMBOL, BOUNDARY)는 id, kind, qualified name, file path, span, 그리고 자유 형식의 메타데이터를 가진 불변 데이터 클래스 (frozen dataclasses)입니다.

**관계 (Relations)**는 방향성이 있는 타입 지정 엣지 (directed, typed edges)입니다:

종류 (Kind)의미
CONTAINS / DECLARES구조적 포함 및 선언
...

결정론적 ID (Deterministic IDs)

노드의 ID는 project::kind::qualified_name의 SHA-256 해시 값입니다:

from graphlens import make_node_id
make_node_id("my-project", "my.module.func", "FUNCTION")
# → 모든 머신에서, 매 스캔마다 동일한 ID 생성

ID가 파일 위치가 아닌 정체성(identity)에만 의존하기 때문에, 재스캔(re-scanning)을 수행해도 동일한 ID가 생성됩니다. 이것이 graph.diff(other)와 증분 업데이트(incremental updates)를 가능하게 하며, CI 환경에서 그래프를 캐싱(cacheable)할 수 있게 만드는 핵심 요소입니다.

단일 언어 도구가 가질 수 없는 기능: 언어 간 경계 (cross-language boundaries)

이 부분이 제가 가장 좋아하는 지점입니다. 어댑터(Adapters)는 서비스가 노출하거나 소비하는 인터페이스(HTTP 경로, 큐 토픽, gRPC 메서드, Temporal 액티비티 등)에 대해 언어에 구애받지 않는 BOUNDARY 노드를 생성하며, EXPOSES 엣지(제공자) 또는 CONSUMES 엣지(소비자)를 가집니다.

경계(boundary)의 ID는 make_boundary_id(mechanism, key)이며, 여기에는 프로젝트나 언어 정보가 포함되지 않습니다. HTTP 경로는 정규화(normalized)되어 /users/1, /users/{user_id} (FastAPI), <int:id> (Flask), :id (Express)가 모두 GET /users/{}로 통합됩니다.

그 결과: Python FastAPI 경로와 동일한 엔드포인트를 호출하는 TypeScript fetch동일한 경계 ID를 생성합니다. 두 그래프를 병합하고 graphlens-link를 실행하면, 언어 간의 격차를 가로지르는 COMMUNICATES_WITH 엣지를 얻을 수 있습니다:

from graphlens import adapter_registry
from graphlens_link import link_graph

...

이제 "어떤 프론트엔드 호출이 이 엔드포인트에 도달하는가?"라는 질문에 답할 수 있습니다. 이는 단일 언어 도구로는 표현조차 불가능한 질문입니다.

다섯 가지 사용 방법

라이브러리로서 사용 (As a library) — 어댑터를 로드하고, GraphLens를 가져와 호출자(callers), 피호출자(callees), 참조(references), 인접 영역(neighborhoods), 차이점(diffs), JSON 라운드트립(round-trips), 다국어 병합(multi-language merges) 등을 쿼리합니다.

CLI를 통한 사용 (From the CLI) — 다섯 가지 서브커맨드(subcommands)가 일반적인 워크플로우를 지원합니다:

graphlens analyze ./repo --output graph.json   # 인덱싱 (index)
graphlens query process_order -g graph.json --op callers
graphlens visualize ./repo                      # 대화형 vis.js HTML 시각화
...

CI에서 — 모든 어댑터(adapter)와 툴체인(toolchain)이 사전 설치된 Docker 이미지(ghcr.io/neko1313/graphlens)와 --strict 옵션을 사용합니다. 푸시(push)할 때마다 인덱싱을 수행하고, 그래프를 아티팩트(artifact)로 게시하며, 손상된 그래프가 발생하면 실패 처리합니다.

MCP를 통한 LLM 에이전트에게graphlens mcp는 저장된 그래프를 Model Context Protocol (MCP) 쿼리 도구(stats, find, callers, callees, references, neighbors, boundaries, communicates_with)로 노출합니다. 코드베이스를 프롬프트에 통째로 쏟아붓는 대신, 에이전트는 정밀한 질문을 던지고 구조화된 작은 답변을 받습니다. 이는 단순한 최선 노력(best-effort) 방식의 텍스트 검색이 아니라, 해결된 엣지(resolved edges)를 제공합니다.

Neo4j 내보내기 방식UNWIND … MERGE Cypher를 사용하여 그래프 데이터베이스로 직접 내보낼 수 있으며(APOC 불필요), 그 후 원하는 방식으로 쿼리할 수 있습니다.

플러그인 아키텍처: SQLAlchemy-dialect 패턴

코어(core)는 어댑터를 절대 직접 임포트(import)하지 않습니다. 각 언어는 Python 엔트리 포인트(entry points)를 통해 스스로를 등록하는 별도의 패키지입니다:

[project.entry-points."graphlens.adapters"]
python = "graphlens_python:PythonAdapter"

호출자(callers)는 이름 문자열을 통해 레지스트리(registry)를 거쳐 어댑터를 해결합니다:

adapter_registry.available()        # ['python', 'typescript', ...]
adapter = adapter_registry.load("python")()

새로운 언어를 추가한다는 것은 LanguageAdapter 계약(contract)에 따라 하나의 패키지를 작성하는 것을 의미하며, 코어에는 아무런 변경이 필요하지 않습니다.

graphlens가 아닌

범위는 의도적으로 좁게 설정되어 있으며, 문서에도 명시되어 있습니다. graphlens는 그래프 IR(Intermediate Representation)을 생성하고 거기서 멈춥니다. 다음 기능은 제공하지 않습니다:

  • 상태를 유지하거나 데이터베이스를 소유하지 않음 (백엔드는 별도의 소비 계층임);
  • 파일 시스템을 감시하거나 자체적으로 증분 인덱싱(incremental re-indexing)을 수행하지 않음 (스캔은 순수 함수임; 결정론적 ID가 증분 업데이트를 가능하게 하지만, 호출자가 이를 주도함);
  • 임베딩(embeddings), 시맨틱 검색(semantic search) 또는 관련성 순위(relevance ranking)를 계산하지 않음 (그래프는 구조적이고 타입 인식적(type-aware)이지, 벡터 인덱스(vector index)가 아님);
  • UI 또는 에이전트 런타임(agent runtime)을 제공하지 않음 (visualize는 정적 HTML을 생성하고, mcp는 쿼리 도구를 노출함 — 둘 다 장시간 실행되는 서비스를 호스팅하지 않음).

그것들은 graphlens를
기반으로 구축된 (built on top of)
도구들에 속합니다. 핵심(core)을 최소한으로 유지하는 것이 graphlens를 조합 가능한(composable) 상태로 유지하는 비결입니다.

벤치마크 (Benchmarks)

실제 프로젝트에서의 처리량(Throughput)이며, 배포된 Docker 이미지 내에서 매 릴리스마다 업데이트됩니다 (단일 콜드 런(single cold run), 지표용):

프로젝트 (Project)언어 (Lang)코드 라인 수 (LOC)노드 (Nodes)시간 (Time)해결됨 (Resolved)
apache/supersetpython399 519156 251148.7s84%
...

사용해 보기 (Try it)

pip install "graphlens-cli[python]"
graphlens analyze . --output graph.json
graphlens visualize .

만약

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0