-
repo마다 5종 자동 페이지 — 단촐한 위키를 결정적으로 채우기IT 2026. 6. 3. 21:00
22개 repo의 위키를 만들어 놓고 봤더니 한 가지가 마음에 안 들었다. "단촐하다." 내용이 풍부한 Rich 그룹 5개 repo만 8개 섹션 표준에 가까웠고, 거의 빈 Bare 그룹 4개 repo는 dependencies.md 한 페이지뿐이다. 같은 도구로 인덱싱했는데 격차가 너무 컸다. DeepWiki.com(Cognition)·Google CodeWiki는 한 repo당 평균 8개 섹션(Overview / Structure / Architecture / API / Subsystems / Operations / Testing / Glossary) + 페이지당 mermaid 3-5개를 floor로 두는데, 우리는 한참 부족했다.
해결책은 단순해 보였다 — "자동 생성 페이지 종류를 늘리자." 그런데 어떻게 늘릴까? gemma에게 더 많이 던지면 환각 비용이 누적된다. 사람이 더 많이 쓰면 22 repo × 5 페이지 = 110페이지를 1인이 유지하는 게 불가능하다. 답은 "5종 자동 페이지 + 결정적/LLM 혼합 + HTML 마커 idempotent 주입"이라는 한 정책으로 모였다.
1. 배경 — 단촐의 원인은 정확히 무엇인가
단촐의 원인을 따져 보니 두 가지로 갈렸다. (a) Rich 그룹조차 자동 카탈로그가 부재 — architecture는 있는데 API 카탈로그·config 카탈로그·internal module 그래프는 어디에도 없었다. (b) Bare 그룹은 수기 서술 자체가 없다 — README도 부족한 repo에 누가 architecture.md를 쓸 시간을 내겠나.
두 원인은 다른 답을 요구한다. Rich 그룹은 자동 카탈로그를 추가해서 격차를 메우고, Bare 그룹은 결정적 추출만으로도 5페이지를 채워준다. 한 가지 자동화로 두 문제를 동시에 풀려면 "모든 repo에 5종 자동 페이지 표준을 둔다"는 정책이 필요했다.
2. 5종 페이지 — 무엇이고, 왜 필요하고, LLM은 어디에
먼저 5종이 각각 무엇이고 왜 필요한지부터 풀어 보자. 다섯 페이지는 우연히 모인 게 아니라 "이 repo를 처음 보는 사람(또는 AI 에이전트)이 가장 먼저 알아야 하는 네 가지 + 용어 한 종"이라는 기준으로 골랐다.
① dependencies.md — "이 repo는 누구에게 기대고, 누가 이 repo에 기대는가"다. 이 repo가 호출하는 다른 repo(out-edge)와 이 repo를 쓰는 다른 repo(in-edge)를 표로 보여준다. 22개 repo가 서로 얽혀 있으면 한 곳을 고쳤을 때 어디가 깨지는지 추측에 의존하게 되는데, in-edge 목록이 그 변경 영향 범위(blast radius)를 즉답한다. "tool_bridge 시그니처를 바꾸면 영향받는 곳이 어디지?"에 grep 없이 답이 나온다.
② internal-dependencies.md — "repo 안에서 모듈끼리 어떻게 얽혀 있는가"다. repo 내부 모듈 간 import 관계를 mermaid 그래프로 그린다. 파일이 스무 개를 넘으면 "어디부터 읽어야 하나"가 막막하고 진입점·허브 모듈이 안 보이는데, 이 그래프가 코드 구조의 지도가 된다. 새로 합류한 사람이나 코드를 처음 읽는 AI 에이전트의 onboarding을 단축하고, 순환 의존도 한눈에 드러난다.
③ api.md — "이 repo가 바깥에 노출하는 표면"이다. HTTP route, CLI 명령, systemd 서비스, public 함수 시그니처를 모은다. "이 서비스 엔드포인트가 뭐였지?"를 매번 코드에서 grep하던 비용을 없애고, AI 에이전트가 이 repo의 도구를 호출하거나 통합할 때 참조하는 계약(contract) 역할을 한다.
④ config-catalog.md — "건드릴 수 있는 손잡이 목록"이다. env var, pyproject 설정, systemd 환경값의 이름·기본값·역할을 한자리에 모은다. 설정은 코드 곳곳에 흩어지기 마련이라 "무엇을 어떻게 바꿀 수 있는지" 전체 그림이 없으면 운영 중 튜닝이나 장애 대응이 더뎌진다. 카탈로그가 있으면 "어떤 env로 동작을 바꾸지?"에 바로 답하고, 코드와 문서가 어긋나는 설정 drift도 드러난다.
⑤ glossary.md — "이 repo만의 용어 사전"이다.
tier·blast radius·context_pack처럼 repo 고유 용어 5~15개를 gemma가 README·SKILL.md·docs에서 뽑아 정의한다. 내부 용어를 모르면 문서 전체가 외계어로 읽히는데, 용어집이 그 진입장벽을 낮추고 RAG 질의의 검색 앵커가 된다.이제 LLM이 어디에 쓰이는지 정직하게 짚자. 처음엔 결정적 페이지를 "LLM 0%"·"LLM 미사용"이라고 적었는데, 그건 어폐가 있다. 표의 데이터는 결정적으로 뽑혀도, 사람이 읽을 문장과 요약은 결국 누군가 써야 하기 때문이다. 실제 모델은 더 단순하다 — 다섯 페이지 모두 결정적 지식(graph DB·AST에서 뽑은 사실)을 ground truth로 깔고, 그 위에서 LLM이 서술·요약을 얹는다. 차이는 "LLM 유무"가 아니라 "LLM이 얼마나 자유롭게 쓰느냐"다. (a) dependencies·internal-dependencies는 결정적 그래프가 본체이고 LLM은 그 관계를 한두 문단으로 요약만 한다 — LLM은 graph DB에 없는 의존을 지어낼 수 없으므로 환각이 원천 차단된다. (b) api·config-catalog는 시그니처·이름·기본값이라는 결정적 골격에 "이게 무슨 역할인가" 설명을 LLM이 채운다(코드 docstring이 있으면 그대로 쓴다). (c) glossary는 LLM이 가장 자유롭다 — 어떤 용어가 중요한지 선정부터 정의 서술까지 gemma가 주도한다. 그래서 정확한 표현은 "LLM 0%"가 아니라 "결정적 사실 위에서 LLM이 서술하는 자유도가 페이지마다 다르다"이다 — 사실에 강하게 묶인 요약 → 골격 + 설명 → 전면 생성으로 이어지는 스펙트럼이다. 어느 경우든 LLM의 출력은 결정적 데이터에 묶여 있어, 위키가 코드와 어긋날지언정 없는 사실을 지어내진 않는다.
이 구조 — 결정적 데이터 레이어 + 그 위의 LLM 서술 레이어 — 가 두 가지 효과를 만든다. 첫째, nightly 재생성이 거의 무료다. nightly(밤마다 자동 실행되는 배치 작업)는 그날 소스가 바뀐 repo만 골라 재생성하는데, 결정적 데이터 레이어(표·그래프·시그니처)는 SQL/AST만 다시 돌리면 끝이라 GPU가 필요 없다. LLM 서술 레이어는 그 데이터에 묶인 짧은 요약·설명이라 토큰이 적고, GPU가 한가할 때 얹는 부가 레이어로 둔다 — GPU가 없으면 결정적 floor만으로도 페이지가 선다. 둘째, 변경된 repo만 건드리므로 부하가 거의 0이다. 바뀌지 않은 repo는 아예 손대지 않으므로 사용자 챗봇 시간을 침범하지 않는다.
3. 코드로 보면 — orchestrator의 단순함
5종 페이지를 한 번에 만드는 orchestrator는
scripts/generate_repo_pages.py다. 각 추출기를 차례로 호출하고 frontmatter를 통일하는 게 전부다.# scripts/generate_repo_pages.py — 5종 페이지의 orchestrator class PageType(enum.Enum): INTERNAL_DEPENDENCIES = "internal-dependencies" API = "api" CONFIG = "config-catalog" GLOSSARY = "glossary" # 각 페이지의 메타 — tier 필드가 어디까지 ground truth인지 시그널 _TIER: dict[PageType, str] = { PageType.INTERNAL_DEPENDENCIES: "derived", # 결정적 그래프 + LLM 요약 PageType.API: "derived", # 결정적 골격 + LLM 설명 PageType.CONFIG: "derived", # 결정적 골격 + LLM 설명 PageType.GLOSSARY: "llm-led", # LLM이 선정·정의 주도 } def _attach_frontmatter(body, repo, page_type, extra=None) -> str: """모든 자동 페이지에 같은 frontmatter 스키마. RAG/viewer가 페이지 출처를 한눈에 식별.""" now = datetime.now(timezone.utc).isoformat(timespec="seconds") lines = [ "---", f"repo: {repo}", f"node_type: {_NODE_TYPE[page_type]}", "source: ast+graph" if page_type != PageType.GLOSSARY else "source: gemma", f"converted_at: {now}", f"generator: {_GENERATOR_NAME[page_type]}", f"tier: {_TIER[page_type]}", "---", ] return "\n".join(lines) + "\n\n" + body def generate_repo(repo: str, out_root: Path, with_llm: bool = False): """한 repo에 5종 페이지를 생성. dependencies는 별도 스크립트.""" repo_dir = PROJECTS / repo # 1. internal-dependencies.md — 결정적 그래프 + (GPU 여유 시) LLM 요약 g = build_graph(repo, db=GRAPH_DB) body = render_internal_deps_page(g, with_llm=with_llm) write(out_root / repo / "internal-dependencies.md", _attach_frontmatter(body, repo, PageType.INTERNAL_DEPENDENCIES)) # 2. api.md — Python ast + .service 라우트 routes = gather_api_surface(repo_dir) body = render_api_page(routes, with_llm=with_llm) write(out_root / repo / "api.md", _attach_frontmatter(body, repo, PageType.API)) # 3. config-catalog.md — 결정적 이름·기본값 + (GPU 여유 시) LLM 설명 cfg = gather_config(repo_dir) body = render_config_catalog_page(cfg, with_llm=with_llm) write(out_root / repo / "config-catalog.md", _attach_frontmatter(body, repo, PageType.CONFIG)) # 4. glossary.md — gemma (옵션) if with_llm: terms = extract_glossary(repo, repo_dir) body = render_glossary_page(terms) write(out_root / repo / "glossary.md", _attach_frontmatter(body, repo, PageType.GLOSSARY))4개 페이지가 각각 100~200줄 추출기로 분리돼 있다. 새 페이지 종류를 추가하면 enum에 항목 + 추출기 함수 + frontmatter 매핑 한 줄씩 추가하면 끝이다. 5종이 6종이 되는 비용이 정말 적다.
4. 한 repo가 채워지는 모습 — Bare 그룹 예시
추상적인 "5종"이 실제로 어떻게 생기는지, dependencies.md 한 장뿐이던 Bare 그룹 repo가 5페이지로 채워지는 모습을 보자. 가령 작은 이미지 유틸 repo(
image-utils)가 다른 두 repo에 쓰이고, 내부에 모듈 셋이 얽혀 있으며, HTTP route 하나와 env var 하나를 노출한다고 하자. 사람이 한 줄도 쓰지 않아도 결정적 추출만으로 아래 골격이 나온다(괄호는 그 줄의 출처).# dependencies.md — graph DB cross-repo edges | 방향 | 상대 repo | 통해서 | | ---- | ---------- | ------ | | out | core-codec | import | | in | report-svc | HTTP | | in | batch-runner | CLI | > 요약: image-utils를 고치면 report-svc·batch-runner가 영향받는다. (LLM 요약) # internal-dependencies.md — graph DB intra-repo (mermaid) # cli.py → core.py, api.py → core.py, core.py → store.py > 요약: core.py가 허브다 — 세 모듈이 모두 여기로 모인다. (LLM 요약) # api.md — Python ast + .service | 종류 | 시그니처 | 설명 | | ----- | ------------ | ---------------------- | | route | POST /resize | 이미지를 리사이즈한다 (docstring) | # config-catalog.md — ast + pyproject + .service | 이름 | 기본값 | 역할 | | ------ | ------ | ------------------- | | MAX_PX | 4096 | 출력 최대 변 길이 (docstring) | # glossary.md — gemma (README·docs 기반) - **resize budget** — 한 요청이 쓸 수 있는 픽셀 상한. MAX_PX로 제어된다.주목할 점은 사람의 손이 0인데도 페이지가 부실하지 않다는 것이다. 의존 표·모듈 그래프·시그니처·설정은 graph DB와 AST에서 결정적으로 떨어지고, "무슨 역할인지" 한 줄은 코드 docstring이 있으면 그대로 옮겨 오고 없을 때만 gemma가 채운다. glossary만 gemma가 본격적으로 서술을 만든다. 그래서 README도 없던 Bare repo가 하룻밤 만에 zero → 5페이지로 올라선다. 이게 "모든 repo에 5종 표준을 둔다"가 가능한 이유다 — 사람의 시간을 한 톨도 요구하지 않는 floor라서다.
5. 운영 결과 — 격차 해소와 RAG 품질 향상
5종 자동 페이지 + 마커 주입을 잠근 직후 22 repo 모두에서 최소 5 페이지가 표면화됐다. Bare 그룹 4개는 zero → 5로 도약하였다. Rich 그룹 5개는 기존 11~24 페이지에 자동 5개가 얹혀 총 16~29 페이지가 되었다. 균일한 floor가 생긴 것이 가장 큰 효과라고 볼 수 있다.
한 가지 부수 효과 — RAG 검색 품질이 즉시 올라갔다. "X 함수 시그니처가 뭐였지?" 같은 질문에 api.md가 정확히 답하고, "Y env var 기본값?"에는 config-catalog.md가 답한다. 이전에는 RAG가 architecture.md 서술을 뒤져서 추측에 가까운 답을 했는데, 이제는 카탈로그에서 결정적 fact를 가져온다. AI 에이전트의 작업 안정성이 한 단계 올라갔다.
idempotent 주입의 가치는 한 사건에서 명확해졌다. 한 번은 잘못된 추출 로직으로 internal-dependencies.md가 깨졌고, 픽스 후 다시 inject_index_links를 돌렸다. 사용자 수기 서술은 한 글자도 안 변했고 마커 블록만 갈아끼워졌다. "같은 스크립트를 재실행해도 안전하다"는 확신이 운영의 자유를 만든다.
마무리 — 단촐을 결정적으로 채우는 법
위키의 단촐은 종종 "잘못된 자동화"로 해결하려다 더 큰 문제를 만든다. LLM을 더 부르거나, 사람이 더 쓰거나, 양쪽 모두 누적 부담을 만든다. 답은 의외로 단순할 수 있다 — 결정적으로 잡히는 정보는 결정적으로 자동화하고, 자동 산출이 사용자 수기 서술을 절대 손대지 않게 분리한다.
이 두 정책의 결합이 5종 자동 페이지 + HTML 마커 주입이다. graph DB·AST가 다섯 페이지의 사실(의존 edge·시그니처·설정 이름)을 결정적으로 깔고, 그 위에서 gemma가 페이지마다 다른 자유도로 요약·설명을 얹는다 — dependencies·internal-dependencies는 그래프에 묶인 요약, api·config는 골격에 설명 한 줄, glossary는 용어 선정까지. LLM 출력이 결정적 사실에 묶여 있어 환각이 차단되는 게 이 구조의 핵심이다. index.md는 사용자 영역으로 두되, 마커 블록 안에 링크만 끼워 넣어 새 페이지의 발견성을 보장한다. "수기 서술의 자율성"과 "자동 카탈로그의 균일성"이 한 페이지 안에 공존한다. 그리고 그 공존이 사람과 LLM의 공동 편집으로까지 번지면 — 그건 다음 글의 몫이다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
같은 사실도 신뢰 레벨이 다르다 — 코드 위키의 Trust Gradient (0) 2026.06.04 불필요한 껍데기 걸러내기 — vulture를 이용한 데드 코드 탐지 및 지식 정제 (0) 2026.06.04 코드의 어두운 구석을 드러내기 — radon을 통한 순환 복잡도 지표 수집 (0) 2026.06.04 LSP를 메모리 안으로 — jedi를 통한 초고속 함수 단위 CALLS 그래프 구축 (0) 2026.06.03 tree-sitter Stage A — 어려웠던 건 빠른 파싱이 아니라 캐시 무효화였다 (0) 2026.06.03 repo마다 신뢰도가 다르다 — 4-tier 분류와 2-layer egress allowlist (0) 2026.06.02 기존 pre-push 훅을 깨뜨리지 않고 끼워 넣기 — chain mode와 fail-soft 정책 (0) 2026.06.02 1인 로컬 환경에도 outbox 패턴 — NetworkX·Qdrant·파일 3-way 정합성 (0) 2026.06.01 위키 하나에 저장소가 셋인 이유 — 그래프 DB·벡터 DB·파일의 분업 (0) 2026.06.01 agent가 wiki로 task를 풀 수 있느냐가 ground truth — with-wiki vs without-wiki로 측정하기 (0) 2026.05.31