본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 05. 19. 12:41

AI 하네스의 심장부 ── AI의 AI에 의한 AI를 위한 지식 그래프 (연재 Part 2)

요약

에어클로젯(airCloset)의 CTO 츠지가 개발한 'Product Graph(cpg)'는 코드, 문서, DB 스키마, 인프라 정의를 하나의 지식 그래프로 통합한 시스템입니다. 이를 통해 AI는 자연어 질문만으로 복잡한 코드의 영향 범위를 파악하고, 데이터 흐름(Trace)과 관련 문서를 스스로 찾아내어 엔지니어뿐만 아니라 비엔지니어(PMO)도 시스템 구조를 이해할 수 있게 돕습니다.

핵심 포인트

  • 코드, 문서, DB 스키마, 인프라를 하나의 지식 그래프(Knowledge Graph)로 통합하여 시맨틱 검색 가능
  • 자연어 질문을 통해 함수, BigQuery 테이블, API 엔드포인트 간의 상류(Write) 및 하류(Read) 관계를 추적
  • AI가 단순 정보 검색을 넘어 다음 액션(Runbook)을 제안하는 에이전트적 특성 보유
  • 엔지니어의 지식 독점을 해소하고 PMO 등 비엔지니어도 시스템 영향도를 파악할 수 있는 환경 구축

여러분 안녕하세요! 에어클로젯(airCloset)에서 CTO를 맡고 있는 츠지(Tsuji)입니다.

Part 1(총론)에서는 cortex라는 플랫폼 위에서 AI가 PR 리뷰나 장애 대응을 수행하고 있는 이야기를 썼습니다. 그 플라이휠(flywheel)의 중심에 있는 것이 바로 Product Graph(구현명: cortex-product-graph, 이하 cpg)입니다.

Part 1에서는 "코드·docs·DB 스키마·인프라 정의가 하나의 지식 그래프(Knowledge Graph)로 통합되어 있어 시맨틱 검색(Semantic Search)이 가능하다"라는 추상적인 내용만 적었습니다. 이번에는 그 내용물 ── 어떻게 만들었는지, 왜 이 설계로 결정되었는지, 무엇이 바뀌었는지를 쓰겠습니다.

연재 목록

#테마주요 장면기사
1총론: cortex의 하네스PR이 무인 머지됨 / 장애가 인지되기 전에 고쳐짐ai-harness-intro
...

갑자기 하나의 장면부터

"KPI 대시보드에 나오는 '버그 발생률'의 계산 로직을 바꾸고 싶다. 어디에 있고, 무엇이 망가질까?" ── 구현에 들어가기 전에, 이런 질문이 생겼다고 가정해 봅시다.

AI에게 이 질문을 그대로 던지면, 함수명도 파일 경로도 주지 않았음에도 AI는 cpg를 시맨틱 검색(Semantic Search)으로 호출하여 관련 노드(node)를 한꺼번에 찾아냅니다. 돌아오는 결과는 함수뿐만 아니라, 해당하는 BigQuery 테이블이나 **API 엔드포인트(endpoint)**까지 포함된 후보 리스트입니다. 응답 끝에 있는 "다음 액션 후보(Runbook)"를 보고, AI는 **쓰기(write) / 읽기(read) 양쪽이 모여 있는 BQ 테이블을 기점으로 트레이스(trace)**를 다시 수행합니다.

최종적으로 AI가 내놓는 답은 다음과 같습니다:

계산 지점:calculateRatePer100pt / calculateBugCount ── 둘 다 순수한 계산 함수이며, 외부 I/O는 없음 (변경해도 직접적인 부작용은 없음)
쓰기 측(상류):syncKpiMetrics / writeKpiMetrics / backfillKpiMetricskpi_bug_rate_per_100pt 테이블에 기록. 이것이 집계 배치(batch)의 본체
읽기 측(하류): BigQuery를 통해 BigQueryKpiRepository.getSummaryByDate가 읽고, /kpi/bugs API → KPI 대시보드 페이지로 연결
관련 docs: docs/generator/kpi.md에 버그 발생률 정의가 있음. 코드를 변경한다면 docs도 동시에 업데이트 필요

