동일한 토큰에 비용을 두 번 지불하지 마세요: 프롬프트 캐싱 (Prompt Caching) 실전 가이드
요약
프롬프트 캐싱을 통해 반복되는 입력 토큰에 대한 비용을 최대 90% 절감하고 응답 속도를 높이는 방법을 설명합니다. Anthropic API의 cache_control 사용법과 효율적인 캐싱을 위한 프롬프트 구조 설계 전략을 다룹니다.
핵심 포인트
- cache_control을 사용하여 반복되는 접두사(prefix)를 캐싱하고 비용을 절감할 수 있습니다.
- 변하지 않는 정적 데이터(시스템 프롬프트, 문서 등)를 프롬프트 앞부분에 배치해야 합니다.
- 캐시 히트 시 입력 비용이 약 90% 할인되어 경제적입니다.
- usage 필드의 input_tokens는 캐시 중단점 이후의 토큰만을 의미함을 주의해야 합니다.
당신은 챗봇을 구축했습니다. 모델이 "그럼 화성은 어떤가요?"라는 질문에 답할 수 있도록 매 턴마다 대화 전체—8,000토큰의 시스템 프롬프트 (system prompt), 업로드된 PDF, 15개의 히스토리 메시지—를 다시 보냅니다.
모델은 그 모든 것을 다시 읽습니다. 매번 말이죠. 당신은 그 모든 것에 대해 전체 가격을 지불합니다. 매번 말이죠.
프롬프트 캐싱 (Prompt Caching)이 이를 해결합니다. 대략 한 줄의 추가적인 JSON이면 충분하며, 반복되는 부분에 대해 입력 비용을 약 90%까지 절감하면서 응답 속도를 눈에 띄게 빠르게 만들 수 있습니다.
함께 살펴보겠습니다.
한 줄 요약 버전
요청의 최상위 레벨에 cache_control을 추가하세요:
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
...
이것이 **자동 캐싱 (automatic caching)**입니다. API는 요청 내의 마지막 캐싱 가능한 블록까지의 모든 내용을 캐싱합니다. 다음에 동일한 내용으로 시작하는 요청을 보내면, 해당 접두사 (prefix)는 다시 처리되는 대신 캐시에서 읽힙니다.
만약 curl 방식이 더 편하다면 동일한 방식입니다:
curl https://api.anthropic.com/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
...
멘탈 모델: 이것은 접두사 (prefix) 캐시입니다
이것은 반드시 내재화해야 할 가장 중요한 사항입니다.
당신의 프롬프트는 정해진 순서대로 읽힙니다:
tools → system → messages
캐싱은 해당 시퀀스의 **접두사 (prefixes)**를 대상으로 작동합니다. 특정 블록에 cache_control을 표시하면 다음과 같이 말하는 것과 같습니다: "요청의 시작부터 이 블록을 포함할 때까지의 모든 것을 캐싱하세요."
즉, 다음과 같은 의미입니다:
- ✅ 앞부분에 있는 정적인 것들 = 캐싱 가능.
- ❌ 초반부에 무언가 변경되면 = 그 이후의 모든 것은 무효화됩니다. 따라서 황금률은 다음과 같습니다: 지루하고 변하지 않는 것들을 먼저 배치하세요. 도구 정의 (tool definitions), 시스템 지침 (system instructions), 50페이지짜리 계약서, 20개의 퓨샷 (few-shot) 예시들. 그런 다음 변동성이 큰 것들—사용자의 실제 질문—을 마지막에 배치합니다. ---
비용 (및 절감액)
하나의 가격 계층 대신 세 개의 가격 계층이 존재합니다:
| Multiplier vs. base input | |
|---|---|
| 5분 캐시 쓰기 (write) | 1.25× |
| ... |
따라서 Claude Opus 5 ($5/MTok input)의 경우:
- 첫 번째 요청 시 캐시 쓰기: $6.25/MTok
- 이후 모든 히트 (hit): $0.50/MTok 한 번만 25%의 프리미엄을 지불하면, 그 이후로는 영구적으로 90% 할인된 가격으로 이용할 수 있습니다. 접두사 (prefix)를 단 두 번만 재사용하더라도 이미 이득입니다. 캐시 중단점 (breakpoint) 자체는 무료입니다. 중단점을 설정하는 것에 대해서는 비용이 청구되지 않으며, 실제로 쓰이고 읽힌 토큰에 대해서만 비용이 청구됩니다.
사용량 필드 읽기 (모두가 실수하는 부분)
"usage": {
"cache_read_input_tokens": 100000,
"cache_creation_input_tokens": 0,
...
input_tokens는 귀하의 전체 입력이 아닙니다. 이는 마지막 캐시 중단점 이후의 토큰만을 의미합니다. 총합은 다음과 같습니다:
total = cache_read_input_tokens + cache_creation_input_tokens + input_tokens
따라서 위의 예시는 50개가 아니라 100,050개의 토큰을 처리한 것입니다. 유용한 부수 효과로, 캐시 히트 (cache hits)는 새로운 입력과 달리 속도 제한 (rate limits)에 영향을 주지 않으므로 실질적인 처리량 (throughput)도 향상됩니다.
빠른 점검 사항: 만약 cache_creation_input_tokens와 cache_read_input_tokens가 모두 0이라면, 아무것도 캐싱되지 않은 것입니다. 아마도 최소 길이(아래 참조) 미만일 가능성이 높습니다. API는 에러를 발생시키지 않고 그냥 조용히 캐싱을 건너뜁니다.
최소 크기 — 이 부분을 놓치지 마세요
모델별 하한선보다 짧은 프롬프트는 캐싱되지 않습니다:
| 모델 | 최소 캐싱 가능 토큰 |
|---|---|
| Opus 5, Fable 5, Mythos 5 | 512 |
| ... |
만약 기준선에 간신히 못 미친다면, 기준을 넘기기 위해 캐싱될 섹션에 내용(더 많은 예시, 더 많은 컨텍스트)을 추가할 가치가 충분히 있습니다. 캐시 읽기 비용이 충분히 저렴하기 때문에, 추가된 토큰 비용은 그 이상의 가치를 합니다.
멀티턴 대화 (Multi-turn conversations): 흐름에 맡기세요
자동 캐싱을 사용하면, 대화가 진행됨에 따라 중단점이 스스로 앞으로 이동합니다:
| 요청 (Request) | 발생하는 일 |
|---|---|
| 1 | System + U1 + A1 + **U2** → 모든 내용이 캐시 (cache)에 기록됨 |
| ... |
별도의 장부 기록(bookkeeping)이 필요 없습니다. 마커를 옮겨 다닐 필요도 없습니다. 각 턴(turn)은 이전 대화 전체를 캐시에서 읽어오고, 새로운 부분만 기록합니다. 이것은 채팅 앱을 위한 최고의 기본 설정입니다.
명시적 중단점 (Explicit breakpoints): 직접 제어해야 할 때
프롬프트의 각 부분이 서로 다른 속도로 변경된다면, 개별 블록에 cache_control을 설정하세요. 최대 **4개의 중단점 (breakpoints)**을 지정할 수 있습니다.
전형적인 RAG-에이전트(RAG-agent) 레이아웃은 다음과 같습니다:
{
"tools": [ /* ... */, { "name": "get_document", "cache_control": {"type": "ephemeral"} } ],
"system": [
...
네 개의 독립적인 세그먼트 (segments):
- 도구 (Tools) — 기본적으로 거의 변경되지 않음
- 지침 (Instructions) — 배포 시 변경됨
- RAG 문서 (RAG documents) — 매일 변경됨
- 대화 (Conversation) — 매 턴 변경됨
RAG 문서를 교체하더라도 세그먼트 1과 2는 유지됩니다. 대화 턴을 추가하더라도 1, 2, 3번 세그먼트가 유지됩니다. 변경 사항은 해당 세그먼트와 그 뒤에 오는(downstream) 모든 세그먼트만 무효화합니다.
말 그대로 모든 사람이 저지르는 실수
비용이 많이 들면서도 조용히 발생하는, 여러분이 꼭 기억해야 할 버그가 있습니다.
여러분의 프롬프트가 다음과 같다고 가정해 봅시다: 1~5번 블록은 거대한 정적 시스템 컨텍스트 (static system context)입니다. 6번 블록은 f"[{timestamp}] {user_message}"입니다. 6번 블록이 마지막 부분이라 적절해 보인다는 이유로, 여러분은 6번 블록에 cache_control을 설정합니다.
- 요청 1 (Request 1): 6번 블록에서 캐시가 작성됩니다. 해시(hash)에는 타임스탬프(timestamp)가 포함됩니다.
- 요청 2 (Request 2): 타임스탬프가 다름 → 해시가 다름 → 미스 (miss). 시스템은 항목을 찾기 위해 5, 4, 3, 2, 1번 블록을 역순으로 탐색하지만... 그 어떤 요청도 그곳에 항목을 작성한 적이 없습니다.
- 결과: 매 요청마다 새로운 캐시가 작성됩니다. 여러분은 영원히 1.25배의 프리미엄 비용을 지불하게 되며, 단 한 번의 읽기(read)도 얻지 못합니다. 역방향 탐색(lookback)은 여러분의 중단점(breakpoint) 뒤에 있는 안정적인 콘텐츠를 찾아 캐싱해 주지 않습니다. 시스템은 이전 요청들이 작성한 항목만을 찾으며, 쓰기(write)는 오직 중단점에서만 발생합니다. 해결책은 단 한 줄입니다.
cache_control을 모든 요청에서 동일한 마지막 블록인 5번 블록으로 옮기세요.
경험 법칙 (Rule of thumb): 캐시를 공유하고자 하는 요청들 사이에서 접두사(prefix)가 동일한 마지막 블록에 중단점을 설정하세요. (참고: 자동 캐싱(automatic caching)도 마지막 블록을 대상으로 하기 때문에 동일한 함정에 빠집니다. 만약 마지막 블록에 요청마다 변하는 타임스탬프가 포함되어 있다면, 대신 정적 접두사(static prefix)에 명시적인 중단점을 사용하세요.)
20-블록 역방향 탐색 창 (The 20-block lookback window)
관련된 주의 사항입니다. 캐시 히트(cache hit)를 찾을 때, 시스템은 중단점의 위치를 확인한 후 역순으로 탐색하지만, 오직 20개 블록까지만 탐색합니다.
- 턴 1 (Turn 1): 10개 블록, 중단점은 10번. 10번 블록에 항목이 작성됨.
- 턴 2 (Turn 2): 15개 블록, 중단점은 15번. 10번까지 역순 탐색하여 턴 1의 항목을 찾음. 히트 (Hit)! 11~15번 블록만 새로 처리됨.
- 턴 3 (Turn 3): 35개 블록, 중단점은 35번. 35번부터 16번 블록까지 확인하지만 아무것도 찾지 못함. 15번 블록에 있는 턴 2의 항목은 탐색 창(window)의 범위를 한 칸 벗어나 있습니다. 미스 (Miss). 전체 재처리.
만약 대화가 한 번의 턴에서 20개 이상의 블록만큼 건너뛸 수 있다면, 필요하기 전에 쓰기가 누적될 수 있도록 더 앞쪽에 두 번째 중단점을 추가하세요.
5분 vs 1시간 결정 (The 5-minute vs 1-hour decision)
기본 TTL(Time To Live)은 5분이며, 캐시에 접근할 때마다 무료로 갱신됩니다. 활발한 채팅 세션은 기본적으로 스스로를 '웜 상태(warm)'로 유지합니다.
"cache_control": { "type": "ephemeral", "ttl": "1h" }
다음과 같은 경우에는 1시간(1h)을 고려하세요:
- 후속 질문(Follow-ups)이 5분 _이후_에 발생하지만 1시간 _이내_에 발생할 가능성이 있는 경우 (자리를 비운 사용자, 실행 시간이 긴 에이전트 하위 작업 등)
- 지연된 후속 질문에서 지연 시간(Latency)이 중요한 경우
- 작업이 보통 5~60분 정도 소요되는 배치(Batching) 작업을 수행하는 경우. 프롬프트가 5분보다 더 자주 사용된다면 5분(5m) 설정을 유지하세요. 새로고침(Refreshes)은 무료이므로, 그렇지 않으면 아무 이유 없이 쓰기 비용을 2배로 지불하게 됩니다. 하나의 요청 내에서 여러 TTL을 혼합하는 것은 허용되지만, 한 가지 규칙이 있습니다: 더 긴 TTL이 반드시 먼저 와야 합니다. 5분 블록 앞에 1시간 블록을 배치하세요.
보너스: 캐시 사전 예열 (pre-warm the cache)
지연 시간(Latency)에 민감한 애플리케이션인가요? 사전 예열을 하지 않는다면, 그날의 첫 번째 사용자가 캐시 미스(cache-miss) 페널티를 감수해야 합니다:
SYSTEM_PROMPT = [{
"type": "text",
"text": "You are an expert software engineer...",
...
max_tokens: 0은 프롬프트를 읽어 들여 중단점(breakpoint)에서 캐시를 작성한 뒤, 빈 content 배열과 stop_reason: "max_tokens"를 반환하며 즉시 종료됩니다. 출력 토큰(output tokens) 비용은 전혀 청구되지 않습니다. (물론 캐시 쓰기 비용은 여전히 지불해야 합니다.)
정확히 지켜야 할 두 가지 사항이 있습니다:
- 중단점을 "warmup" 플레이스홀더가 아닌 공유된 (shared) 콘텐츠(시스템 프롬프트)에 설정하세요. 그렇지 않으면 엔트리가 플레이스홀더를 기준으로 키(key)가 생성되어 실제 트래픽이 캐시를 전혀 활용하지 못하게 됩니다. 이것이 사전 예열(pre-warming)에 자동 캐싱이 아닌 명시적인 중단점이 필요한 이유입니다.
- 실제 요청과 동일한 사고 설정(thinking config) 및
effort설정을 사용하세요. 이 설정들은 프롬프트에 렌더링되므로, 사전 예열 설정이 일치하지 않으면 아무도 사용하지 않는 엔트리가 작성됩니다.max_tokens: 0은stream: true, 확장된 사고(extended thinking), 구조화된 출력(structured outputs), 강제된tool_choice, 또는 배치(Batches) 요청 내부에서는 거부됩니다.
캐시를 깨뜨리는 요소
캐시 히트(Cache hits)를 위해서는 100% 바이트 단위로 동일한 (byte-identical) 접두사(prefix)가 필요합니다. 캐시를 무효화하는 요소들은 다음과 같습니다:
| 변경 사항 | 영향 범위 (Blast radius) |
|---|---|
| 도구 정의 (Tool definitions) | 전체 (Everything) |
| ... |
한 가지 교묘한 사례가 있습니다: 일부 언어(Go, Swift)는 JSON을 직렬화(serializing)할 때 맵(map) 키의 순서를 무작위로 정렬합니다. 만약 tool_use 블록의 키 순서가 뒤섞여 나온다면, 캐시 히트(cache hit)가 발생하지 않으며 그 이유를 파악하기 어려울 것입니다. 키 순서를 고정(Pin)하세요.
또한: 캐시는 조직(organization)별로 격리되어 있으며, Claude API의 경우 워크스페이스(workspace)별로도 격리됩니다. 그리고 캐시 엔트리는 첫 번째 응답이 시작된 이후에만 사용할 수 있습니다. 따라서 동일한 접두사(prefix)로 10개의 요청을 병렬로 보내면 10개 모두 캐시 미스(miss)가 발생합니다. 요청을 하나 보내고 기다린 다음, 팬 아웃(fan out) 하세요.
트러블슈팅 체크리스트 (Troubleshooting checklist)
캐시가 작동하지 않나요? 다음 목록을 확인해 보세요:
- 캐시된 섹션이 호출 간에 **바이트 단위로 동일(byte-identical)**한가?
- 모델의 **최소 토큰 수(minimum token count)**를 초과했는가?
- 중단점(breakpoint)이 변하지 않는(stays the same) 블록(타임스탬프나 사용자 입력이 없는 곳)에 위치해 있는가?
- 호출이 TTL(Time To Live) 범위 내에서 이루어지고 있는가?
-
tool_choice, 이미지 존재 여부, 사고 설정(thinking config),effort설정이 **일관(consistent)**된가? - JSON의 **키 순서가 안정적(stable)**인가?
- 대화 내용이 길어져서 20-블록 룩백(20-block lookback) 범위를 벗어났는가?
예상치 못한 캐싱 설정(긍정적이든 부정적이든)을 경험하셨나요? 댓글로 공유해 주세요. 🚀
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기