🔥 Claude 프롬프트 캐싱, 공식 문서 안쪽 규칙들

#Claude#Anthropic#프롬프트 캐싱#Claude Code#LLM 비용 최적화
1763자
21분

Claude 프롬프트 캐싱 prefix hash와 20블록 lookback 개념도

우리 팀에서 지난 며칠 새 Claude Code 쿼터 소진 사건, 2.1.108의 ENABLE_PROMPT_CACHING_1H 플래그까지 두 편이 나가는 동안 나는 계속 한 가지가 걸렸다. 사건은 이해했는데 "캐시가 정확히 어떻게 저장되고 어떻게 끊기는가"를 내가 설명할 수 있는가 했을 때, 말이 막혔다.

그래서 자리에 앉아 Anthropic의 프롬프트 캐싱 공식 문서를 처음부터 끝까지 다시 읽었다. 결론부터 말하면, TTL이나 가격 얘기만 하고 넘긴 글은 절반만 읽은 거다. 문서 중간에 묻혀 있는 세 덩어리, 즉 prefix hash 메커니즘, 20블록 lookback 윈도, 그리고 thinking block과 캐시의 상호작용. 이 세 가지가 실은 과금이 왜 그렇게 튀는지를 가장 정확히 설명해준다. 이 글은 그 안쪽 규칙들을 하나씩 정리한 기록이다.

캐시는 "문서"를 기억하지 않는다, "앞에서부터 여기까지의 prefix"를 기억한다

공식 문서가 가장 먼저 박는 개념이 이거다. 캐시 엔트리는 요청의 앞쪽부터 cache_control이 찍힌 블록까지를 이어 붙인 전체 prefix의 해시를 키로 저장된다. 순서는 고정: toolssystemmessages.

이 한 줄이 실무에서 갖는 의미가 꽤 크다. "내 시스템 프롬프트를 캐시했다"는 말은 기술적으로 정확하지 않다. 저장된 건 "그 시점까지의 tools + system + messages 전부가 이어 붙은 prefix"다. tools 배열에서 파라미터 설명 한 줄만 고쳐도, 그 뒤의 system 캐시도 messages 캐시도 전부 무효가 된다. 계층적이다.

prefix hash와 20블록 lookback — 가장 오해받는 부분

여기가 문서에서 제일 파고들 만한 섹션이다. Anthropic이 "three core principles"라는 이름으로 정리한 세 원칙.

첫째, 캐시 쓰기(write)는 breakpoint에서만 일어난다. cache_control을 찍은 그 블록 하나에 대해서만 엔트리를 만든다. 그 앞쪽 중간 지점들에 대해서는 아무것도 저장하지 않는다.

둘째, 캐시 읽기(read)는 이전 요청이 써둔 엔트리를 찾는 작업이다. 현재 breakpoint 위치에서 prefix hash를 계산해 매칭하는 엔트리가 있는지 본다. 없으면 한 블록 뒤로 걸어가 다시 계산해 본다. 그 반복이다.

셋째, lookback 윈도는 20블록이다. 20번 시도해서도 못 찾으면 포기한다.

이 세 원칙을 합치면 많이들 오해하는 동작 하나가 풀린다. "시스템 프롬프트가 안 바뀌면 알아서 캐시되겠지"는 틀리다. 시스템이 찾는 건 "변하지 않은 내용"이 아니라 "이전 요청이 거기에 써둔 기록"이다. 쓴 적이 없는 위치에서는 read가 영원히 미스다.

공식 문서의 "common mistake" 예시를 거의 그대로 옮기면 이런 상황이 된다.

블록 1~5번이 정적인 시스템 컨텍스트, 6번이 매 요청마다 바뀌는 타임스탬프 + 유저 메시지. cache_control을 6번에 찍어 두면?

  • 요청 1: 블록 6에 write. 해시에 타임스탬프가 들어감.
  • 요청 2: 타임스탬프가 다르므로 블록 6의 prefix hash가 다름. lookback이 블록 5, 4, 3, 2, 1로 내려가지만, 거기엔 어떤 요청도 엔트리를 쓴 적이 없음. 미스.

결과: 매 요청마다 cache_creation만 찍히고 cache_read는 0인, 최악의 과금 패턴. 문서가 이걸 "silently inflating costs"의 전형이라 부르는 이유다.

해결은 간단하다. cache_control요청 간에 동일한 마지막 블록 위에 찍는다. 위 케이스에선 블록 5. 그러면 매 요청마다 블록 5의 prefix hash가 같아서 read 히트가 나고, 블록 6은 breakpoint 뒤의 꼬리라 원래부터 캐시 대상이 아니었으니 문제 없다.