"그럼 docs를 함께 변경하고, 배포는 집계 배치가 돌아가지 않는 시간대에 한다"라고 판단할 수 있습니다.

물론 저 자신은 이 계산 로직도, 함수도, BQ 테이블도 전부 알고 있습니다. 제가 직접 작성했으니까요. 하지만 반대로 말하면, 저 이외의 사람들은 이곳을 건드릴 수 없습니다. 누군가 "버그 발생률을 바꾸고 싶다"라고 생각하면, 결국 저를 붙잡으러 오는 수밖에 없게 됩니다 ── 이것이 3개월 전까지의 상황이었습니다.

지금은 위와 같은 조사와 판단을 PMO 멤버(비엔지니어)가 cpg를 사용하여 스스로 하고 있습니다. grep이나 설계서로도 닿지 않았던 "코드에 무엇이 있고, 무엇이 망가지는가"를 자연어 질문 하나로 얻을 수 있게 된 결과입니다.

이를 실현하고 있는 것이 cpg입니다 ── 코드·docs·DB 스키마·인프라 정의를 하나의 지식 그래프(Knowledge Graph)로 통합한 것으로, 함수명을 모르더라도 "하고 싶은 일"이라는 자연어로부터 관련 정보를 1~2 홉(hop) 내에 찾아낼 수 있습니다. 도구의 반환값 자체가 '다음에 호출해야 할 도구'를 나타내는 Runbook 구조로 되어 있기 때문에, AI가 기점 노드를 스스로 다시 선택하여 심층 분석할 수 있다는 점도 포인트입니다.

여기서부터는 이것이 어떻게 만들어졌는지에 대한 이야기입니다.

정적 분석(Static Analysis)만으로는 부족했던 code-graph

cortex 플랫폼은 별도로 정적 분석으로 코드베이스를 그래프화하는 시스템을 가지고 있습니다 (상세 내용은 별도 기사에서 다룰 예정이므로 여기서는 간단히 언급만 하겠습니다). 외부 공개용 운영 리포지토리(repository) 군을 대상으로, JS/TS 코드를 AST(Abstract Syntax Tree) 분석하여 함수의 호출 관계, API 엔드포인트, DB 액세스, 이벤트 발행·구독과 같은 의존 관계를 자동으로 추출하는 메커니즘입니다.

이것은 정적 분석 (Static Analysis)으로서 정확하며, 외부 공개용 리포지토리(Repo)에서는 현재도 사용 중입니다 (후술). 하지만 cortex 자체에 이 접근 방식을 적용했을 때는 '구상했던 성과'에 도달하지 못했습니다.

구체적으로 도달하지 못한 점은 세 가지입니다:

  • 컨텍스트(Context)가 없음 ── 노드는 존재하지만, "이 API가 무엇을 위해 존재하는가", "왜 이 테이블에 이 컬럼이 있는가"와 같은 **의미(Semantics)**가 그래프(Graph)에 실려 있지 않음. AI에게 "KPI의 버그율을 계산하는 코드는 어디인가?"라고 물어도, 함수명이나 인자명이 우연히 그럴싸하지 않으면 검색되지 않음.
  • 진입점(Entry Point)이 없음 ── 목적하는 파일 경로이나 함수명을 이미 알고 있어야만 검색을 시작할 수 있음. "잠깐 찾아볼게"라는 동작이 성립되지 않음.
  • 몇 번의 홉(Hop)만으로 버스트(Burst) ── 시작 노드에서 1~2홉만 진행해도 관련 노드가 지수적으로 팽창하여, AI가 한 번에 처리할 수 있는 크기를 가볍게 초과함. 트레이스(Trace) 결과가 너무 길어서 사용할 수 없음.

요컨대, "기계적으로는 정확하지만, 의미의 가중치(Weighting)가 없는" 그래프였다는 이야기입니다. AI가 활용하기 위해서는 한 단계 더 높은 "무엇이 중요한가 / 왜 연결되어 있는가"가 필요했습니다.

반면, DB 그래프는 잘 작동하고 있었다

마침 그 무렵, 다른 접근 방식으로 만들고 있던 DB Graph MCP는 기대한 대로 작동하고 있었습니다.

