
46개 저장소의 컨텍스트를 AI를 위해 의미론적으로 검색 가능하게 만들기 (Part 2)
요약
46개의 코드 저장소를 하나의 지식 그래프로 통합한 후, 의미론적 검색이 불가능했던 엔트리 포인트 문제를 해결하는 과정을 다룹니다. DB 테이블 정보를 포함한 기존 db-graph의 성공 패턴을 코드 그래프에 적용하여 자연어 검색 기능을 구현합니다.
핵심 포인트
- 정적 분석 그래프와 AI 생성 컨텍스트를 결합하여 의미론적 검색 구현
- 모델에게 추론이 아닌 검증된 사실을 제공하는 것이 핵심 원칙
- db-graph의 성공적인 패턴을 코드 그래프(code-graph)에 이식
- DB 테이블 노드를 경계 노드로 활용하여 그래프 간 결합 시도
안녕하세요, airCloset의 CTO인 Ryan입니다.
Part 1에서 저는 정적 분석 (static analysis)을 통해 46개의 프로덕션 코드 저장소를 하나의 지식 그래프 (knowledge graph)로 통합하는 것에 대해 작성했습니다. 그래프 자체는 구축되었지만, 저는 포스트를 마무리하며 **네 가지 미결 과제 (open issues)**를 제시했습니다: 의미론적 검색 (semantic search)의 부재, 노드 폭발 (node explosion), 함수가 실제로 무엇을 하는지 알기 위해 파일을 직접 열어야 하는 문제, 그리고 새로운 경계 패턴 (boundary pattern)이 나타날 때마다 새로운 파서 (parser)를 작성해야 하는 비용 문제입니다.
이번 Part 2는 그중 첫 번째 문제인 엔트리 포인트 문제 (entry-point problem, 의미론적 검색 불가)를 어떻게 해결했는지에 관한 것입니다. 나머지 세 가지 문제는 Part 1에서 설명한 그대로 남아 있으며, 엔트리 포인트 문제가 해결된 후 새롭게 떠오른 문제들과 함께 마지막에 다시 다루겠습니다.
엔트리 포인트 문제부터 시작하는 이유는 간단합니다. 그래프가 존재하더라도 그곳에 도달하는 유일한 방법이 grep뿐이라면, 모델은 결국 추론 (inference)을 하게 됩니다. **"모델에게 추론이 아닌 검증된 사실을 제공하라"**는 핵심 원칙이 무너지는 것입니다. 따라서 엔트리 포인트 문제는 다른 문제들보다 먼저 해결되어야 했습니다.
힌트는 db-graph에 있었다
몇 달 전, 저는 이미 다른 도메인에서 동일한 구조적 문제를 해결한 적이 있습니다 — 바로 db-graph 프로젝트입니다.
내부적으로 우리는 많은 서비스에 걸쳐 방대한 수의 DB 테이블을 보유하고 있었고, 그 누구도 전체 그림을 파악하지 못하고 있었습니다. 서로 다른 사람들이 각기 다른 부분은 잘 알고 있었지만, 전체 지도는 누구의 머릿속에도 들어오지 않았습니다. 그래서 저는 db-graph를 구축했습니다: ORM 정의로부터 스키마 (schema)를 정적으로 추출하고, Gemini를 사용하여 테이블별 설명을 생성하며, 이를 768차원 벡터 (vector)로 임베딩 (embed)하여 그래프에 저장함으로써 전체 내용을 자연어로 의미론적 검색이 가능하게 만들었습니다.
해당 기사를 작성할 당시에는 991개의 테이블을 다루었습니다. 오늘날 이 프로젝트는 **21개 스키마 / 1,133개 테이블 / 10,815개 컬럼 (column)**에 걸쳐 있으며, 이제 사람들은 테이블 이름을 모르더라도 자연어로 데이터를 찾는 방식으로 업무를 수행하고 있습니다.
그곳에서 증명된 패턴은 다음과 같습니다:
정적 분석 그래프 (Static-analysis graph) + AI 생성 컨텍스트 (AI-generated context) = 자연어 의미론적 검색 (natural-language semantic search)의 작동
동일한 패턴을 코드 그래프 (code-graph)에 적용하기
DB 그래프 (db-graph)에서 작동했다면, 코드 그래프 (code-graph)에서도 작동해야 합니다. 그 생각이 머릿속에 떠오른 순간, 한 가지 사실을 깨달았습니다:
코드 그래프 (code-graph)는 이미 경계 노드 (boundary nodes)로서 "DB 테이블 노드 (DB table nodes)"를 포함하고 있다는 점입니다. — 이는 제가 Part 1에서 다루었던 경계 노드 유형 중 하나입니다.
따라서 제가 코드 그래프 (code-graph)와 DB 그래프 (db-graph)를 단순히 **결합 (join)**하기만 하면, 코드 그래프 (code-graph)는 자동으로 DB 그래프 (db-graph)의 의미론적 컨텍스트 (semantic context)를 상속받게 됩니다. 단 하나의 어노테이션 (annotation)도 작성하지 않고도, 기존의 자산만으로 그래프를 의미 있게 더 풍부하게 만들 수 있습니다.
이것이 바로 "그래프 결합 (joining graphs)"이라는 아이디어가 처음 등장한 지점입니다. 각 그래프를 독립된 섬으로 취급하는 것이 아니라, 그래프 간의 결합을 설계하는 것입니다.
하지만 API / 이벤트 (Event) / 페이지 (Page)에는 여전히 의미가 필요하며, 모든 함수에 어노테이션을 다는 것은 불가능합니다
DB 그래프 (db-graph)를 결합함으로써 DB 컨텍스트 (DB context)는 해결되었습니다. 하지만 남은 경계들 (API / 이벤트 (Event))과 그래프의 진입점 유형 (페이지 (Page))에는 여전히 의미가 부여되어야 합니다. 정적 분석 (Static analysis)만으로는 이러한 요소들로부터 의도 (intent)를 추출할 수 없으므로, 컨텍스트 (context)는 다른 곳에서 가져와야 합니다.
선택지는 명확했습니다: 어노테이션 (annotations)을 통해 코드에 의도를 직접 작성하는 것입니다 (이는 제가 AI Harness Series, Part 2에서 다루었던 cortex의 내부 지식 그래프 (internal knowledge graph)에서 사용된 것과 동일한 접근 방식입니다).
문제는 46개 저장소 (repos)에 있는 모든 함수에 어노테이션 (annotation)을 달 수는 없다는 점입니다. 함수는 수만 개에 달할 것입니다. 기존의 프로덕션 코드베이스 (production codebase)를 운영 중인 숙련된 팀에게 모든 것을 소급하여 어노테이션 (annotate)하라고 요구하는 것은 현실적이지 않습니다.
하지만 여기서 두 번째 깨달음이 찾아왔습니다:
중요한 것은 오직 경계 노드 (boundary nodes)뿐입니다. 따라서 경계 주변에만 어노테이션 (annotate)을 한다면 그것으로 충분합니다.
AI 에이전트가 "이 코드를 변경하면 무엇이 망가지나요?" 또는 "다른 저장소(repos)에서 이 API를 어떻게 호출하나요?"라고 물을 때, 에이전트에게 필요한 것은 함수별 로직 설명이 아닙니다. 에이전트에게 필요한 것은 **경계 의도 (boundary intent)**입니다. 즉, 이 화면의 용도는 무엇인지, 이 API가 무엇을 반환하는지, 이 이벤트(Event)가 비즈니스 상의 어떤 마일스톤을 나타내는지에 대한 정보입니다.
= 최소한의 어노테이션 (annotations), 최대한의 의미. 이것이 설계의 핵심이 되었습니다.
어노테이션 그래프 (annotation graph) 설계
이를 하나로 통합하면 (내부적으로 우리는 이 어노테이션 그래프를 서비스-제품-그래프 (service-product-graph), 또는 SPG라고 부릅니다):
세 개의 그래프가 SAME_ENTITY 엣지 (edges)로 연결되어 대등한 관계로 존재합니다. 계층 구조는 없습니다. 어떤 그래프에서 시작하더라도 다른 그래프에 도달할 수 있습니다.
- 코드-그래프 (code-graph, 구조) — 정적 분석 (static analysis)을 통해 얻은 함수 / 클래스 / 경계 노드 (boundary nodes) (46개 저장소)
- DB-그래프 (db-graph, DB 컨텍스트) — 의미론적으로 기술된 1,133개의 테이블
- 어노테이션 그래프 (annotation graph, 의도) — 경계 주변에만 작성된
@graph-*태그
AI 에이전트의 진입점은 세 개의 그래프를 모두 탐색하는 단일 **MCP 서버 (MCP server)**입니다. AI 에이전트는 DB-그래프에 직접 접근하지 않습니다. 어노테이션 그래프의 MCP 서버가 에이전트를 대신하여 DB-그래프 호출을 프록시 (proxy) 합니다.
어노테이션 그래프에는 Page / Section / Dialog / Field / Action / Api / Task의 7가지 노드 유형이 있습니다. 초기 버전은 화면 중심이었기에 screen-graph라고 불렸으나, 백엔드 Api / Task까지 커버하도록 확장되면서 service-product-graph로 이름을 변경했습니다.
어노테이션 예시
어노테이션이 어떻게 생겼는지 보여주는 예시입니다 (가상의 예시이지만 실제 형태와 유사합니다):
/**
* @graph-page /home
* @graph-business 메인 화면. 멤버는 현재 대여 중인 항목을 확인하고, 아이템을 구매하며, 반납을 시작할 수 있습니다.
...
여기서 중요한 두 가지가 있습니다:
- **
@graph-business**는 의도(intent) 텍스트를 담고 있습니다 (실제 코드베이스에서는 일본어로 작성되어 있습니다). 이것이 바로 벡터화(vectorized)되는 대상이며, 의미론적 검색 (semantic search)의 실체입니다. - **
@graph-flow/@graph-status**는 이것이 멤버 라이프사이클(회원 가입 → 월간 구독 → 스타일링 루프 → 해지 등) 중 어디에 위치하는지, 그리고 어떤 멤버 세그먼트를 위한 것인지를 나타냅니다. 이들은 의미의 두 번째 차원을 더해줍니다: "이 화면은 월간 구독 멤버를 위한 스타일링 루프 내에서 나타납니다."
또한 @graph-case(테스트 케이스가 파생되는 조건부 패턴 태그)도 있지만, 그것은 다음에 다루겠습니다.
일상적인 개발 워크플로우를 방해하지 않고 어노테이션(Annotations) 실행하기
여기서부터 실무적인 부분이 시작됩니다.
어노테이션 그래프 (annotation graph)를 구축하기로 결정했을 때, 다음과 같은 제약 사항들이 있었습니다:
- 엔지니어들은 인간의 코드 리뷰 (human code review)와 함께 일반적인 제품 개발을 진행합니다.
- AI 리뷰는 아직 모든 저장소(repo)에 연결되어 있지 않습니다. Cortex의 완전 자동화된 리뷰(AI Harness Series, Part 6에서 다룸)는 Cortex 모노레포 (monorepo) 내부에서만 작동합니다.
- 일반적인 리뷰 업무량에 더해 인간에게 어노테이션 리뷰까지 요청하는 것은 불가능한 일입니다.
- 심지어 동일한 PR 내에서 "인간은 코드를 리뷰하고, AI는 어노테이션을 리뷰한다"와 같이 역할을 나누는 것도 두 개의 리뷰 스트림을 하나로 섞어버려 모두를 혼란스럽게 만듭니다.
다시 말해: 동일한 PR 내에서 인간과 AI를 섞지 마세요.
해결책은 어노테이션을 별도의 브랜치(branch)로 물리적으로 분리하는 것이었습니다.
main브랜치는 건드리지 않습니다. 엔지니어의 일반적인 워크플로우(flow)는 이전과 정확히 동일하게 유지됩니다.- AI 전용 영역인 별도의 **주석 브랜치 (annotation branch)**를 구축합니다.
main브랜치에 변경 사항이 생기면 웹훅(webhook)이 실행됩니다.- 주석 브랜치는 차이점(diff)에 대한 **주석 생성 (generating)**과 이를 **검토 (reviewing)**하는 작업을 처리합니다. AI가 이 모든 과정을 엔드 투 엔드(end-to-end)로 수행합니다.
- 엔지니어 입장에서는
main브랜치만 다루며, 주석이 존재하는지조차 알 필요가 없습니다.
이는 AI Harness Series, Part 6에서 언급된 "모든 코드 라인이 AI 게이트를 통과한다"는 이상향을 기존 조직의 제약 조건에 맞춰 변형한 것입니다. cortex(내부 AI 플랫폼)는 제가 처음부터 조립한 모노레포(monorepo)이므로, "모든 커밋이 AI 게이트를 통과한다"는 원칙이 실제로 적용됩니다. 하지만 46개의 저장소로 구성된 프로덕션 시스템에서는 그 전제 조건이 성립하지 않습니다. 그래서 저는 그 이상향을 포기하는 대신 분리했습니다. 즉, 엔지니어의 워크플로우는 한 브랜치에서, AI의 주석 워크플로우는 다른 브랜치에서 병렬로 실행되도록 한 것입니다.
SLO를 통한 그래프 간 일관성 보호
주석 파이프라인을 실행하는 것만으로는 세 가지 그래프(코드 그래프 / DB 그래프 / 주석 그래프) 간의 **조인 품질 (quality of the joins)**을 보장할 수 없습니다. 따라서 전체 그래프에 걸친 일관성을 자동으로 확인하는 일련의 SLO(서비스 수준 목표)가 존재합니다.
주요 규칙은 다음과 같습니다:
- API 체인 연결성 (API chain connectivity) —
HANDLES_API핸들러의 최소 **95%**는 하위 함수 호출(downstream function calls)을 가져야 합니다. (= API를 수신한 후 아무것도 하지 않는 핸들러가 없어야 함) - DB 액세스 완전성 (DB access completeness) — DB 읽기/쓰기 엣지(edge)의 최소 **80%**는 db-graph의 컬럼 노드(column nodes)와 조인되어야 합니다. (= 코드 그래프의 DB 경계가 db-graph의 의미와 연결되어야 함)
- 이벤트 필드 해석 (Event field resolution) — 이벤트 엣지(Event edges)의 최소 **70%**는 필드 수준의 정보를 포함해야 합니다.
- 모호한 엣지 없음 (No ambiguous edges) — 이름 해석이 모호한 엣지(name-resolution-ambiguous edges)는 0이어야 합니다. (심각도: 에러)
이것들은 사실상 "경계(boundaries)들이 서로 연결되어야 하지 않을까?"라는 단순한 질문을 SLO(Service Level Objective)로 변환한 것에 불과합니다. 임계값(threshold) 아래로 떨어지는 항목이 발생하면 알람이 울리며, 이를 통해 전체 그래프의 신뢰성을 매일 방어하게 됩니다.
Part 1에서 다룬 일일 경계 분석 크론(cron) 작업(연결률 5% 하락 시 알람)은 코드 그래프(code-graph)에만 국한된 것이었습니다. 이것은 **교차 그래프 SLO (cross-graph SLO)**로, 그래프들 사이의 조인(join)을 보호합니다. 특정 저장소(repo)에 파서를 추가하거나, 새로운 어노테이션(annotation)을 작성하거나, 스키마(schema)를 변경하는 등 어떤 일이 일어나더라도, 다음 날 아침이면 모든 조인에서의 품질 저하를 확인할 수 있습니다.
SAME_ENTITY 브릿지를 통한 정적 그래프(Static Graph)와 어노테이션 그래프(Annotation Graph)의 조인
지금까지
운영상의 실수(footgun)도 한 가지 있었습니다. 초기 구현에서는 중복을 피하기 위해 INSERT NOT EXISTS를 사용했습니다. 하지만 BigQuery의 스트리밍 버퍼(streaming-buffer) 가시성 지연(visibility lag)으로 인해 중복 데이터가 유입되었습니다. 한 저장소에서는 하룻밤 사이에 엣지(edge) 수가 106개에서 214개로 두 배로 늘어나기도 했습니다. 저희는 작업을 멱등적(idempotent)으로 만들기 위해 MERGE INTO로 다시 작성하여 이 문제를 해결했습니다.
결과: "구독료 계산"을 통해 그래프로 진입하기
이 모든 준비가 완료되면서, Part 1의 마지막에서 다루었던 진입점(entry-point) 문제가 마침내 해결되었습니다:
"회원들의 구독료 계산(the subscription-fee calculation)이 잘못된 것 같다"
이러한 자연어 쿼리(natural-language query)를 어노테이션 그래프(annotation graph)에 던지면, 벡터 검색(vector search)이 관련 노드들(Page / Api / Function / DB table)을 사실(facts)로서 반환합니다. 거기서부터 SAME_ENTITY는 다른 저장소의 호출자(callers) 및 피호출자(callees)를 포함한 코드 그래프(code-graph) 함수들로 여러분을 안내합니다. 코드 그래프의 DB 경계(DB boundaries)를 통해 db-graph로 넘어가 관련 컬럼(columns)들을 가져올 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기