20블록 lookback이 왜 있는지도 이 맥락에서 보인다. 대화가 성장해서 breakpoint가 매 턴 뒤로 밀려도, 이전 턴의 write가 20블록 안에 있으면 자동으로 hit이 난다. 반대로 한 턴에 20블록 이상을 추가하는 상황(긴 문서 붙여넣기, 병렬 도구 호출 결과 다발 주입)이 한 번이라도 터지면 그 턴 이후의 캐시 체인이 끊어진다. 문서가 "add a second breakpoint closer to that position"을 권하는 이유가 여기다.

자동 캐싱, 문서를 다시 읽으면서 알게 된 새 패턴

국내 블로그들이 대부분 "블록별로 cache_control 찍기"만 다루는데, 공식 문서는 지금 자동 캐싱(automatic caching) 을 첫 번째 예시로 밀고 있다. 요청 바디 최상단에 cache_control 한 줄만 박으면 시스템이 "마지막 캐시 가능한 블록"에 알아서 breakpoint를 찍는다.

json
{
  "model": "claude-opus-4-6",
  "max_tokens": 1024,
  "cache_control": { "type": "ephemeral" },
  "system": "You are a helpful assistant that remembers our conversation.",
  "messages": [
    { "role": "user", "content": "My name is Alex. I work on ML." },
    { "role": "assistant", "content": "Nice to meet you, Alex!" },
    { "role": "user", "content": "What did I say I work on?" }
  ]
}
json
{
  "model": "claude-opus-4-6",
  "max_tokens": 1024,
  "cache_control": { "type": "ephemeral" },
  "system": "You are a helpful assistant that remembers our conversation.",
  "messages": [
    { "role": "user", "content": "My name is Alex. I work on ML." },
    { "role": "assistant", "content": "Nice to meet you, Alex!" },
    { "role": "user", "content": "What did I say I work on?" }
  ]
}

대화가 길어지면 breakpoint 위치도 자동으로 따라 움직인다. 멀티턴 대화에는 편하다.

다만 함정이 있다. 자동 caching이 붙는 위치가 "마지막 캐시 가능한 블록"이라, 그 블록이 요청마다 바뀌는 블록(방금 본 타임스탬프 트랩)이면 앞서 설명한 miss 루프에 그대로 빠진다. 문서에 "Automatic caching hits the same trap"이라고 명시돼 있다. 이럴 때는 자동 캐싱 대신 명시적 breakpoint를 정적 prefix의 끝에 찍어야 한다.

자동 캐싱과 명시적 breakpoint는 섞어 쓸 수 있다. 단, 자동 쪽이 요청당 최대 4개인 breakpoint 슬롯 중 하나를 가져간다. tools 따로, system 따로, 그리고 자동 캐싱으로 messages까지. 이렇게 조합하는 게 실용적이다.

무엇이 캐시를 깨는가 — 계층 테이블

공식 문서의 "What invalidates the cache" 표가 실은 이 글에서 제일 자주 들여다볼 조각이다. 계층은 tools → system → messages. 상위가 바뀌면 하위도 전부 깨진다.

자주 발 걸리는 항목만 뽑으면 이렇다.

  • tool 정의 수정: 이름, 설명, 파라미터 하나만 바뀌어도 tools, system, messages 전체 무효.
  • web search / citations 토글: 시스템 프롬프트가 내부적으로 바뀌므로 system과 messages 무효.
  • 이미지 추가/제거: 어디에 추가되든 messages 무효.
  • tool_choice 변경: messages만 무효.
  • thinking 파라미터 변경: budget을 살짝 조절한 것만으로도 messages 무효.
  • extended thinking에 non-tool-result 유저 블록 추가: 이전 thinking 블록들이 컨텍스트에서 전부 빠지고, 그 뒤 messages도 캐시에서 함께 제거.

내가 제일 자주 놓쳤던 건 tool_choice 변경 쪽이다. "이번엔 Claude가 꼭 그 도구를 쓰도록 강제하자"며 {"type": "tool", "name": "..."}을 던지면 messages 캐시가 통째로 날아간다. 평소에 auto로 돌리다가 한 요청만 강제하는 패턴이 가장 비싸다.

그리고 문서 Troubleshooting 맨 아래에 조용히 묻혀 있는 함정이 하나 더 있다. tool_use 블록의 JSON 키 순서 문제다. Swift와 Go처럼 구조체를 JSON으로 직렬화할 때 키 순서가 결정적이지 않은 언어로 요청을 만들면, 내용이 같아도 직렬화 결과가 매번 달라서 prefix hash가 안 맞는다. 매칭은 100% 일치를 요구하니까. Go에서는 구조체 태그로 순서를 고정하거나 encoding/json이 아닌 직렬화기를 쓰고, Swift에서는 JSONEncoder.OutputFormatting = .sortedKeys를 걸어야 한다.