DB Graph는 cortex 내의 15개 스키마, 991개 테이블에 액세스하는 MCP 서버로, 테이블과 컬럼을 AI가 생성한 설명문과 함께 시맨틱 검색 (Semantic Search)할 수 있습니다. 예를 들어 "반납 처리 확인에 관한 테이블"과 같은 자연어 쿼리로, 테이블명에 "반납"이 포함되어 있지 않더라도 의미로 연결되는 노드가 반환됩니다.

무엇이 달랐는지, 한참을 고민한 뒤에야 알 수 있었습니다. DB 그래프에는 '컬럼 설명·테이블 설명'이라는 비즈니스 컨텍스트(Business Context)가 각 노드에 붙어 있고, 그것이 임베딩 (Embedding)에 실려 있다는 점이었습니다. 이것이 '의미로 연결된다'를 지탱하는 본질이었습니다.

정적 분석 기반의 code-graph에는 그것이 없었습니다. 타입(Type)과 호출 관계는 있어도, "왜 이 함수가 존재하는가"는 아무도 적어두지 않았습니다.

가설 ── DB 그래프의 정수를 code graph에 가져올 수 있다면

여기서부터의 가설은 단순했습니다:

"노드마다 비즈니스 컨텍스트가 적혀 있고, 그것이 임베딩에 실려 있다" ── 이것만이 DB 그래프가 잘 작동하는 핵심(Essence)이라면, code graph에 똑같은 일을 수행하게 함으로써 정적 분석의 한계를 구조적으로 뛰어넘을 수 있을 것이다.

문제는 어디에 '비즈니스 컨텍스트'를 둘 것인가였습니다.

선택지를 모두 나열하면 다음과 같습니다:

위치예시문제점
외부 문서설계서 / wiki / Notion코드와 별개로 관리됨. 금방 어긋남. 아무도 유지보수하지 않음
외부 메타데이터sidecar YAML / *.meta.json파일이 이중 관리됨. 이름 변경 시 쉽게 깨짐
전용 그래프 DBNeo4j / Neptune에 주석을 직접 가짐소스와 DB가 이중 관리됨. PR diff에 나타나지 않아 리뷰 불가능
TypeScript 데코레이터 (Decorator)코드에 @GraphNode({...})를 붙임트랜스파일 (Transpile) 과정에 포함됨 = 런타임 (Runtime) 의존성 증가. AST 분석만으로는 모두 파악하기 어려움
DSL 파일독자적인 표기법 .graph 파일새로운 표기법을 학습시키는 비용이 높음. 에디터 보완 기능도 직접 구현해야 함
JSDoc 주석@graph-business / @graph-connects코드 본체와 물리적으로 같은 위치. AST만으로 추출 가능. 런타임 의존성 제로

특히 데코레이터가 아닌 JSDoc을 선택한 것은 의도적입니다:

  • 런타임 의존성 제로 (Runtime Zero Dependency): 데코레이터는 트랜스파일 후의 코드에 남기 때문에 런타임 동작에 영향을 줄 가능성이 있습니다. JSDoc은 주석이므로 실행 의미를 갖지 않으며, 프로덕션 빌드 (Production Build)에서 주석을 제거하면 빌드 결과물에도 남지 않습니다.
  • 코드 이외에도 동일한 형식으로 확장 가능: 동일한 @graph-* 표기법을 infra/의 Pulumi 정의나 docs/...

markdown frontmatter에 전개할 수 있다. 데코레이터(decorator)는 TypeScript 문법에 얽매인다.

AST 해석만으로 전부 추출 가능: ts-morph로 선언을 순회하면, 코드와 JSDoc을 단 한 번의 스캔으로 가져올 수 있다. 데코레이터(decorator)를 사용하면 타입 정보(type information)를 해결해야 하는 상황이 발생하여 빌드가 무거워진다.

PR diff에 자연스럽게 노출: JSDoc은 코드 바로 위에 작성하므로, 코드를 변경하는 PR에는 관련 JSDoc의 변경 사항이 반드시 동일한 파일에 포함된다. 리뷰어가 놓칠 리 없다.

