문서 이동 시에도 깨지지 않는 링크
요약
문서의 파일 경로가 변경되더라도 링크가 깨지지 않도록 '정체성(Identity)' 기반의 링크 시스템을 구축하는 방법을 설명합니다. Git의 커밋 디프(commit diff)를 활용하여 파일 이동을 감지하고, 고유한 fileId를 유지함으로써 안정적인 딥링크를 구현하는 기술적 접근법을 다룹니다.
핵심 포인트
- 경로(Path) 대신 고유 식별자(Identity)를 기반으로 링크 구축
- 파일 이동 시에도 유지되는 안정적인 fileId 설계
- Git 커밋 히스토리를 활용한 파일 이동 감지 및 ID 상속
- Docs-as-code 환경에서의 링크 안정성 확보
규모가 커졌을 때 직접 겪어보기 전까지는 사소해 보이는 문제가 하나 있습니다. 문서 페이지를 발행하고, 에이전트(Agent)가 이를 인용하고, 누군가 북마크를 해두었는데 — 그 후 작성자가 파일 이름을 바꾸거나 다른 폴더로 이동시키는 상황입니다. 내용은 동일합니다. 하지만 링크는 죽어버립니다.
Docs-as-code (문서의 코드화) 환경에서는 이런 일이 끊임없이 발생합니다. 파일이 재구성되고, 폴더 구조가 변경되며, getting-started.md가 onboarding/intro.md로 바뀌기도 합니다. 이러한 모든 이동은 이전 경로를 가리키던 링크, 인용, 그리고 모든 에이전트의 답변을 조용히 깨뜨립니다. 만약 당신의 링크가 단순히 "파일 경로"라면, 그 링크는 파일 경로만큼만 안정적입니다. 즉, 전혀 안정적이지 않다는 뜻입니다.
우리는 소스 제어(Source control) 내에서 콘텐츠가 이동하더라도 유효하게 유지되는 링크가 필요했습니다. 우리가 이를 어떻게 구축했는지 소개합니다.
핵심 아이디어: 경로가 아닌 정체성 (Identity)
경로는 "위치"이지 "정체성"이 아닙니다. 해결책은 모든 문서에 콘텐츠와 함께 이동하는 안정적인 정체성을 부여하고, 경로 대신 그 정체성을 기반으로 공개 링크를 구축하는 것입니다.
따라서 딥링크(Deeplink)는 다음과 같은 형태를 띱니다:
폴더 구조도 없고, 파일 이름도 없으며, 확장자도 없습니다. 오직 두 가지의 안정적인 식별자만 존재합니다: 해당 문서가 어떤 콘텐츠 소스에서 왔는지, 그리고 문서 자체를 고유하고 영구적으로 명명하는 fileId입니다. 파일을 원하는 곳 어디로 이동하더라도 링크는 여전히 연결됩니다. 왜냐하면 링크는 파일이 어디에 살고 있었는지와 관련이 없었기 때문입니다.
전체적인 요령은 이동 중에도 fileId를 안정적으로 유지하는 것입니다. 여기서 git의 가치가 드러납니다.
git으로부터 안정적인 file id 도출하기
단순한 방식은 쉽습니다. 파일 경로를 해싱(Hash)하는 것입니다.
fileId = sha256(repositoryFilePath);
이렇게 하면 깔끔하고 결정론적인(Deterministic) ID를 얻을 수 있지만, 우리가 피하려고 했던 바로 그 결함이 발생합니다. 파일을 이동하면 해시값이 바뀌고, 따라서 새로운 ID와 새로운 링크를 얻게 됩니다. 쓸모가 없게 되는 것이죠.
진정한 작업은 "새로운" 경로가 사실은 단순히 이동한 기존 문서임을 감지하고, 그 ID를 계속 유지(carry forward)하는 것입니다. Git은 이미 이 방식을 알고 있습니다. 모든 커밋 디프 (commit diff)는 이름 변경을 source → target 쌍으로 기록합니다. 따라서 경로를 개별적으로 해싱하는 대신, 커밋 히스토리 (commit history)를 따라가며 디프를 통해 무엇이 이동했는지 파악합니다.
한 커밋에서 다음 커밋으로 이동하며, 모든 변경 사항에 대해 다음과 같이 질문합니다: 이 파일이 이전에 다른 경로로 존재했었는가?
const changes = commitDiff.changes || [];
for (const change of changes) {
const previousPath = change.sourceServerItem; // 이전에 존재하던 위치
...
핵심 라인은 ID를 유지하는 것입니다. 파일이 이동할 때, 그 새로운 경로는 기존 경로의 ID를 상속받습니다. 문서는 이동 중에도 정체성을 유지하며, 따라서 링크도 유지됩니다.
이 커밋에서 변경되지 않은 파일들은 기존에 가지고 있던 ID를 그대로 유지합니다:
for (const item of allFilesAtThisCommit) {
if (!pathToId.has(item.path)) {
pathToId.set(item.path, previousPathToId.get(item.path) ?? sha256(item.path));
...
히스토리를 따라 커밋별로 이 작업을 수행하면, ID가 현재 위치가 아닌 문서의 계보 (lineage)에 고정된 path → fileId 맵 (map)을 얻게 됩니다.
까다로운 사례 처리하기
단순한 구현 방식이 실패하기 쉬운 두 가지 실제 사례를 언급할 가치가 있습니다:
충돌 (Collisions). 예외적인 상황에서 서로 다른 두 문서가 해시에서 유도된 동일한 ID로 결정될 수 있습니다. 우리는 중복을 감지하고, 충돌하는 항목에 커밋 ID를 접두사로 붙여 모호함을 해소함으로써 두 개의 서로 다른 문서가 절대 동일한 링크를 공유하지 않도록 합니다:
if (duplicateFileIds.has(fileId) && sha256(repoFilePath) === fileId) {
pathToId.set(repoFilePath, targetCommitId + fileId);
}
비용 (Cost). 대규모 저장소(Repository)에서 커밋 차이(Commit diffs)를 탐색하는 것은 공짜가 아닙니다. 이는 파일 내용이 아닌 커밋 데이터가 비용이 많이 드는 부분이라는 선택적 페칭(Selective fetching)의 사례와 같은 교훈을 줍니다. 따라서 이 유도 과정은 캐시된 불변의 커밋 메타데이터(Immutable commit metadata)에 의존하며, 마지막으로 처리된 커밋과 현재 커밋 사이의 차이(Diff)만 처리합니다. 히스토리에 대한 비용은 한 번만 지불하면 되며, 그 이후에는 점진적으로 이동합니다.
루프 닫기: 사용되는 곳에 링크 임베딩하기
안정적인 ID는 그것이 실제로 소비자(Consumer)에게 도달할 때만 유용합니다. 에이전트(Agent)를 지원하는 지식 저장소(Knowledge store)에 문서를 게시할 때, 우리는 딥링크(Deeplink)를 콘텐츠 내부에 직접 찍고 그와 함께 메타데이터로도 저장합니다:
const deeplink = `https://${host}/cid/${contentId}/fid/${fileId}`;
fileContents = `${fileContents}\n\ndocument_link:${deeplink}`;
filePathToDeepLink[blobPath] = deeplink;
또한 문서당 하나의 작은 사이드카 레코드(Sidecar record) — {fileId}.metadata.json — 를 작성하여, 누가 마지막으로 수정했는지와 언제 수정했는지를 기록합니다:
const sidecar = {
metadataSchemaVersion: '1',
lastUpdatedBy: change.sourceLastUpdatedBy ?? '',
...
이제 에이전트가 이러한 문서 중 하나를 바탕으로 질문에 답변할 때, (a) 파일이 소스 제어(Source control) 내의 어디에 위치하든 상관없이 실제 페이지를 가리키고, (b) 출처(Provenance) — 마지막 편집자, 마지막 편집 시간 — 를 함께 제공하는 링크를 인용할 수 있습니다. 이를 통해 답변의 신뢰성을 확보하고 추적할 수 있습니다.
이것이 중요한 이유
변화는 작지만 보상은 큽니다. 파일 경로를 링크로 취급하는 것을 중단하십시오. 각 문서에 Git 계보(Git lineage)에서 유도된 내구성이 있는 정체성(Durable identity)을 부여하고, 해당 정체성을 기반으로 공개 링크를 구축하며, 인간과 에이전트 모두를 포함한 소비자들이 실제로 이를 접하는 곳에 임베딩하십시오.
콘텐츠는 소스 제어 내에서 자유롭게 이동할 수 있습니다. 작성자는 아무런 고민 없이 구조를 재편성할 수 있습니다. 그리고 모든 링크, 인용, 북마크는 항상 올바른 페이지를 가리킵니다. 애초에 경로(Path)를 가리키고 있었던 것이 아니기 때문입니다.
_이 글은 Docs-as-code at scale 시리즈의 세 번째 파트입니다:*
- 문서 빌드를 위해 전체 리포지토리를 클론하는 것을 중단하세요
- 선택적 문서 가져오기(selective doc fetching)에서 비용이 많이 드는 부분은 파일이 아니라 커밋(commits)입니다
- 문서 이동 시에도 깨지지 않는 링크 (현재 위치)
Sai Pramod Upadhyayula는 Microsoft에서 AI 기반 엔터프라이즈 지식 플랫폼을 담당하는 시니어 소프트웨어 엔지니어(Senior Software Engineer)이며, DocFX 오픈 소스 생태계의 기여자입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기