프롬프트 캐싱으로 Claude API 비용 70% 줄인 실전 후기
2026.04.14API 비용 청구서를 보고 정신이 들었다
내가 만든 작은 사이드 프로젝트가 Claude API를 호출한다. 사용자 1명당 호출 5번, 호출당 평균 토큰 8천 개 정도. 첫 달 청구서를 보고 깜짝 놀랐다.
대부분이 입력 토큰 비용이었다. 시스템 프롬프트와 도구 정의가 호출마다 통째로 다시 청구되고 있었다. 같은 프롬프트를 매번 새로 읽히는 셈이다.
프롬프트 캐싱을 적용한 다음, 같은 트래픽에서 비용이 70% 가까이 줄었다. cache_control 한 줄이 만든 차이다. 이 글은 그 한 줄에 도달한 과정과, 거기서 더 짜낸 최적화들이다.
프롬프트 캐싱이 정확히 뭔가
Anthropic의 프롬프트 캐싱은 자주 반복되는 프롬프트 앞부분을 캐시에 저장하는 기능이다. 다음 호출에서 같은 앞부분을 보내면 캐시 히트가 일어나고, 그 부분의 입력 비용이 90%까지 할인된다.
캐시 작성 비용은 표준 입력의 1.25배다. 즉 한 번만 호출되는 프롬프트는 오히려 손해다. 두 번 이상 재사용되면 이득이고, 다섯 번 이상이면 큰 이득이다.
제약은 두 가지다.
- TTL(Time To Live)이 5분 — 5분 동안 같은 프롬프트가 다시 호출되지 않으면 캐시가 사라진다 (1시간 캐시도 있지만 더 비싸다)
- 앞부분이 정확히 동일해야 — 한 글자라도 다르면 캐시 미스
가장 단순한 적용
@anthropic-ai/sdk로 호출할 때, 캐싱하고 싶은 메시지 끝에 cache_control을 붙인다.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-4-7",
max_tokens: 1024,
system: [
{
type: "text",
text: LONG_SYSTEM_PROMPT,
cache_control: { type: "ephemeral" },
},
],
messages: [{ role: "user", content: userMessage }],
});이 한 줄로 시스템 프롬프트가 캐시에 저장된다. 다음 호출이 5분 내에 같은 시스템 프롬프트로 들어오면 캐시 히트.
응답 객체에 캐시 정보가 같이 온다.
console.log(response.usage);
// {
// input_tokens: 50,
// cache_creation_input_tokens: 0,
// cache_read_input_tokens: 8000,
// output_tokens: 200
// }cache_read_input_tokens가 0보다 크면 히트한 거다.
어디까지 캐시할 수 있는가
cache_control은 메시지 단위로 최대 4개까지 붙일 수 있다. 즉 캐시 영역을 4개로 나눠 운영할 수 있다.
내가 실제 프로젝트에서 쓰는 분할:
{
system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }],
tools: [
// 도구 정의들
{ ..., cache_control: { type: "ephemeral" } } // 마지막 도구에 붙임
],
messages: [
{ role: "user", content: [
{ type: "text", text: BIG_DOCUMENT, cache_control: { type: "ephemeral" } }
]},
{ role: "assistant", content: ASSISTANT_REPLY },
{ role: "user", content: [{ type: "text", text: NEW_QUESTION }] }
]
}세 영역을 캐시한다. 시스템 프롬프트, 도구 정의, 컨텍스트 문서. 가장 자주 변하는 마지막 사용자 질문만 캐시 밖이다.
캐시 히트율을 높이는 설계 원칙
캐시는 앞부분이 정확히 같아야 동작한다. 이걸 의식하고 프롬프트를 설계하는 게 핵심이다.
원칙 1: 변하지 않는 것을 앞에 둔다
[변하지 않는 시스템 프롬프트] ← 캐시
[변하지 않는 도구 정의] ← 캐시
[자주 변하는 사용자 컨텍스트]
[지금 이 호출의 질문]시간순으로 정리하지 말고, 변동성 순으로 정리한다.
원칙 2: 동적 값을 시스템 프롬프트에 박지 않는다
이런 코드를 자주 본다.
const systemPrompt = `Today is ${new Date().toISOString()}. User name is ${userName}. ...`;날짜와 사용자 이름이 매번 바뀌니까 캐시가 절대 히트하지 않는다. 동적 값은 사용자 메시지 쪽으로 빼야 한다.
const systemPrompt = `You are an assistant that follows the rules below. ...`; // 고정
const userMessage = `Today: ${date}. My name: ${userName}. Question: ${q}`; // 동적원칙 3: 무관한 텍스트를 시스템 프롬프트에 끼워넣지 않는다
A/B 테스트, 디버그 ID, 트레이싱 정보를 시스템 프롬프트에 넣으면 안 된다. 캐시를 깨는 가장 흔한 실수다.
5분 TTL을 어떻게 다루는가
5분 TTL이 가장 까다로운 제약이다. 트래픽이 5분에 한 번 미만이면 캐시가 계속 만료된다.
내 대응법은 두 가지다.
방법 1: 워밍 호출
트래픽이 적은 시간대에는 5분마다 더미 호출을 보내서 캐시를 살려둔다.
// 5분마다 한 번
setInterval(async () => {
await client.messages.create({
model: "claude-opus-4-7",
max_tokens: 1,
system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }],
messages: [{ role: "user", content: "ping" }],
});
}, 4 * 60 * 1000); // 4분에 한 번 (안전 마진)비용 계산을 해보고 결정한다. 워밍 호출 비용 < 캐시 미스 비용이면 이득이다.
방법 2: 1시간 캐시
비싼 옵션이다. cache_control: { type: "ephemeral", ttl: "1h" }로 1시간 TTL을 쓸 수 있다. 캐시 작성 비용이 표준의 2배지만, 트래픽이 분당 한 번 정도면 5분보다 1시간이 유리하다.
내 사이드 프로젝트는 트래픽 패턴이 한 번 사용자가 들어오면 10분쯤 머물고 가는 식이라, 1시간 캐시가 잘 맞았다.
비용을 실측하는 방법
캐싱 효과를 감으로 판단하면 안 된다. 호출마다 usage를 로깅한다.
const log = {
input: response.usage.input_tokens,
cache_create: response.usage.cache_creation_input_tokens,
cache_read: response.usage.cache_read_input_tokens,
output: response.usage.output_tokens,
};
// 비용 계산 (Opus 4.7 기준 예시 단가)
const PRICING = {
input: 15 / 1_000_000,
cache_create: 18.75 / 1_000_000,
cache_read: 1.5 / 1_000_000,
output: 75 / 1_000_000,
};
const cost =
log.input * PRICING.input +
log.cache_create * PRICING.cache_create +
log.cache_read * PRICING.cache_read +
log.output * PRICING.output;이걸 모든 호출에 박고 매일 합계를 본다. 캐시 히트율이 떨어지면 즉시 알 수 있다. 단가는 Anthropic 공식 페이지에서 항상 최신을 확인한다.
흔한 함정 4가지
함정 1: 캐시가 동작 안 한다고 생각하지만 동작한다
cache_creation_input_tokens가 0이고 cache_read_input_tokens도 0일 때, "캐시가 안 되네"라고 생각하기 쉽다. 하지만 입력이 1024 토큰 미만이면 캐싱이 적용 안 된다는 최소 크기 제약이 있다. 짧은 프롬프트는 캐시 대상이 아니다.
함정 2: 도구 순서를 매번 바꾼다
도구 배열의 순서가 바뀌면 캐시가 깨진다. 도구 정의를 동적으로 정렬하는 코드가 있다면 멈춰야 한다. 항상 같은 순서를 보장한다.
함정 3: 메시지 형식이 미세하게 다르다
사용자 메시지를 어떨 때는 string으로, 어떨 때는 [{type: "text", text: "..."}]로 보내면 다른 입력으로 처리된다. 한 가지 형식으로 통일한다.
함정 4: 캐시 작성을 너무 자주 한다
cache_control을 마구 붙이면 캐시 작성 비용(표준의 1.25~2배)이 누적된다. 캐시는 2회 이상 재사용될 가능성이 있을 때만 붙인다. 한 번 쓰고 버려질 컨텍스트는 캐시하지 않는다.
실측 결과
내 프로젝트 한 달 비교:
- 캐싱 적용 전: 월 약 220만 토큰 입력 / $33
- 캐싱 적용 후: 월 약 220만 토큰 입력 (그중 캐시 히트 80%) / $10
비용 70% 감소. 응답 속도도 부수 효과로 빨라졌다(캐시 히트 시 latency가 낮아진다).
내가 한 일은 이게 전부다.
- 시스템 프롬프트에
cache_control추가 - 도구 정의에
cache_control추가 - 동적 값을 시스템 프롬프트에서 사용자 메시지로 이동
- 트래픽 적은 시간대에 4분 간격 워밍 호출
정리
프롬프트 캐싱은 한 줄 추가로 끝나지 않는다. 캐시 히트율을 높이는 프롬프트 구조가 본질이다. 변하지 않는 것을 앞에, 변하는 것을 뒤에. 동적 값은 시스템에서 사용자로. 이 두 원칙만 지켜도 캐시 효과가 두 배로 살아난다.
API로 Claude를 쓰는 모든 서비스는 캐싱을 적용해야 한다. 비용이 70% 줄어드는 기능을 안 쓸 이유가 없다. 처음 한 번 설계만 신경 쓰면, 그 다음부터는 자동이다.