읽는 이(사람과 AI 모두)에게 문서로서 동시에 기능: JSDoc은 본래 IDE의 호버(hover) 표시나 AI 에이전트가 읽는 컨텍스트(context)로 사용되는 영역이다. 그 자리에 @graph-business를 작성하면, 코드를 읽는 사람에게는 선언의 의도를 그 자리에서 바로 읽을 수 있게 해주고, 코드를 작성하는 AI에게도 주변 함수의 의미를 파악하는 실마리가 된다. 그래프용 메타데이터가 이차적으로 코드 리딩/편집의 보조 정보로도 활용되는 이중의 역할을 수행한다.

덧붙여, 이 설계의 본질은 "코드에 부수되는 해석 가능한 주석을 SSoT(Single Source of Truth, 단일 진실 공급원)로 만든다"는 구조이며, TypeScript / JSDoc은 어디까지나 그 하나의 구현 방식일 뿐이다. Python이라면 docstring + ast, Go라면 주석 + go/ast, Rust라면 /// + syn과 같이 동등한 조합이 있다면 어떤 언어에서도 동일한 패턴으로 구현할 수 있다. 중요한 것은 "주석을 어디에 쓰는가"가 아니라 "코드와 물리적으로 같은 장소에, AST 해석만으로 가져올 수 있는 형태로 쓴다"라는 불변 조건이다.

마찬가지로, cortex가 모노레포(monorepo)이기 때문에 이 패턴이 성립하는 것도 아니다. 오히려 리포지토리가 분리되어 있어 AI가 코드를 추적하기 어려운 상황일수록 진가를 발휘한다. 모노레포라면 AI도 grep / file read를 통해 어떻게든 추적할 수 있지만, 멀티레포(multi-repo) 환경에서는 리포지토리를 넘나드는 호출이나 데이터 흐름(data flow)을 추적하는 것 자체가 어렵다. 각 리포지토리에서 동일한 빌드를 실행하여 노드(node) / 에지(edge)를 생성하고, 이를 중앙의 그래프로 집약하면 리포지토리 경계를 넘는 연결 관계가 AI로부터 단 한 번의 홉(hop)으로 보이게 된다. 실제로 사외용 프로덕션 리포지토리 그룹(multi-repo 구성)에 대해서도 동일한 나리지 그래프(knowledge graph)를 별도로 운용하고 있다(상세 내용은 추후 별도로 다루겠다).

접근 방식 ── "코드로부터의 추론"을 버리고, JSDoc을 SSoT로 삼는다

코드 그래프(code graph)의 문제는 "의미가 없다"는 것이었다. 그렇다면 답은 간단하다 ── 의미 그 자체를 코드에 심어버리면 된다.

cortex 내의 코드 그래프에서는 코드만으로 의미나 연결을 추론하는 방침을 완전히 버렸다. 대신에,

모든 선언(함수 / 클래스 / 메서드 / API / Page / Cron / etc.)에 전용 JSDoc 태그를 작성한다. 그래프는 거기서부터 조립한다.

라는 방침으로 완전히 전환했다.

이것이 의미하는 바는, 비즈니스 컨텍스트(business context)의 SSoT가 코드 그 자체가 된다는 것이다. docs와 code 사이에 간극이 존재하는 것이 아니라, **코드에 작성된 JSDoc이 곧 정본(正本)**이다. 문서가 오래되어서 AI도 틀린다는 식의 구조적인 문제는 여기서 한 단계 해결된다.

동일한 소스 코드에서 추출한 "코드로부터의 추론만으로 만든 그래프"와 "JSDoc을 SSoT로 삼은 나리지 그래프"를 비교해 보면, 노드에 무엇이 담겨 있는지의 차이가 명확히 드러난다.

구체적인 태그는 다음과 같다 (cpg 자체 코드의 한 예시):

