애드블럭 종료 후 사이트를 이용해 주세요.

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 멀티프로젝트 Claude Code 설정 운영기 5편 - 30k 깎을 계획으로 시작해 6.3k에서 멈춘 이유
    AI 엔지니어링 2026. 9. 8. 07:47
    728x90
    반응형

    세션을 열고 아무 명령도 내리지 않았는데 컨텍스트에 이미 6,490 토큰이 차 있었다. 글로벌 CLAUDE.md와 프로젝트 CLAUDE.md가 매 세션 통째로 실린 결과다.

    이 시리즈에서 나는 프로젝트 여러 곳의 .claude/를 한 저장소에서 관리하고(1편), 팀 룰과 개인 override를 분리하고(2편), 에이전트에게 Edit은 주되 commit은 막고(3편), 룰 주입을 상시 로드에서 조건부 로드로 바꿨다(4편). 4편 끝에서 "이거 토큰 꽤 먹는데" 하고 넘어간 자리를, 이번엔 자를 대고 전수로 다시 쟀다. 55개 컴포넌트를 분류하니 유지 16 / 압축 23 / 병합 8 / 제거 5 / 재측정 3이 나왔다. 명목 절감 추정치는 약 30.2k 토큰이었다.

    실제로 깎은 건 6.3k다. 80%가 미달이다. 그런데 그 미달이 옳았다는 게 이 글에서 하려는 이야기다. (측정은 2026년 6월, 정리는 7월 기준.)

    매 세션 세금을 내는 층은 하나뿐이다

    하네스에 쌓인 문서를 한 덩어리로 보면 판단이 흐려진다. 로드 시점으로 갈라야 한다.

     

    상시 로드는 세션을 열자마자, 사용자가 뭘 시키든 상관없이 지불한다. 호출 시 로드는 그 커맨드나 스킬을 실제로 부를 때만 들어온다. 한 세션이 전부를 부르는 일은 없다. 조건부 read는 훅이 조건을 판정해 통과할 때만 파일을 읽는다. 4편에서 React 룰을 여기로 옮겼다.

    이 구분을 안 하고 총합만 보면 "14만 토큰짜리 하네스"라는 무서운 숫자가 나오는데 그건 아무도 지불하지 않는 금액이다. 반대로 상시 로드의 6,490은 하루에 세션을 열 번 열면 열 번 다 낸다. 깎을 가치는 층마다 완전히 다르다.

    깎기 전에 자부터 만들었다

    토크나이저를 붙일까 하다가 그만뒀다. 목적이 절댓값이 아니라 before/after 비교여서 chars/4 근사로 통일했다. 오차 ±15%는 문서에 적어두고 시작했다. 대신 매번 같은 방식으로 재도록 스크립트로 고정했다.

    #!/usr/bin/env bash
    # measure-context.sh — AI 하네스가 세션에 싣는 컨텍스트를 레이어별로 집계한다.
    # 출력: 레이어 | 파일 수 | chars | chars/4 추정 tok
    # 주의: chars/4는 근사(±15% 수준)다. 절대값이 아니라 before/after 상대 비교용.
    set -euo pipefail
    
    ROOT="${1:-$HOME/.claude}"
    
    # 레이어 정의: "표시명|find 시작 경로|이름 패턴"
    LAYERS=(
      "상시 로드: 글로벌 CLAUDE.md|$ROOT/CLAUDE.md|"
      "commands|$ROOT/commands|*.md"
      "skills|$ROOT/skills|*.md"
      "agents|$ROOT/agents|*.md"
      "guides|$ROOT/docs/guides|*.md"
    )
    
    total_files=0; total_chars=0
    
    printf '%-28s %6s %10s %10s\n' "레이어" "파일" "chars" "tok(≈)"
    printf '%s\n' "----------------------------------------------------------"
    
    for entry in "${LAYERS[@]}"; do
      IFS='|' read -r label path pattern <<< "$entry"
    
      # 레이어 자체가 없을 수 있다(프로젝트마다 구성이 다르다) → 0으로 기록하고 계속.
      if [[ ! -e "$path" ]]; then
        printf '%-28s %6s %10s %10s\n' "$label" 0 0 0
        continue
      fi
    
      # 심링크로 공유하는 파일은 실체 경로 기준으로 한 번만 센다.
      # -type f는 심링크를 아예 빼버리므로, -L(추적) + readlink 실경로 dedup 조합을 쓴다.
      # 이미지·폰트 같은 바이너리는 컨텍스트에 안 실리니 이름 패턴으로 배제한다.
      if [[ -f "$path" ]]; then
        files=("$path")
      else
        mapfile -t files < <(
          find -L "$path" -type f -name "${pattern:-*}" -print0 2>/dev/null \
            | xargs -0 -r -n1 readlink -f \
            | sort -u
        )
      fi
    
      # 파일이 0개면 wc가 stdin을 붙잡고 멈춘다 → 빈 배열은 건너뛴다.
      if (( ${#files[@]} == 0 )); then
        printf '%-28s %6s %10s %10s\n' "$label" 0 0 0
        continue
      fi
    
      # 한국어 룰 문서가 섞이므로 바이트(-c)가 아니라 문자(-m)로 센다.
      chars=$(wc -m -- "${files[@]}" | tail -n1 | awk '{print $1}')
      printf '%-28s %6d %10d %10d\n' "$label" "${#files[@]}" "$chars" "$((chars / 4))"
    
      total_files=$(( total_files + ${#files[@]} ))
      total_chars=$(( total_chars + chars ))
    done
    
    printf '%s\n' "----------------------------------------------------------"
    printf '%-28s %6d %10d %10d\n' "합계" "$total_files" "$total_chars" "$((total_chars / 4))"

    dedup은 레이어 안에서만 걸린다. 같은 파일을 commands/skills/에 각각 심링크로 걸어뒀다면 두 번 잡힌다. 그건 오히려 잘된 일이었다. 그 중복이 이번에 병합한 8건의 절반이었다.

    기준선을 잡을 때 미커밋 변경 8개를 먼저 정리하고 깨끗한 HEAD에서 다시 쟀다. 워킹 트리가 지저분한 상태의 기준선은 나중에 어떤 결론도 지탱하지 못한다.

    스테이지 하나가 곧 되돌림 단위 하나다

    문서를 지우고 합치는 작업은 코드보다 회귀를 늦게 들킨다. 테스트가 빨갛게 되는 게 아니라, 2주 뒤 에이전트가 이상하게 굴 때에야 "아 그때 그 문단"이 떠오른다. 그래서 압축 자체보다 되돌림 경로를 먼저 설계했다.

     

    스테이지 하나를 릴리스 하나에 정확히 대응시켰다. 회귀가 보이면 그 릴리스만 되돌리면 된다. 상시 로드 파일 편집은 릴리스 두 번 안에 묶었다. 그 파일이 바뀌면 프롬프트 캐시가 통째로 깨지기 때문이다. 잘게 쪼개는 게 항상 옳은 건 아니다.

    릴리스 직전에는 링크 무결성 검사를 붙였다. 슬림화의 실체는 "본문을 지우고 포인터 한 줄로 대체"인데 그 한 줄이 죽으면 계약이 통째로 증발한다.

    #!/usr/bin/env bash
    # check-links.sh — 릴리스 직전, 하네스 문서의 내부 마크다운 링크가 살아있는지 본다.
    set -euo pipefail
    ROOT="${1:-$HOME/.claude}"
    broken=0
    
    while IFS= read -r -d '' doc; do
      dir=$(dirname "$doc")
      while IFS= read -r target; do
        target="${target%%#*}"               # 앵커 제거: guide.md#section → guide.md
        [[ -z "$target" ]] && continue       # 같은 문서 내 앵커만 있는 링크는 대상 아님
        [[ -e "$dir/$target" ]] && continue  # 상대 경로는 그 문서 위치 기준으로 푼다
        echo "BROKEN  ${doc#"$ROOT"/} -> $target"
        broken=$((broken + 1))
      done < <(
        grep -oE '\]\([^)]+\)' "$doc" \
          | sed -E 's/^\]\(//; s/\)$//' \
          | grep -vE '^(https?://|mailto:|#)' || true
      )
    done < <(find -L "$ROOT" -type f -name '*.md' -print0)
    
    (( broken > 0 )) && { echo "링크 무결성 실패: ${broken}건 — 릴리스 중단." >&2; exit 1; }
    echo "링크 무결성 OK"

    압축했다가 그대로 되돌린 파일이 하나 있다

    처음엔 "글자 수 많은 순서대로 깎는다"로 접근했다. 가장 큰 덩어리는 게이트 판정을 담당하는 평가자 에이전트의 정의 파일이었다. 산문이 장황해 보여서 598자를 덜어냈다.

    그리고 골든 셋을 돌렸다. 점수 모드는 회귀 0으로 그대로였는데 의미 리뷰 모드에서 회귀가 1건 났다. 해당 모드의 프롬프트는 바이트 단위로 동일했으니 비결정성일 가능성이 높았다. 그래도 되돌렸다. 사전에 "이 압축은 골든 셋 유지가 조건이고 미달이면 revert"라고 못 박아뒀기 때문이다.

    되돌림은 실패가 아니다. 실패는 조건을 못 박지 않고 깎아서 회귀가 났는지조차 모른 채 넘어가는 쪽이다. 이후 규칙을 하나 더 세웠다. 편향을 붙잡아 두는 구조물, 그러니까 평가자의 역할·제약·편향 완화 계약, 점수표의 닫힌 집합, AI 저작 표기 금지 단락은 아예 압축 대상에서 뺐다(diff 0). 장황해 보이는 문장이 실은 모델을 특정 행동에 묶어두는 밧줄인 경우가 있다.

    명목 30.2k와 실현 6.3k 사이

    최종 집계는 590,688자 → 565,372자, 추정 −6,329 토큰이었다. 명목의 5분의 1이다.

    차이가 난 자리는 대부분 게이트 계약 파일이었다. 감사 단계에서는 "이 문단 절반은 줄일 수 있다"로 분류됐지만 실제로 손을 대보면 한 문장이 권한 경계 하나를 지탱하고 있었다. 그래서 보수적으로 덜 깎았다. 명목 추정치는 의미를 안 건드린다는 제약을 넣기 전의 숫자였던 셈이다.

    대신 가장 값어치 있는 항목은 전량 실현됐다. 상시 로드가 6,490 → 3,998 토큰이 됐다. 이 절감은 세션을 열 때마다 반복해서 돌아온다. 나머지 4k는 어차피 그 커맨드를 부른 세션에서만 아꼈을 돈이다. 총합 −6.3k라는 숫자보다 "세션당 −2.5k"가 이 작업의 실제 성적표다.

    char로 잡히지 않은 이득도 있었다. 같은 사실이 세 문서에 조금씩 다르게 적혀 있던 drift 3건이 없어졌다. --agent 플래그의 의미가 한쪽에선 "전체 에이전트 활성화", 다른 쪽에선 "단계별 최소 활성화"로 적혀 있었는데 이건 토큰이 아니라 정확도 문제였다.

    재측정 스테이지에서 보조 모델의 적대적 비판 오버레이도 같이 쟀다. 골든 셋 fixture에 일부러 약한 보고("문제없음")를 물려주고 실제 결함을 복원하는지 봤더니 10런에서 findings 31건 중 27건 채택, 약 87%가 나왔다. 다만 이 수치는 상한 값이다. 약한 stub을 상대로 한 것이라 제대로 된 메인 모델 보고가 놓친 것만 잡아내는 진짜 한계 수율은 아니다. 유지 판정을 내리기엔 충분했고 그 이상으로 해석하진 않았다.

    지운 걸 되살리는 규칙까지 같이 적었다

    제거는 되돌림 계획이 있을 때만 안전하다. 그래서 earn-back 룰 한 줄을 프로젝트 CLAUDE.md 편집 규칙에 넣었다. 제거한 컴포넌트가 막고 있던 실패 모드가 두 번 재발하면 그 컴포넌트만 복원하고 복원 사이에 검증 런을 둔다. 한 번은 우연일 수 있다. 두 번이면 그 문단이 실제로 일하고 있었다는 증거다.

    같은 형태의 룰을 모델에도 걸었다. 감사할 때 쓴 최상위 티어 모델을 사용량 한도 때문에 도중에 못 쓰게 됐다. 한 티어 아래로 내려와 기준점을 다시 잡아야 했다. 여기서도 "한도가 풀리면 원래 티어로 복귀하되, 복귀 직후 같은 골든 셋을 한 번 돌린다"를 같이 적어뒀다. 측정 없는 하향도 측정 없는 상향도 하지 않는다.

    마무리

    컨텍스트 예산은 총합이 아니라 로드 시점으로 갈라야 한다. 매 세션 반복해서 지불하는 층에서만 절감이 복리로 돌아온다. 깎기 전에 되돌리는 경로부터 만든다. 회귀 조건을 미리 못 박았다면 되돌림은 정상 경로다. 명목 절감의 80%를 포기하는 게 옳을 때도 있다. 남은 글자가 계약을 지탱하고 있을 때가 그렇다.

    지금 터미널에서 wc -m ~/.claude/CLAUDE.md를 쳐보라. 그 숫자를 4로 나눈 값이, 당신이 오늘 연 모든 세션에 곱하기로 붙은 금액이다.

    관련 글

    반응형

    댓글

Designed by Tistory.