thinking block과 캐시의 특이한 동거

extended thinking을 쓰면서 캐싱도 같이 쓰면, 별도 규칙이 하나 더 붙는다. 이 부분은 개인적으로 문서에서 가장 신중하게 읽어야 했던 섹션이다.

thinking block 자체엔 cache_control을 못 붙인다. 그런데 이전 어시스턴트 턴에 포함된 thinking block은 요청 content가 캐시될 때 함께 캐시된다. tool_use 루프에서 tool_result를 돌려주며 대화를 이어갈 때 특히 그렇다. 그리고 캐시에서 읽혀 들어오면 input 토큰으로 과금된다.

특이한 규칙은 캐시 무효화 쪽이다.

  • tool_result만 유저 메시지로 들어오면 캐시 유효 유지.
  • tool_result가 아닌 일반 유저 메시지가 들어오면 이전의 모든 thinking block이 컨텍스트에서 제거되고, 그 뒤에 따라오는 messages까지 캐시에서 빠진다.

공식 문서는 이걸 "designates a new assistant loop"라고 설명한다. 새 사용자 입력은 기존 사고 체인을 리셋시키는 신호라는 거다. 말은 되는데, agent 설계 입장에선 낯선 동작이다. 중간에 사용자 개입이 한 번이라도 끼면 이전 thinking이 통째로 날아가는 구조니까, thinking을 길게 유지하면서 사용자 상호작용을 섞는 agent를 설계한다면 이 지점이 캐시 ROI의 병목이 된다.

usage 필드를 읽는 법

응답의 usage를 정확히 읽는 법도 문서에 박혀 있는데, 사람들이 이걸 대충 읽다가 숫자를 잘못 해석한다.

json
{
  "usage": {
    "input_tokens": 50,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 100000,
    "output_tokens": 503
  }
}
json
{
  "usage": {
    "input_tokens": 50,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 100000,
    "output_tokens": 503
  }
}

세 필드를 이렇게 구분해야 한다.

  • cache_read_input_tokens: 이번 요청에서 캐시에서 읽어 온 토큰.
  • cache_creation_input_tokens: 이번 요청에서 새로 캐시에 쓴 토큰.
  • input_tokens: breakpoint 뒤쪽, 즉 어떤 캐시에도 해당하지 않는 꼬리 토큰.

실제 총 input 토큰은 세 값을 전부 더해야 나온다. input_tokens 하나만 보고 "이번엔 50토큰밖에 안 썼네"라고 좋아하면 안 된다. 그건 breakpoint 이후의 꼬리일 뿐이고, 앞쪽 100,050토큰은 따로 과금된다.

그리고 문서가 명시한 가장 실용적인 진단 규칙: 두 캐시 필드가 모두 0이면 캐시가 안 된 상태다. 모델별 최소 캐시 길이(Opus 4.6/4.5와 Haiku 4.5는 4,096토큰, Sonnet 4.6은 2,048, Sonnet 4.5/Opus 4.1 등은 1,024)를 못 채웠거나, 앞에서 본 tool_use 키 순서 문제 같은 미묘한 이유로 해시가 안 맞는 경우다. 침묵 실패라 에러도 안 뜬다. 직접 로그를 찍어 봐야 안다.

1시간 TTL을 쓰면 cache_creation 아래 ephemeral_5m_input_tokensephemeral_1h_input_tokens가 따로 찍혀서, 어느 버킷에 얼마를 썼는지 구분할 수 있다.

여러 TTL을 섞어 쓸 때의 순서 규칙

한 요청에서 1시간과 5분 TTL을 같이 쓸 수 있다. 단 제약이 하나. 1시간 TTL 엔트리가 5분 TTL 엔트리보다 앞에 있어야 한다. 거꾸로 배치하면 API가 그냥 에러를 돌려준다.

이 규칙 덕에 자연스러운 패턴이 생긴다: 거의 바뀌지 않는 큰 컨텍스트(CLAUDE.md, 긴 reference 문서)는 1시간으로 요청 앞쪽에 걸고, 매 턴 가까이 오는 도구 결과나 최근 파일 snippet은 5분으로 뒤쪽에 건다.

2월 5일부터 바뀐 것: workspace 단위 캐시 격리

