선택적 문서 가져오기에서 비용이 많이 드는 부분은 파일이 아니라 커밋입니다
요약
대규모 리포지토리에서 문서를 선택적으로 가져올 때 발생하는 병목 현상이 파일 내용이 아닌 커밋 히스토리 탐색에 있음을 밝힙니다. 이를 해결하기 위해 파일 대신 불변하는 커밋 메타데이터를 캐싱하고, 커밋 디프(diff)를 활용한 점진적 업데이트 방식을 제안합니다.
핵심 포인트
- 병목 현상의 원인은 파일 다운로드가 아닌 커밋 그래프의 재귀적 탐색임
- 파일 콘텐츠 대신 불변하는 커밋 메타데이터를 캐싱하는 것이 효율적임
- 커밋 디프를 활용해 전체 히스토리 탐색 없이 증분 업데이트 가능
- 메타데이터 캐싱은 RAG 및 에이전트의 지식 베이스 최신성 유지에 도움
지난 포스트에서 저는 가져오기 전에 해결하기를 주장했습니다. 즉, 200,000개의 파일을 가져오기 전에 문서 매니페스트 (doc manifest)를 사용하여 중요한 50개의 파일이 무엇인지 파악하라는 것입니다. 이는 명백한 낭비를 해결해 줍니다. 하지만 수십 개의 대규모 리포지토리 (repos)에 걸쳐 이를 실제 운영 환경에 적용하고 — Azure DevOps 및 콘텐츠 버전 제공자 팀과 긴밀히 협력하면서 — 우리는 진짜 비용이 제가 예상하지 못한 곳에 숨어 있다는 것을 발견했습니다.
그것은 파일 내용이 아니었습니다. 바로 커밋 (commit) 데이터였습니다.
놀라운 사실: 커밋 히스토리를 탐색하는 것이 병목 현상입니다
Git 제공자에게 "무엇이 변경되었고, 어떤 버전인가요?"라고 물을 때, 여러분은 저렴한 호출을 한 번 하는 것이 아닙니다. 커밋 데이터는 여러 계층에 걸쳐 있습니다. 트리 (trees)는 트리를 참조하고, 커밋 (commits)은 부모 (parents)를 참조하며, 단순한 탐색 (naive walk)은 깊은 재귀적 순회 (recursive traversal)로 확산됩니다. 50개의 마크다운 (markdown) 파일이 필요한 빌드를 위해, 우리는 파일들을 다운로드하는 데 시간을 쓰는 것이 아니라, 그 주변의 커밋 그래프 (commit graph)를 해결하는 데 대부분의 시간을 소비하고 있었습니다.
콘텐츠 가져오기 (content fetch)는 쉬운 부분이었습니다. 버전 해결 (version resolution)이 세금(tax)이었습니다.
해결책: 콘텐츠가 아닌 메타데이터를 캐싱하세요
본능적으로는 파일 콘텐츠를 캐싱 (cache)하고 싶어집니다. 우리는 그 반대로 했습니다.
커밋 데이터에는 한 가지 아름다운 속성이 있습니다: 그것은 불변 (immutable)입니다. 커밋 SHA는 영원히 정확히 하나의 트리를 가리킵니다. 따라서 intro.md의 바이트 (bytes)를 캐싱하는 대신 (이는 변경되며, 우리는 최신 상태를 유지하기를 원합니다), 우리는 커밋 메타데이터 (commit metadata) — 즉 트리 구조, 파일-to-블롭 매핑 (file-to-blob mappings), 버전 그래프 (version graph)를 캐싱합니다.
패턴은 다음과 같습니다:
- 단 한 번의 전체 재귀적 가져오기 (recursive fetch). 리포지토리당 단 한 번만 깊은 탐색 (deep-walk) 비용을 지불하고, 결과로 나온 커밋/트리 메타데이터를 저장합니다.
- 디프 (diffs)를 사용하여 점진적으로 빌드하기. 그 이후에는 다시는 전체 히스토리를 탐색하지 마십시오. 마지막으로 알려진 SHA 이후의 커밋 디프 (commit diff)만 요청하고, 캐싱된 메타데이터를 앞으로 패치 (patch)해 나갑니다.
비싼 재귀적 탐색 (recursive traversal)이 저렴한 증분 업데이트 (incremental update)로 변합니다. 그리고 캐싱된 레이어는 불변 메타데이터 (immutable metadata)이기 때문에, 무효화 (invalidation)에 대한 골칫거리가 없습니다. 기존의 것을 무효화하는 것이 아니라, 새로운 커밋에 대한 지식을 오직 _추가 (appending)_할 뿐이기 때문입니다.
예상치 못한 보너스: 정직한 최신성 (freshness)
이 캐싱 레이어는 단순히 시간을 절약하는 것 이상의 역할을 한다는 것이 밝혀졌습니다. 그것은 최신성을 정직하게 만들었습니다.
만약 RAG 파이프라인 (RAG pipeline)이나 에이전트 (agent)의 지식 베이스 (knowledge base)에 문서를 공급하고 있다면, 가장 무서운 실패 모드 (failure mode)는 오래되었거나 관련 없는 파일에서 가져온 답변입니다. 커밋 디프 (commit diff)는 이를 위한 가장 깔끔한 신호입니다. 버전 레벨에서 어떤 문서가 이동했는지, 추가되었는지, 또는 삭제되었는지를 정확하게 알려줍니다. 이제는 맹목적인 일정에 따라 재인덱싱 (re-indexing)을 하는 대신, 실제 변경 사항에 따라 재인덱싱을 시작하게 됩니다. "이 문서가 여전히 최신인가?"라는 질문은 다시 가져오기 (re-fetch)를 하는 대신 {repo, commitSha, path}에 대한 저렴한 조회 (lookup)가 됩니다.
다르게 나타나는 문제점: 모노레포 (monorepo) vs. 수많은 작은 레포지토리 (many small repos)
조직이 구조를 어떻게 잡느냐에 따라 동일한 문제가 정반대의 형태로 나타납니다.
모노레포 (monorepo)의 경우, 어려운 점은 _정밀도 (precision)_입니다. 하나의 커밋 그래프 (commit graph)가 있지만, 문서들은 20만 개의 파일 사이에 파묻혀 있습니다. 이때 커밋 디프 (commit diff)는 당신의 조력자가 됩니다. 하나의 캐싱된 트리 (tree)와 디프 (diff)만 있으면 그 파일들 중 정확히 어떤 것이 변경되었는지 알 수 있으며, 매니페스트 (manifest)의 src 스코핑 (scoping)과 exclude 패턴은 탐욕적인 **/*.md 패턴이 변경 로그 (changelog)나 아카이브된 폴더까지 휩쓸어가지 않도록 막아줍니다. 모든 것이 하나의 버전을 공유하기 때문에 최신성을 유지하는 것은 비교적 쉽습니다.
하나의 조직 아래 있는 수많은 레포지토리 (many repos)의 경우, 이는 조정 (coordination) 및 출처 (provenance) 문제입니다. 50개의 레포지토리, 50개의 커밋 그래프, 50개의 매니페스트가 존재하며, 어떤 것들은 매니페스트가 아예 없을 수도 있습니다. 바로 이 지점에서 캐싱된 커밋 메타데이터 (cached commit metadata)가 제값을 합니다. 매번 빌드할 때마다 50개의 히스토리를 다시 훑는 대신, 각 레포지토리를 저렴하게 디프 (diff)함으로써 "이들 전체에서 무엇이 변했는가?"라는 질문에 답할 수 있습니다. 저희에게 도움이 되었던 몇 가지 사항은 다음과 같습니다:
- 저장소별 매니페스트 (per-repo manifest)를 "여기서 문서란 무엇인가"에 대한 정의로 신뢰하세요. 전역적인 글로브 (glob) 패턴을 강요하지 마세요. 그것이 저장소 경계를 넘어 무관한 파일들이 유출되는 방식입니다.
- 모든 것을
{repo, commitSha, path}로 고정 (Pin)하고, 타이머가 아닌 커밋 디프 (commit diff) 발생 시 재해결 (re-resolve) 하세요. 데이터의 노후화 (Staleness)는 거의 항상 재인덱싱 주기 (re-index-cadence)의 문제이지, 가져오기 (fetch)의 문제가 아닙니다. - 매니페스트가 없는 저장소는 명시적으로 범위 외 (out-of-scope)로 취급하세요. "그냥 전부 인덱싱하자"라는 폴백 (fallback) 방식은 하류 (downstream) 단계에서 무관한 답변을 생성하는 가장 큰 원인입니다.
핵심 요약 (The takeaway)
선택적 가져오기 (Selective fetching)를 통해 200,000개의 파일을 50개로 줄일 수 있습니다. 하지만 전체 파이프라인의 속도와 신뢰성 (trustworthiness)은 그 아래의 계층에서 나옵니다. 즉, 불변의 커밋 메타데이터 (immutable commit metadata)를 한 번 캐싱한 다음, 커밋 디프 (commit diff)를 지속적으로 활용하는 것입니다. 모노레포 (Monorepos)는 더 나은 필터링을 지향하게 만들고, 멀티레포 (many-repos)는 더 나은 출처 (provenance)를 지향하게 만듭니다. 커밋 디프 (commit-diff) 계층은 이 두 가지 모두를 빠르게 만들며, 에이전트 (agent)가 자신의 문서가 실제로 최신 상태임을 알 수 있게 해줍니다.
동일한 캐싱된 커밋 히스토리 (cached commit history)는 다른 기능도 지원합니다. 바로 파일이 이동하거나 이름이 변경되어도 깨지지 않는 문서 링크입니다. 이는 이 시리즈의 다음 포스트에서 다룰 주제입니다.
이 글은 Docs-as-code at scale 시리즈의 파트 2입니다:
- 문서 빌드를 위해 전체 저장소를 클론하는 것을 중단하세요
- 선택적 문서 가져오기에서 비용이 많이 드는 부분은 파일이 아니라 커밋입니다 (현재 위치)
- 문서가 이동해도 깨지지 않는 링크
Sai Pramod Upadhyayula는 Microsoft의 시니어 소프트웨어 엔지니어로 AI 기반 엔터프라이즈 지식 플랫폼을 담당하고 있으며, DocFX 오픈 소스 생태계의 기여자입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기