ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 외부 API 100%, 내부는 trend — 양적 검증을 두 층으로 가르는 이유
    IT 2026. 5. 30. 23:00
    외부 API 100%, 내부는 trend — 양적 검증을 두 층으로 가르는 이유

    위키를 한 달 운영해 보면 한 가지 이상한 양상이 보인다. 모든 페이지가 syntax 깨끗하고, 모든 anchor가 valid한 함수를 가리키며, 그런데 한 repo에 대한 질문에 RAG가 답하지 못한다. 들어가서 보면 그 repo의 함수 200개 중 위키에 등장한 게 5개뿐이다. 위키는 valid하지만 그 repo에 대해 사실상 아무 말도 하지 않는다.

    이 상황은 "적힌 줄"을 검증하는 다른 검증들로는 절대 잡히지 않는다. 그것들은 적힌 syntax가 valid한지, 적힌 좌표가 실재하는지를 본다. 한 번도 적히지 않은 함수에 대해서는 할 말이 없다.

    의 차원 — 코드에 정의된 노드 수 대비 위키에 언급된 비율을 보는 coverage 검증이 그 자리에 있다. 이 글은 그 검증의 두 가지 결정을 본다. 첫째, 모든 함수를 다 노출시키는 게 목표가 아니다 — 빈 자리에는 두 종류가 있고, 각각에 다른 정책을 건다. 둘째, 측정에서 단순 substring grep은 거짓 매칭을 낳는다 — word boundary가 필요하다.

    1. 다른 검증이 보지 않는 것 — 빈 자리

    적힌 것의 valid성과 적히지 않은 영역의 부재는 서로 다른 종류의 실패다. "잘못 적힌 줄"을 잡는 검증과 "적히지 않은 함수"를 잡는 검증은 본질적으로 다르다. 후자가 잡히지 않으면, 한 repo가 사실상 위키 밖에 있어도 모두 통과한다.

    RAG 입장에서 보면 차이가 극명하다. 적힌 함수 5개에 대한 질문에는 정확히 답한다. 적히지 않은 195개에 대한 질문에는 "관련 정보 없음" 또는 더 나쁘게는 "5개 중 가장 가까운 것"으로 hallucinate한다. 사용자는 후자를 의심 없이 따라간다 — 다른 검증들이 깨끗한 시스템에서 RAG가 무관한 답을 하리라고 기대하지 않으니까. "잘 검증된 위키"라는 신뢰가 오히려 빈 자리의 거짓을 가린다.

    2. 빈 자리에는 두 종류가 있다

    여기서 자연스러운 첫 반응이 한 번 꺾인다. "그럼 코드에 정의된 모든 함수를 위키에 노출시키자"가 떠오르지만, 이게 목표가 아니다. 빈 자리는 한 종류가 아니다.

    • 외부 인터페이스의 빈 자리: 다른 모듈이 호출하는 export 함수가 위키에 빠지면, 그건 위키가 사실상 거짓을 가르치는 상태다. 다른 팀이 위키만 보고 "이 인터페이스는 없다"고 잘못 결론낸다. 거짓의 비용이 크다.
    • 내부 구현의 빈 자리: 한 모듈 안에서만 쓰이는 helper가 위키에 빠진 건 안타깝지만 거짓은 아니다. 그 모듈을 호출하는 외부 사용자에게는 보일 일이 없는 정보이고, 모듈 내부 개발자는 코드를 직접 본다.

    두 빈 자리에 같은 정책을 걸면 한쪽이 무너진다. 둘 다 strict fail로 잡으면 새 internal helper 추가가 매번 nightly를 깨고 — 정상적인 코드 변경 흐름이 알림 폭주로 이어진다. 둘 다 trend로 보면 외부 API가 위키에서 빠진 거짓 상태를 잡지 못한다. 동일 metric을 두 정책으로 나누는 것이 이 검증의 본질적 결정이다.

    diagram

    위 다이어그램은 동일한 코드 base에서 빈 자리가 어떻게 두 흐름으로 갈라지는지를 보인다. 왼쪽(붉은 계열)이 strict — 외부 인터페이스가 위키에 빠지면 즉시 fail로 차단한다. 오른쪽(황 계열)이 trend — 내부 helpers는 일시 lag를 허용하고 누적 기울기만 본다. 색이 다른 이유는 두 빈 자리의 비용 구조가 다르기 때문이다. 거짓을 가르치는 일은 막아야 하고, 일시 lag는 받아들여야 한다.

    3. 외부 API — 100% 강제, fail로 처리

    API.md(또는 언어에 맞는 export 목록 — Python의 __all__, C/C++의 public header, Rust의 pub 항목)을 source of truth로 둔다. 거기에 적힌 모든 항목은 위키에 정의가 있어야 한다. 빠지면 fail이다.

    def check_api_coverage(api_md: Path, wiki_root: Path) -> list[str]:
        """API.md에 적힌 export 항목이 모두 위키에 정의되어 있는지 검사."""
        # API.md를 파싱해 export 인터페이스 이름 목록을 뽑는다
        # (마크다운의 ## 헤더 또는 ` `로 묶인 식별자가 관행)
        api_names = parse_exports(api_md)
    
        # 위키 본문에서 정의된(= 시그니처 + 설명) 항목만 모은다
        # 단순 mention이 아니라 "anchor + 설명" 양쪽이 있어야 정의로 인정
        wiki_defined = collect_wiki_defs(wiki_root)
    
        missing = [n for n in api_names if n not in wiki_defined]
        return missing  # 비어 있으면 통과, 한 개라도 있으면 nightly fail
    

    이 검증을 strict fail로 잡을 수 있는 이유는 분모가 통제되기 때문이다. 새 외부 API 추가는 의도적 결정이고, 자주 일어나지 않는다. 새 export를 PR로 들어올때 위키 정의도 생성되어야 한다. nightly가 갑자기 새 함수 50개를 받으면 해당 함수들의 정의를 즉시 위키에 생성한다.

    여기서 mention(언급)과 정의(definition)를 구분한다. 외부 API 검증은 "이름이 본문 어딘가에 나오는가"로 만족하지 않는다. 시그니처와 설명이 명확히 있어야 정의로 친다 — 거짓을 막는 것이 목적이라면, "이름만 한 번 등장하는 빈 멘션"은 거짓을 덮어주는 역할만 한다. anchor 형식(file:LINE)과 설명 문단이 함께 있어야 한다.

    4. 내부 함수 — trend만 기록, fail 안 시키는 이유

    내부 helpers는 분모가 통제되지 않는다. 한 PR이 함수 50개를 한 번에 추가하는 일이 정상이다. 위키는 nightly에 들어가서야 갱신된다. 그 24시간 동안 분모가 50 늘어난 채 분자는 그대로다. 이걸 fail로 잡으면 정상적인 코드 변경 흐름이 매번 nightly를 깬다. 알림이 폭주하고 운영자가 알림을 꺼버린다.

    그래서 fail이 아니라 metric으로 기록한다. 매일 비율을 시계열에 적고, 4주 누적 기울기가 음의 방향이면 분기 보고에 띄운다. 알림 임계치는 즉시적인 절댓값이 아니라 "한 달 동안 -10%p 이상 떨어졌나"로 잡는다. 일시 lag는 통과시키고, 누적 drift는 잡는다.

    diagram

    위 차트는 내부 함수 coverage가 매일 메트릭으로 기록되어 4주에 걸쳐 어떤 신호로 자라는지를 보인다. repo A는 안정적으로 높음 — 위키가 잘 따라간다. repo B는 중간에서 흔들리지 않음 — 절댓값은 낮아도 안정. repo C는 매주 떨어진다 — 코드는 늘었는데 위키는 따라가지 못함. 한 시점의 절댓값보다 시간 trend의 기울기가 본질적 신호다. 절댓값이 낮아도 안정적이면 그 repo의 사정에 맞춘 정상 상태일 수 있다.

    운영자 입장에서 진짜 효과는 "위키 작성 우선순위를 데이터로 결정할 수 있게 된다"는 것이다. 다음 분기에 어느 repo의 위키를 집중해서 늘릴지 — 감으로 결정하는 게 아니라 trend가 떨어지는 repo부터 다룰 수 있다. 이 검증은 정책 수립의 input이 되는 자리다.

    5. grep substring match의 함정 — word boundary가 필요한 이유

    지금까지 "위키에 노드 이름이 등장한 수"를 어떻게 셀지를 말하지 않았다. 가장 단순한 답은 name in wiki_text — Python의 substring 매칭. 그런데 이게 거짓 매칭을 낳는다.

    parse를 찾는다고 하자. 위키 본문에 parser가 있다. parse_args가 있다. preparse가 있다. 셋 다 substring으로는 parse를 포함한다. 카운트가 +3 — 실은 정의된 parse 함수 하나는 위키에 한 줄도 적혀 있지 않은데도. 수치가 위로 부풀려진다. coverage가 60%라고 하지만 실은 30%일 수 있다. trend도 거짓이 된다 — 신규 함수 parser_v2가 추가되면 분모는 1 늘고 분자도 1 늘어서 비율 변화가 없는 것처럼 보인다.

    수정은 word boundary. 정규식 \b으로 양쪽이 단어 문자([A-Za-z0-9_])가 아닌 경계에서만 매칭한다.

    import re
    import ast
    from pathlib import Path
    
    def count_defs(repo_root: Path) -> set[str]:
        """repo의 모든 .py 파일에서 의미 있는 노드 이름 — 분모 모집단."""
        names = set()
        for py in repo_root.rglob("*.py"):
            try:
                tree = ast.parse(py.read_text())
            except SyntaxError:
                continue
            for n in ast.walk(tree):
                if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
                    names.add(n.name)
        return names
    
    def count_wiki_mentions(wiki_root: Path, defined_names: set[str]) -> int:
        """위키에 등장한 노드 이름 수 — 분자.
    
        word boundary(\b)로 substring 거짓 매칭을 막는다:
          - parse는 매칭됨
          - parser, parse_args, preparse는 매칭 안 됨
        re.escape — 함수 이름에 . 같은 정규식 메타 문자가 섞여도 안전.
        """
        wiki_text = "\n".join(p.read_text() for p in wiki_root.rglob("*.md"))
        count = 0
        for name in defined_names:
            if re.search(r"\b" + re.escape(name) + r"\b", wiki_text):
                count += 1
        return count
    
    def coverage(repo_root: Path, wiki_root: Path) -> float:
        defined = count_defs(repo_root)
        return count_wiki_mentions(wiki_root, defined) / max(len(defined), 1)
    

    두 변경의 핵심은 re.search(r"\b" + re.escape(name) + r"\b", wiki_text) 한 줄. \b이 양쪽 단어 경계를 강제하고, re.escape이 함수 이름에 정규식 메타 문자(., +, ? 등)가 섞여도 그 자체로 매칭하게 만든다. 클래스 메서드 cls.method 같은 점 포함 이름이 있다면 후자가 특히 중요하다.

    parseparse_args가 둘 다 정의된 repo에서, 위키에 parse_args만 적혀 있다고 하자. \b 매칭은 parse_args mention을 정확히 잡지만, parse는 매칭에서 빠진다 — 정상이다. 반대로 위키에 단독 parse가 있고 정의에는 parse_args만 있다면 parse는 매칭에서 빠진다 — 이것도 정상이다. substring 매칭의 거친 거짓은 사라지고, 남는 모호함은 anchor 형식을 강제할 때 해결된다(file:LINE 좌표는 함수 단위로 유일).

    6. 정리

    코드 노드 수 대비 위키 언급 비율을 매일 측정하되, 빈 자리에 두 종류가 있다는 점을 정책으로 반영한다. 외부 인터페이스는 100% 강제로 fail — 빠지면 거짓을 가르치는 위키가 되니까. 내부 helpers는 trend로만 기록 — 정상적인 코드 변경에 따른 일시 lag를 fail로 만들면 알림이 무의미해지니까. 측정에서는 substring grep의 부풀려진 카운트를 막기 위해 word boundary 정규식을 쓴다. 같은 metric을 두 정책으로 다르게 처리하는 것이 핵심이고, 양적 양심까지가 이 검증의 본분이다 — 의미적 깊이는 별도의 의미 검증이 답할 영역이다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.