국내에서 상대적으로 덜 알려진 항목 하나. 2026년 2월 5일부로 Claude API와 Azure AI Foundry의 프롬프트 캐시가 workspace 단위로 격리된다. 그전까지는 organization 단위였다.

조직 안에 workspace가 여러 개 있는 팀에는 의미가 크다. 예전엔 workspace A의 캐시 엔트리를 workspace B가 공짜로 읽을 수 있었지만, 이제는 각 workspace가 별도로 쌓아야 한다. Amazon Bedrock과 Google Vertex AI는 기존 org 단위 격리를 유지한다.

개인 개발자에겐 체감이 없지만, 여러 workspace로 나눠 API 키를 관리하는 조직은 캐시 ROI 계산을 다시 해야 한다. 같은 prefix가 반복되더라도 workspace가 다르면 write 비용이 두 번 발생한다.

사건을 다시 보면

여기까지 읽고 나면 3월의 그 조용한 회귀가 왜 그렇게 아팠는지가 더 정확히 보인다. TTL이 1시간에서 5분으로 되돌아갔다는 건, 단순히 "캐시가 좀 더 자주 만료된다"는 뜻이 아니다. 같은 요청 패턴이라도 TTL이 짧으면 cache_creation 비율이 올라가고 cache_read 비율이 내려간다. 5분 TTL의 write는 1.25×, 1시간의 write는 2×, read는 둘 다 0.1×다. 세션이 평균 10분씩 굴러가면 1시간 TTL은 한 번 쓰고 계속 읽지만, 5분 TTL은 매 두 번째 턴마다 재적재가 일어난다.

1M 컨텍스트에서 이게 터지면, Claude Code를 만든 Boris Cherny가 직접 인정한 대로 "컴퓨터를 한 시간 떠났다가 와서 이어가면 보통 풀 cache miss"다. 토큰이 20배로 튀는 체감이 여기서 나온다. Sean Swanson의 분석이 2월 대비 타월 낭비율이 15~53% 튀었다고 보고한 건 이 구조적 차이를 측정한 결과다.

이 문서에서 진짜 가져갈 것

공식 문서를 꼼꼼히 다시 읽고 내가 정리한 액션 아이템은 이렇다.

  • 모든 API 클라이언트에서 usage 필드를 강제 로깅한다. cache_creation과 cache_read 비율을 주간 단위로 본다. 이게 없으면 "잘 캐시되고 있을 거야"는 희망일 뿐이다.
  • 명시적 breakpoint를 쓸 때는 정적 prefix의 끝에 찍는다. 요청마다 바뀌는 블록 위에 찍히면 영원히 miss다.
  • 자동 캐싱을 쓰면서 마지막 블록이 가변적이라면, 자동이 아니라 명시적 breakpoint로 바꾼다.
  • tool_choice와 thinking 파라미터 변경을 요청 단위로 일관되게 유지한다. 변덕스러운 설정 변경이 messages 캐시를 매번 날린다.
  • Go/Swift 코드가 요청을 만든다면 JSON 키 순서를 강제 고정한다.
  • 20블록 이상을 한 번에 추가하는 패턴(긴 문서 붙여넣기, 병렬 도구 결과 대량 주입)이 있으면, 미리 앞쪽에 두 번째 breakpoint를 심어둔다.

프롬프트 캐싱은 "켜기만 하면 자동으로 싸지는 기능"이 아니다. 쓰는 쪽이 prefix 구조를 의식하고, usage로 실측하고, 계층 무효화 규칙을 염두에 두어야 비용이 실제로 떨어진다. 공식 문서가 이 규칙들을 빠짐없이 써두고 있는데도 대부분의 글이 TTL과 가격표만 다루고 지나가는 건 아쉽다. 다음에 캐시가 의심스러우면 "TTL 바꿔야지" 전에 usage부터 찍어 보자. 열 번 중 아홉 번은 그 안에 답이 있다.

참고 자료

YouTube 영상

채널 보기
우리가 매일 쓰는 맞춤법 검사기와 라우터 속에 숨겨진 알고리즘은? | Trie 자료구조 이야기
AI는 왜 수백 차원의 벡터를 사용할까? 고차원 공간과 행렬 | 선형대수학
AI는 데이터를 어떻게 분류할까? 벡터의 거리와 KNN 알고리즘 | 선형대수학
트라이(Trie)에서 단어를 삭제하는 방법 | Trie 자료구조 이야기
트라이(Trie)를 이용한 자동 완성 알고리즘 | Trie 자료구조 이야기
직교성과 벡터 투영 | 선형대수학
AI를 위한 선형대수학 - 소개 | 선형대수학
내적의 기하학적 의미와 코사인 유사도 원리 | 선형대수학