Claude Code 비용 문제, 제3막 — 비용 절감을 위한 다양한 옵션 생태계
요약
Claude Code 사용 시 발생하는 LLM 비용을 절감하기 위한 오픈 소스 생태계와 기술적 전략을 분석합니다. 특히 프롬프트 캐시 효율을 유지하면서 입력/출력 토큰을 압축하는 Headroom과 같은 도구의 작동 원리를 다룹니다.
핵심 포인트
- 비용 절감 시 모델 범위의 프롬프트 캐시 유지가 핵심임
- 요청 접두사를 매번 재작성하면 캐시 효율이 급격히 저하됨
- Headroom은 콘텐츠 유형 인식 라우팅과 가역적 압축을 제공함
- CacheAligner를 통해 휘발성 콘텐츠를 제거하여 캐시 적중률을 높임
LLM 비용을 절감하는 것을 목표로 하는 거대한 오픈 소스 (open-source) 생태계가 존재합니다. 이를 평가하는 요령은 세 가지 비용 항목 중 어느 것을 공략하는지 — 캐시된 입력 (0.1×), 캐시되지 않은/쓰기 입력 (1×/2×), 또는 출력 (가장 비싸며 캐시되지 않음) — 를 묻고, 그 과정에서 모델 범위의 프롬프트 캐시 (prompt cache)를 포기하지 않는지를 확인하는 것입니다.
마지막 조항이 반복되는 함정이며, 이 가이드 전체를 관통하는 핵심 내용입니다:
매 턴마다 요청 접두사 (request prefix)를 다시 작성하는 모든 것은 모델 범위의 프롬프트 캐시를 포기하게 됩니다. 도구가 실질적인 절감 효과를 얻으려면 (a) 캐시된 접두사를 바이트 단위로 동일하게 유지하거나, (b) 캐시가 커버하지 않는 비용 항목, 즉 **출력 (output)**을 공략해야 합니다.
우리는 비용 항목 순서에 따라 생태계를 살펴볼 것입니다: 입력/캐시 압축기, 출력 압축기, 모델 라우터 (model routers), 그리고 Claude Code 내부에서 라우팅이 실제로 비용을 발생시키는 방식 순입니다.
입력 / 캐시 라인 압축기
Headroom — 입력 / 캐시 쓰기 라인 압축
Headroom (headroomlabs-ai/headroom, 이전 chopratejas/headroom; Apache-2.0)은 코딩 에이전트를 위한 로컬 우선 (local-first) 압축 라이브러리 + 프록시 (proxy) + MCP이며, 이 카테고리에서 가장 정교하게 설계된 도구입니다. (또한 매우 최신이며 빠르게 변화하고 있습니다 — 2026-01에 생성되었으며 하루에도 수십 개의 커밋이 이루어지고 있으므로, 세부 사항은 유동적이라고 간주하십시오.) 다음 세 가지가 이를 주목할 만하게 만듭니다:
- 콘텐츠 유형 인식 라우팅 (Content-type-aware routing) — 맹목적으로 압축하는 대신 콘텐츠 유형별로 압축기를 선택합니다 (아래 표 참조).
- 가역성 (Reversibility) — 원본을 저장하며, 모델이 필요할 때 전체 텍스트를 검색하므로 압축으로 인해 정보가 손실되지 않습니다.
CacheAligner— 휘발성 콘텐츠(날짜, ID 등)를 접두사(prefix)에서 제거하여 호출 간에 접두사가 안정적으로 유지되도록 합니다 (캐시와 함께 압축을 구성하는 방식의 절반입니다; 아래 참조).
자체 보고 결과: 정확도를 유지하면서 47–92%의 토큰 절감 효과를 보였습니다 (GSM8K ±0, SQuAD는 19% 압축 시 97%, BFCL 도구는 32% 압축 시 97%) [벤더 자체 보고]. 독립적인 측정 결과는 훨씬 낮습니다. 유일하게 엄격한 제3자 벤치마크에서는 ~10%의 전송(on-wire) 절감을 발견했는데, 이는 Headroom이 자체 발표한 플릿 중앙값인 **4.8%**와 일치하며, 헤드라인 수치보다 한 자릿수 낮은 수준입니다. [독립 벤치마크, 2026-06]
Headroom의 콘텐츠 유형 라우팅 [med]:
| Content | Compressor | Savings | Preserves |
|---|---|---|---|
| JSON 배열 | SmartCrusher | 70–90% | 키(keys), UUID, 불리언(booleans) |
| ... | |||
| Headroom이 실제로 토큰을 절감하는 방법 — 두 가지 표면. 이 표를 볼 때 한 가지 구별점을 인지하지 못하면 오해할 수 있습니다. 70–95%의 비율은 **도구 출력(tool output)**에서 달성되는 것이지, 프록시가 전달하는 대화 내용 전체에서 달성되는 것이 아닙니다. Headroom은 상반된 캐시 규칙을 가진 두 가지 표면에서 작동합니다: |
- 압축 표면 (The compression surface) — MCP 서버 / SDK / 훅(hooks)을 통한 도구 결과. 도구가 큰 데이터 덩어리(60KB 검색 결과, 노이즈가 많은 빌드 로그, 거대한 JSON 배열 등)를 반환할 때, Headroom은 그 데이터 덩어리가 트랜스크립트에 추가되기 전에 압축합니다. 에이전트는 이미 축소된 형태를 저장하고 다시 전송하므로, 모델은 항상 작은 버전만 보고 주변에 프롬프트 캐시가 형성됩니다. 이는 바이트가 어떤 캐시가 존재하기 전에 축소되기 때문에 안전합니다. 즉, 무효화(bust)할 캐시 접두사가 없는 것입니다. 본질적으로 Headroom의 모든 실제 절감 효과는 여기서 발생하며, 자체 문서에서도 그렇게 명시하고 있습니다: _
따라서 사고 모델(mental model)은 다음과 같습니다: Headroom은 **캐시 상류(upstream of the cache)**에서 도구의 출력을 축소하며(무료이며 안전함), **캐시 지점(at the cache)**에서의 대화 재작성은 거부합니다(이는 캐시 버스터(cache-buster)가 될 것이기 때문입니다). 아래의 캐시 킬러(cache-killer) 체크리스트는 왜 두 번째 영역이 반드시 패스스루(passthrough)여야만 하는지에 대한 이유를 설명합니다.
여기서 "결합(compose)"의 의미. 압축(Compression)과 캐싱(Caching)은 토큰 비용에 대한 두 가지 별개의 할인 방식이며, 문제는 이들이 서로 *중첩(stack)*되는지 아니면 *상쇄(cancel)*되는지의 여부입니다. 캐싱은 서버가 이미 확인한 바이트에 대해 90% 할인 쿠폰을 적용하는 것과 같습니다. 캐시된 접두사(prefix)는 전체 가격 대신 0.1× 가격(즉, "캐시 읽기(cache read)")으로 청구됩니다. 압축은 단지 더 적은 바이트를 전송함을 의미합니다. 이들이 **결합(compose)**되는지(둘 다 적용됨), 아니면 **충돌(fight)**하는지(하나가 다른 하나를 무효화함)는 전적으로 어떤 바이트를 압축하느냐에 달려 있습니다:
| 수행하는 작업 | 100K 토큰 캐시된 접두사의 비용 | |
|---|---|---|
| 충돌 (Fight) | 전체 접두사를 100K → 20K로 압축하여 캐시된 바이트를 재작성함 | 이 20K는 이제 새로운 바이트가 됩니다 → 캐시 미스(miss) 발생 → 2× 쓰기(write)에 해당하는 약 40K 상당으로 청구됨. 텍스트는 줄였지만 쿠폰을 무효화했으며, 청구 금액은 오히려 상승했습니다 (읽기 기준으로는 10K 상당이었음). |
| 결합 (Compose) | 캐시된 구간을 바이트 단위로 동일하게 유지하고, *새로운 뒷부분(fresh tail)*만 압축함 | 100K는 0.1× 읽기인 약 10K 상당으로 유지되며, 새로운 뒷부분도 더 작아집니다 — 두 가지 할인 모두 적용됩니다. |
따라서 "압축을 캐시와 결합(composing compression with the cache)한다"는 것은 압축이 토큰을 줄이면서도 캐시 할인이 유지되도록 구조를 조정하는 것을 의미합니다. 이는 두 가지 별개의 요소가 필요하며, 그중 하나만이 CacheAligner입니다:
-
안정적인 접두사 (A stable prefix) — 캐시된 바이트를 호출마다 변동시키는 휘발성 콘텐츠(날짜, ID 등)가 없어야 합니다. 이것이
CacheAligner의 핵심 역할입니다. 즉, 휘발성 콘텐츠를 메시지 끝부분(tail)으로 이동시키는 것입니다. Claude Code는 이미 정확히 이 작업을 수행하고 있습니다. 날짜는 시스템 브레이크포인트(system breakpoints) 뒤에 위치하며, 과금 헤더 토큰(billing-header token)은 캐시 키에서 제외됩니다 (제1막). 따라서Cache${CacheAligner}가 Claude Code 뒤에서 추가로 제공하는 이점은 적습니다. 이 기능은 시스템 프롬프트 상단에 타임스탬프나 세션 ID를 남겨두는 직접 구현한 (hand-rolled) API 클라이언트들에게 제 역할을 다합니다. (현재 Headroom에서CacheAligner는 실제로 비활성화된 상태로 배포되었으며 삭제될 예정입니다. 재작성된 코드가 캐시 핫 존(cache hot zone)을 *변형(mutate)*시킨다는 사실이 발견되었기 때문이며, 이는 해당 기능이 보호하고자 했던 바로 그 대상입니다.) -
캐시 안전 압축 (Cache-safe compression) — 캐시된 구간(span)은 바이트 단위로 동일하게 유지하고, 새로 추가된 끝부분(tail)만 축소해야 합니다. 캐시된 바이트를 다시 쓰게 되면, 0.1배의 읽기 비용을 아끼려다 2배의 쓰기 비용을 지불하게 됩니다 (제2막의 반전). 이는
CacheAligner와는 다른 메커니즘이며, Claude Code의 이면에서 실제로 중요한 부분입니다. 만약 이를 잘못 구현하면 압축기가 절약하는 비용보다 더 많은 비용을 발생시킵니다. 보통 압축기를 아예 사용하지 않는 것보다 비용이 더 많이 드는데, 이는 멀티 턴(multi-turn) Claude Code 세션에서 캐시된 접두사가 비용의 대부분을 차지하기 때문입니다. 이것이 바로 아래의 버그 목록과 직결되는 문제입니다.
프록시 뒤에서 Claude Code 실행하기 — 캐시 파괴 체크리스트
캐시는 바이트 단위로 정확히 일치하는 최장 접두사 일치 (byte-exact longest-prefix match) 방식입니다 (제1막). API는 새로운 요청의 앞부분 바이트가 동일한 동안에만 캐시된 항목을 확장하며, 처음으로 달라지는 바이트부터 그 이후의 모든 내용을 다시 처리합니다. Headroom은 Claude Code와 API 사이의 해당 경로에서 로컬 프록시로 동작합니다. 여기서 발생하는 위험은, Headroom이 의미를 바꾸지 않으면서도 해당 바이트들을 변경할 수 있다는 점이며, 이는 캐시 일치를 놓치기에 충분한 원인이 됩니다.
어떻게 작동하는지 확인하기 위해 하나의 요청을 따라가 보겠습니다. 세 개의 노드가 있습니다:
[A] Claude Code ──원래 바이트 (raw bytes)──▶ [B] Headroom (프록시) ──재방출된 바이트 (re-emitted bytes)──▶ [C] Anthropic API
(클라이언트) (로그 / 압축) (서버 + 캐시)
A — Claude Code가 요청을 생성합니다. Claude Code는 전체 요청을 JSON 바이트(model, tools, system, messages)로 직렬화(serialization)하며, : 또는 , 뒤에 공백을 두지 않는 압축된 (compact) JSON을 작성하고, 비-ASCII 문자의 경우 원시(raw) UTF-8을 사용합니다. content 필드는 다음과 같은 리터럴 바이트 형태로 Headroom에 전달됩니다:
{"role":"user","content":"café"}
B — Headroom이 요청을 다시 방출합니다. 도구 출력(tool output)을 압축하거나 단순히 본문을 로그로 남기기 위해, 네이티브 코드는 들어오는 바이트를 객체로 파싱하고, 하나의 필드를 수정한 뒤, 이를 다시 직렬화합니다. 이때 Headroom의 인코더는 Claude Code의 인코더와 다릅니다. Python의 기본 json.dumps()는 동일한 객체를 다음과 같이 다시 방출합니다:
{"role": "user", "content": "caf\u00e9"}
요청하지 않은 두 가지 변경 사항이 발생했습니다: 모든 :와 , 뒤에 공백이 추가되었고, é가 6글자의 이스케이프(escape) 문자인 \u00e9로 변환되었습니다. 동일한 객체이지만 두 가지 유효한 바이트 인코딩이 존재하며, Headroom은 단지 Claude Code와 다른 인코딩을 선택했을 뿐입니다. Headroom은 이를 **직렬화 드리프트 (serialization drift)**라고 부르며, REALIGNMENT/01-bug-list.md에는 이것이 발생하는 모든 방식에 대한 72개의 P0–P6 감사 항목이 나열되어 있습니다. 각 항목은 실제 Anthropic의 접두사 캐시(prefix-cache) 규칙과 일치합니다.
C — Anthropic이 캐시를 조회합니다. 이것이 프록시(proxy) 개발자가 실수하는 단계입니다. 캐시는 서버가 수신하는 바이트, 즉 Headroom의 출력물에 대해 키(key)를 생성하며, Claude Code의 원본은 전혀 보지 못합니다. 따라서 매 턴마다 서버는 이 요청의 접두사(prefix)를 Claude Code가 생성한 것이 아니라, Headroom 자체가 이전 턴에 보냈고 서버가 캐싱했던 바이트와 대조합니다. 만약 Headroom가 매 턴 바이트 단위로 완전히 동일하게(byte-identically) 재직렬화(re-serialization)한다면, 매칭이 이루어져 캐시 히트(hit)가 발생합니다. 다만 캐시가 Claude Code 대신 Headroom의 인코딩을 기준으로 키를 생성할 뿐입니다. 비용이 폭증하는 경우는 오직 Headroom의 출력이 턴과 턴 사이에 바이트 단위로 안정적이지 않을(not byte-stable) 때뿐입니다. 그리고 단순한 재직렬화 방식은 이를 기본값으로 만듭니다: sort_keys가 없는 json.dumps(), set/dict의 반복(iteration) 순서, 변동하는 부동 소수점(float) 포맷팅, 또는 일부 메시지는 재직렬화하면서 다른 메시지는 바이트를 그대로 복사하는 방식 등이 이에 해당합니다. 이러한 변화 중 어느 하나라도 요청의 상단(top) 근처 바이트를 변경하면, 그 이후의 전체 접두사 키가 재설정(re-key)됩니다. 결과적으로 0.1× 비용의 cache_read 대신 2× 비용의 콜드 라이트(cold write)가 발생하게 됩니다. (또한 Claude Code가 이미 압축된 바이트로 워밍업(warmed)해둔 캐시 앞에 Headroom를 배치하는 순간, Headroom의 공백이 포함된 출력이 기존 항목들과 일치하지 않으므로 일회성 콜드 재구축(cold rebuild) 비용이 발생합니다.)
이로 인해 발생하는 비용:
- 프록시 트래픽이 100% 콜드 라이트(0% 히트)로 수렴합니다 — 단순히 Headroom가 건드린 메시지만 그런 것이 아닙니다. 변동된 바이트가 상단 근처에 위치하면 그 이후의 모든 내용에 대해 키가 재설정됩니다.
- 서버가 이미 캐싱하고 있는 콘텐츠임에도 불구하고, 매 턴 접두사 비용이 10~20배 증가합니다 (0.1× 읽기 → 1~2× 쓰기). 비용을 절감하려 만든 프록시가 절감액보다 더 많은 비용을 발생시킬 수 있습니다.
- 이는 조용히 발생합니다 — 요청 자체는 유효하므로 HTTP 200 응답과 올바른 결과가 반환되며 에러는 발생하지 않습니다. 유일한 증거는 사용량입니다:
cache_read는 0 근처에 머물고, 매 턴cache_creation은 높게 나타납니다. 이는 스택 트레이스(stack trace)가 아니라 청구서에서 발견하게 됩니다.
아래의 각 행은 Headroom의 출력이 턴(turn)을 거치며 왜 바이트 안정성(byte-stability)을 잃게 되는지를 보여주는 또 다른 방식이며, 각각은 버그 목록에 실제로 등록된 항목이므로 직접 찾아볼 수 있습니다. Headroom 버그 (Headroom bug) 열은 해당 ID를 제공합니다 (P0 = "cache-killer smoking guns — 모든 고객이 영향을 받는 결정적 증거"):
| 드리프트 원인 (Drift cause) | 프록시의 동작 (What the proxy did) | 바이트의 변화 (What changed in the bytes) | Headroom 버그 (Headroom bug) |
|---|---|---|---|
| 구분자 / 이스케이프 드리프트 (Separator / escaping drift) | 기본 json.dumps()로 재인코딩 | ,→,, :→:, raw UTF-8 → \uXXXX | P0-2 (또한 P0-1 — 시스템 프롬프트 .strip() + 메모리 추가) |
| ... |
이 네 가지 모두 제1막에서 언급된 '조용한 무효화 요인(silent invalidators)'이며, 이제 프록시 개발자의 관점에서 확인된 것입니다. 이들은 단 하나의 규칙으로 귀결됩니다: 원래의 요청 바이트(request bytes)를 있는 그대로(verbatim) 전달하십시오. 만약 반드시 변형(mutate)해야 한다면, messages의 끝부분(tail)에 대해서만 정밀한 바이트 조각 교체(surgical byte-fragment replacement)를 수행해야 하며, 전체 엔벨로프(envelope)를 파싱 후 재방출(parse-and-re-emit)해서는 절대로 안 됩니다. 또한 다음 요청에서 cache_read가 변하지 않았음을 확인하십시오. 이를 무시하는 압축기(compressor)는 토큰을 절약하는 것보다 콜드 리라이트(cold rewrites)로 인해 훨씬 더 많은 비용을 잃게 됩니다.
⚠️ 실수 — JSON을 재직렬화(re-serializes)하는 "압축" 또는 "로깅" 프록시를 추가하는 것. 재인코딩이 캐시 안전(cache-safe)을 유지하려면 모든 턴에서 정확히 동일한 바이트를 재현해야만 합니다. 하지만 단순한(naive) 코드는 결코 이를 수행하지 못합니다 (구분자, 이스케이프, 키 순서, 부동 소수점 형식, 또는 바이트 복사된 메시지와 재직렬화된 메시지의 혼용 등이 모두 드리프트됩니다). 실제로 프록시를 거치는 모든 트래픽의 히트율(hit rate)은 ~0으로 떨어집니다. 이것은 프록시를 사용할 때 저지를 수 있는 가장 값비싼 실수입니다.
✅ 해결책 — 원래의 요청 바이트를 있는 그대로(verbatim) 전달하십시오. 전체 엔벨로프를 파싱 후 재방출하는 방식이 아니라,
messages의 끝부분에 대해서만 정밀한 바이트 조각 교체 방식으로 변형하십시오. 프록시를 삽입한 후cache_read를 통해 검증하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기