/**
* 노드에 embedding을 설정 (in-place)
* BQ의 기존 데이터와 비교하여, textForEmbedding이 변경된 노드만 재생성
...

태그의 역할을 정리하면 다음과 같다:

태그역할
@graph-node노드 유형을 명시 (생략 시 Function)
@graph-stack이 선언이 속한 infra stack 명
@graph-domain비즈니스 도메인 (쉼표로 구분하여 여러 개 가능)
@graph-business이 선언이 무엇을 하는지에 대한 고유 설명 (Embedding 입력의 본체)
@graph-connects연결 대상 (여러 개 가능, via:로 파라미터 레벨 추적, none으로 연결 없음 명시)

@graph-business

Embedding 텍스트의 입력이 된다는 점이 핵심입니다. 노드 이름이 아니라, 자연어 문장 하나가 AI의 검색에 효과적입니다. 실제로 이 문장을 쓰는 것은 거의 AI이며, cpg의 경우 평소처럼 코드를 작성하는 흐름 속에서 AI가 JSDoc도 함께 작성해 줍니다 (후술할 ESLint에서 누락되면 빌드가 실패하므로, AI도 쓰지 않을 수 없습니다).

누락을 물리적으로 허용하지 않는다

단, 이 설계는 누락을 허용하는 순간 단번에 무너집니다. @graph-business가 없는 함수가 단 하나라도 있다면, 그 함수는 의미 검색(Semantic Search)에서 히트되지 않습니다. @graph-connects가 없는 함수가 단 하나라도 있다면, 그 함수를 기점/종점으로 하는 데이터 플로우(Data Flow)가 그래프(Graph)에 올라가지 않습니다.

따라서, 누락을 물리적으로 허용하지 않는 메커니즘을 구축했습니다:

  • 5개의 ESLint 플러그인 ── 태그 존재 검증, 구문 검증, 명명 규칙 (stack / domain의 allowlist), @graph-connects 필수화, @graph-connects none 오용 탐지 (외부 서비스 호출 코드에 none을 작성했을 의심 사례 탐지)
  • 자동 PR 리뷰 (Part 1 ③) ── 태그 누락은 [Graph] Critical로 지적, 문서(docs)와의 괴리는 [Doc] Critical로 지적

결과적으로, "코드를 작성하는 순간, 비즈니스 컨텍스트(Business Context)가 반드시 함께 작성되는" 상태가 성립됩니다. 새로운 함수를 추가하면, 그 함수의 의미와 연결 대상도 반드시 JSDoc에 작성됩니다.

여기서 솔직한 심정을 말하자면, "모든 선언에 5개의 태그를 쓰라"는 규칙을 인간에게 강제했다면, 아마 3일 만에 리뷰 현장은 아수라장이 되었을 것입니다. 함수를 하나 추가할 때마다 @graph-business를 한 문장 짜내고, @graph-connects를 빠짐없이 열거하고, 명명 규칙의 allowlist를 참조하고── 이를 매번 하는 것은 보통 힘든 일이 아닙니다.

이것이 성립할 수 있는 이유는, 코드를 작성하는 주체가 기본적으로 AI이기 때문입니다. AI에게 JSDoc 5개를 쓰는 노력은 코드 본체를 작성하는 노력에 비해 오차 범위 수준입니다. ESLint나 자동 리뷰가 피드백 루프(Feedback Loop)에 들어가 있다면 AI는 빠짐없이 태그를 작성할 것이고, 인간 리뷰어는 "태그가 사실과 일치하는가"만 확인하면 됩니다.

할루시네이션(Hallucination)이 발생하는 위치가 바뀐다

여기서 일어나고 있는 일을 다른 각도에서 보면, 할루시네이션의 위치가 바뀌고 있다는 이야기이기도 합니다. 저는 할루시네이션을 어디에 가두어 둘 것인가가 AI 하네스(Harness) 설계의 기본이라고 생각합니다.

다른 기사(Agentic Graph RAG)에서도 썼듯이, AI와 그래프를 결합한 시스템에서는 "할루시네이션은 사라지는 것이 아니라, 발생하는 위치가 바뀔 뿐"입니다. cpg의 경우, 그 위치는 다음과 같습니다:

  • 그래프 구축 단계 (graph construction phase): 할루시네이션 제로(Zero). 컨텍스트는 코드에 작성되어 있으며, ts-morph를 통한 AST 분석도 BigQuery로의 MERGE도 완전히 결정론적(Deterministic)입니다. LLM은 개입하지 않습니다.
  • 그래프 참조 단계 (graph reference phase): 할루시네이션 제로(Zero). MCP 툴은 BQ로부터 사실(Fact)만을 반환합니다.
  • JSDoc 기술 단계 (JSDoc description phase): 여기가 할루시네이션의 입구입니다. @graph-business가 사실과 일치하는지, @graph-connects의 열거가 누락되지 않았는지는 AI가 작성하는 이상 틀릴 여지가 있습니다.

다만, 입구는 자동 PR 리뷰로 단단히 봉쇄되어 있습니다. 태그 누락은 [Graph] Critical, 사실 오인은 [Doc] Critical로 차단됩니다. 코드가 틀렸다면 그것을 작성한 AI 자신이나, 다른 리뷰어 AI가 지적하여 수정합니다.

결과적으로, 그래프에 올라간 이후의 데이터는 "리뷰가 완료된 코드로부터 결정론적으로 추출된 사실"로서 다룰 수 있는 상태가 성립됩니다. 쿼리를 할 때마다 LLM이 새로 생성한 답이 아니므로, AI 에이전트가 cpg를 호출하여 돌아오는 노드(Node)나 에지(Edge)에 대해 "이것은 생성된 거짓말일지도 모른다"라는 가드(Guard)를 걸 필요가 없습니다. 사실만을 반환하는 툴로서 설계를 깔끔하게 확정 지을 수 있습니다.

빌드 ── ts-morph로 추출하여, 컨텍스트와 함께 그래프화

JSDoc이 단일 진실 공급원(SSoT, Single Source of Truth)으로서 작성되어 있다면, 나머지는 그것을 추출하여 그래프화하기만 하면 됩니다. 구현 방식은 다음과 같습니다:

  • ts-morph를 사용하여 JS/TS 코드를 AST (Abstract Syntax Tree) 분석: 모든 선언(함수 / 클래스 / 메서드 / 타입 / enum / 변수 / 식문 / export default 등)을 순차적으로 수집
  • JSDoc으로부터: 5가지 태그를 순차적으로 수집하고, @graph-* 태그를 추출하여 ParsedGraphTags 구조로 정규화
  • 노드 생성: qualifiedName = "<filePath>:<name>"를 ID로 하여 graph 노드 생성
  • 에지(Edge) 생성: @graph-connects 엔트리당 1개의 에지 생성. via: / cardinality 등의 메타데이터도 유지
  • 임베딩 (Embedding) 생성: @graph-business 텍스트를 Vertex AI Embedding (gemini-embedding-2)에 전달하여 벡터화
  • BigQuery에 로드: 모든 노드 / 에지를 cortex.product_graph_nodes / cortex.product_graph_edgesMERGE

@graph-business를 그대로 임베딩 입력으로 사용하고 있기 때문에, 자연어로 "KPI의 버그 발생률을 계산하는 코드"라고 물었을 때, 함수명에 "bug"나 "rate"가 포함되어 있지 않더라도 설명문의 의미적 유사성으로 검색이 가능해집니다.

전체 흐름은 다음과 같습니다. apps/ / infra/ / docs/의 3개 계통이 각각 파서(Parser)를 거치고, 제너레이터(Generator)에서 하나의 노드 집합으로 병합되며, 차분이 있는 노드만 Vertex AI에 전달되어 BigQuery에 저장됩니다.

빌드 비용은 거의 제로

빌드는 GitHub Actions를 통해 push to main 시 자동으로 실행됩니다. 차분 임베딩 (Differential Embedding)을 구현하여 다음과 같이 동작합니다:

  • BigQuery의 기존 노드의 textForEmbedding과 새로운 텍스트를 비교
  • 변경되지 않은 노드는 BigQuery의 기존 임베딩을 그대로 재사용
  • 변경된 노드만 Vertex AI에 전달

일반적인 push 시에는 수십 개의 노드 정도만 변경되므로, 비용은 $0.001 이하로 충분합니다. 전체 재생성(복구용, workflow_dispatch로 트리거)을 수행하더라도 8,000개 이상의 노드에 대해 약 $0.075 정도입니다.

왜 스토리지로 BigQuery를 선택했는가

"지식 그래프 (Knowledge Graph)"라고 하면 전용 그래프 DB (Neo4j, Neptune, Memgraph 등)를 구축하는 구성을 떠올리는 분들이 많을 것입니다. cortex에서는 BigQuery에 2개의 테이블 (product_graph_nodes / product_graph_edges)을 두는 것만으로 구성했습니다. 이유는 세 가지입니다:

  • 비용 구조 자체가 다름: 전용 그래프 DB는 "클러스터를 상시 가동하는 비용"이 하한선이 되지만, 현재 구현에서는 BigQuery가 스토리지 비용 + 쿼리한 만큼의 온디맨드 (On-demand) 쿼리 비용만 발생합니다. AI 에이전트가 상시 호출하므로 쿼리 비용이 무시할 수 없는 수준이 되겠지만, 그럼에도 서버를 24/7 가동하는 구성보다 명확하게 저렴하다는 점이 큽니다.
  • 벡터 검색 / 코사인 유사도 / SQL을 동일한 곳에서 작성 가능: BigQuery에는 VECTOR_SEARCHML.DISTANCE가 있어, @graph-business의 임베딩에 대한 시맨틱 검색 (Semantic Search)도 일반적인 JOIN/필터링과 동일한 쿼리 내에서 작성할 수 있습니다. cpg의 "시맨틱 검색 + 노드 속성으로 필터링 + 인접 노드 JOIN"이 단일 쿼리로 완결된다는 점이 큰 장점입니다.
  • Graph 기능이 GA(General Availability)되는 시점에 GQL(표준 그래프 쿼리 언어)로 전환하기 쉬움: BigQuery는 이미 Graph in BigQuery를 프리뷰(Preview)로 제공하고 있으며, GA 이후에는 기존 테이블에 graph view를 씌워 GQL로 MATCH (n)-[e]->(m)와 같은 질의를 수행할 수 있을 것으로 기대됩니다. 현재의 테이블 설계 그대로 향후 GQL로 전환할 수 있다는 점이 은근히 강력한 포인트입니다.

요컨대 "전용 그래프 DB의 강점(GQL)을 미래에 확보하면서도, 지금은 일반적인 BQ(BigQuery) 테이블로 운용할 수 있다"는 양측의 이점을 모두 취한 형태입니다. 일반적인 RAG 스택(pgvector / Pinecone 등)에 그래프용 DB를 추가하는 구성과 비교하면, 운용해야 할 시스템 수도 학습 비용도 명확하게 적게 듭니다.

이 부분은 재현 가능한 샘플로서 공개했습니다

지금까지의 "JSDoc 어노테이션을 AST 분석으로 그래프화하는" 부분은 최소 구성으로 재현할 수 있을 것이라 판단하여, 동작하는 샘플로서 공개했습니다:

@graph-*를 추출하여, { kind: "node", ... } / { kind: "edge", ... } 형태의 ndjson을 내뱉기만 하는 500행 미만의 작은 라이브러리입니다. pnpm run example로 동작하는 샘플이 포함되어 있습니다. 클론하지 않고도 샘플의 출력 이미지를 보고 싶은 분들을 위해, 빌드된 ndjson을 리포지토리에 그대로 올려두었습니다 → examples/sample/output.ndjson. 직접 테스트해 보고 싶은 분들은 이용해 주세요.

단, 이것은 어디까지나 코드를 graph화하는 부분일 뿐입니다. cortex의 진정한 가치는 여기에 docs와 DB 스키마를 동일한 그래프에 올리는 것에서부터 시작됩니다. 그 내용은 다음 장에서 다룹니다.

연결 — docs와 DB를 동일한 그래프에 올리기 (cortex 고유의 확장)

샘플 ndjson을 살펴보면, @graph-connects users [reads_from, via:id]usersraw string으로서 targetId에 들어가 있을 뿐입니다. 이것을 "단순한 문자열"로 남겨두지 않고, **users 테이블의 컬럼 정의, 파티션 정보, 컬럼 단위의 설명문을 가진 리치 노드(rich node)**로서 그래프에 가져오면 검색의 해상도가 한 단계 높아집니다.

cortex에서는 이를 세 가지 방향으로 수행합니다.

1. DB 스키마도 동일한 그래프의 노드로 만들기

cpg는 코드뿐만 아니라, cortex 내의 DB 스키마도 동일한 빌드 과정에서 함께 가져옵니다. 코드 측에서 @graph-connects users [queries, via:id]라고 작성된 users는, 빌드 시점에 컬럼 정의, 파티션, 설명문이 포함된 리치 Table 노드로 해결됩니다 (동일한 이름의 stub이 있다면, id를 유지한 채 내용물만 교체됩니다 — 에지는 깨지지 않습니다).

여기서 중요한 점은, 테이블/컬럼의 설명문을 AI에게 사후에 작성하게 하는 것이 아니라, Pulumi로 스키마를 정의하는 파일 내의 description 필드에서 그대로 흡수한다는 것입니다. Pulumi 측의 모습은 다음과 같습니다 (cpg 자체의 테이블 정의 발췌):

export const productGraphNodesTable = new gcp.bigquery.Table('cortex-prod-product-graph-nodes', {
  datasetId: 'cortex',
  tableId: 'product_graph_nodes',
  ...

테이블 단위의 description도, 컬럼 단위의 description도, Pulumi의 정의 그 자체가 그대로 의미 검색(semantic search)의 Embedding 입력이 됩니다. 즉, DB 측에서도 **스키마 정의 자체가 SSoT(Single Source of Truth, 단일 진실 공급원)**이며, cpg의 JSDoc과 동일한 사상 — "설명은 사물이 정의되어 있는 곳에 작성한다" — 가 관통되어 있습니다. 코드의 JSDoc을 수정하면 의미 검색이 수정되는 것과 마찬가지로, Pulumi의 description을 수정하면 의미 검색이 수정됩니다.

결과적으로, 코드 측에서 테이블로 향하는 에지를 한 홉(hop) 따라가면, 컬럼 정의까지 포함된 진짜 테이블 정보에 도달하게 됩니다.

2. docs를 디렉토리 규칙으로부터 자동 노드화

docs/ 하위의 Markdown 파일도 그래프에 올립니다. 메커니즘은 간단합니다. 디렉토리 구조를 규칙화함으로써, 각 docs 파일이 어떤 stack / domain에 속하는지를 기계적으로 결정할 수 있도록 했습니다:

docs/{category}/{name}.md

예시 (cpg 자체의 docs 기준):

  • docs/product-graph/README.md → stack: product-graph

, domain:Engineering

docs/code-graph/README.md

→ stack:code-graph

, domain:Engineering

docs/mcp/db-graph/README.md

→ stack:mcp-db-graph-server

, domain:Engineering

이 파일들을 **Document 노드 (Document node)**로서 그래프에 가져오고, code 노드 측의 @graph-stack과 일치하는 Document를 찾아 documented_by 에지 (edge)를 자동으로 생성합니다. 예를 들어 apps/graph/product/ 하위의 코드는 @graph-stack product-graph가 붙어 있으므로, docs/product-graph/README.md와 자동으로 링크됩니다. 코드를 변경하면 관련 docs가 자동으로 링크된 상태가 됩니다.

이를 통해, 예를 들어 AI 리뷰어가 "이 코드를 변경했는데, 관련 docs가 오래되지는 않았나?"를 그래프 1홉 (1-hop)으로 확인할 수 있게 됩니다 (Part 1에서 등장했던 [Doc] Critical 지적의 정체가 바로 이것입니다).

3. 인프라 정의도 노드화

infra/ 하위의 Pulumi 코드에도 @graph-*를 작성합니다. 예를 들어, cortex 자체의 graph 관련 인프라는 다음과 같은 형태입니다:

/**
* @graph-node {CronSchedule}
* @graph-stack code-graph
...

이렇게 하면 **CronSchedule 노드 (node)**로서 그래프에 포함되며, 트리거 대상인 CloudRunJob 노드와 triggers 에지로 연결됩니다. Pulumi의 정의 자체가 그래프의 입구가 되므로, "이 cron으로 실행되는 코드는 어디인가?"를 그래프를 통해 추적할 수 있습니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0