Claude / OpenAI / Gemini의 캐싱 방식 비교
앞 글에서 prompt cache의 원리("prefix 일치율")를 다뤘다. 이번 글에서는 그 원리를 세 provider가 각자 어떻게 구현했는지를 비교한다.
대충 결론부터.
말로 정리하면 이렇다.
Claude : 개발자가 명시적으로 cache breakpoint를 박는다
OpenAI : 자동. 호출 결과의 cached_tokens로 사후 확인
Gemini : 자동(implicit) + 수동(explicit context caching) 두 모드
같은 코드를 세 provider에 그냥 옮기면 캐시 동작이 다르다. 그래서 "Agent 프레임워크가 알아서 해주겠지"라는 믿음은 매우 위험하다.
Claude (Anthropic)
어떻게 동작하나
명시적이다. 개발자가 cache_control을 박은 위치까지가 캐시 prefix가 된다.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-6",
system=[
{
"type": "text",
"text": LONG_SYSTEM_RULES,
"cache_control": {"type": "ephemeral"}, # ← 캐시 경계
}
],
tools=[
{
"name": "search_docs",
"description": "...",
"input_schema": {...},
"cache_control": {"type": "ephemeral"}, # ← tool도 캐시 가능
}
],
messages=[
{"role": "user", "content": user_question},
],
)캐싱 단위
ephemeral : 약 5분 TTL (기본)
1h : 1시간 TTL (가격 다름, opt-in)
어디까지 캐시할 수 있나
- tool definitions (스키마 통째로)
- system 메시지
- messages 안의 텍스트 블록
- 이미지/PDF도 캐시 가능
- 한 요청에 최대 4개의 cache breakpoint
사용량 확인
응답의 usage에 다음이 잡힌다.
cache_creation_input_tokens : 이번 호출에서 캐시를 새로 만든 토큰 수
cache_read_input_tokens : 이번 호출에서 캐시에서 읽은 토큰 수
input_tokens : 그 외 일반 input
cache_read 비율이 높을수록 비용 효율이 좋다는 신호다.
가격 직관
cache write : 일반 input보다 약간 비쌈 (1.25x 정도)
cache read : 일반 input보다 훨씬 쌈 (0.1x 수준)
처음 한 번은 약간 비싸지만, 두 번째 호출부터 본전 회수가 빠르다.
흔한 함정
- cache_control 위쪽 한 글자라도 다르면 cache miss
- system 안에 현재 시간 박으면 cache 안 잡힘
- breakpoint 다음에 또 dynamic 콘텐츠 → 두 번째 breakpoint 필요
- ephemeral 5분 TTL 안에 다음 호출이 안 들어오면 캐시 만료
OpenAI
어떻게 동작하나
자동이다. prefix가 같으면 자동으로 캐시 적용된다. 개발자가 cache_control 같은 마커를 박지 않아도 된다.
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": LONG_SYSTEM_RULES},
{"role": "user", "content": user_question},
],
)이 호출이 일정 길이 이상이면 자동으로 prefix가 캐시 후보가 된다.
캐시 단위
- 보통 1024 토큰 정도 이상부터 캐시 대상
- prefix 일치율로 자동 판단
대화 history, tools, system 모두 prefix가 같다면 캐시될 수 있다.
사용량 확인
응답 usage에 다음 필드가 들어온다.
prompt_tokens
completion_tokens
prompt_tokens_details:
cached_tokens ← 이번 호출에서 캐시에서 읽은 양
cached_tokens / prompt_tokens 비율이 캐시 적중률 지표다.
가격 직관
cached input : 일반 input의 절반 정도
write 비용 : 별도로 부과되지 않음 (자동이라 투명함)
자동인 만큼 가격 모델도 단순하다.
흔한 함정
- prompt가 짧으면 캐시 대상에서 빠짐
- system 앞쪽에 매번 다른 metadata가 들어가면 캐시 깨짐
- "Agent framework가 messages를 매번 재조립"하면서 순서가 미세하게 바뀌면 miss
- 멀티 모델 라우팅하면 모델별로 캐시가 분리되어 hit률이 떨어짐
Gemini (Google)
Gemini는 두 가지 모드를 모두 제공한다.
모드 1: Implicit Caching (자동)
OpenAI처럼 동일 prefix를 자동으로 캐시한다. 별도 설정 없이 동작한다.
from google import genai
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=[
{"role": "user", "parts": [{"text": LONG_SYSTEM_RULES + user_question}]},
],
)사용량에 cached_content_token_count가 잡힌다.
모드 2: Explicit Context Caching (수동 리소스)
이게 Gemini의 특이한 부분이다. 캐시를 별도 리소스로 만들어두고, 그 리소스를 참조해서 호출한다.
from google import genai
from google.genai import types
client = genai.Client()
# 1) 캐시 리소스 생성 (한 번)
cache = client.caches.create(
model="gemini-2.5-pro",
config=types.CreateCachedContentConfig(
system_instruction="너는 사내 문서 도우미다. 규칙: ...",
contents=[long_company_handbook], # 큰 PDF/문서를 통째로
ttl="3600s",
),
)
# 2) 캐시를 참조해서 호출 (여러 번)
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=[user_question],
config=types.GenerateContentConfig(cached_content=cache.name),
)어디에 좋은가
- 큰 문서/책/매뉴얼을 같이 보면서 여러 번 질문할 때
- 같은 시스템 instruction + 같은 ground truth로 다수 사용자에게 답할 때
- 동영상, 긴 코드베이스 같은 대용량 입력
사용량 확인 / 가격 직관
cached_content_token_count : 캐시에서 읽은 토큰 수
- explicit caching은 저장 비용도 같이 들어감 (시간당 GB 단가)
- 호출당 비용은 일반 input의 1/4 정도 (모델/사이즈에 따라)
저장 비용이 있기 때문에 TTL이 중요하다. 안 쓸 캐시는 빨리 삭제한다.
흔한 함정
- explicit cache는 모델별/리전별로 따로 만들어야 함
- 캐시 리소스 관리(생성/삭제)가 백엔드 책임
- TTL 만료 후엔 재생성 비용 발생
- implicit는 prefix 깨지면 OpenAI처럼 그냥 miss
한 장 비교표
| 항목 | Claude | OpenAI | Gemini (implicit) | Gemini (explicit) |
|---|---|---|---|---|
| 제어 방식 | 명시적 (cache_control) | 자동 | 자동 | 명시적 (리소스 생성) |
| 캐시 단위 | 텍스트 블록 (tool/system/messages) | prefix 일치 | prefix 일치 | 별도 캐시 리소스 |
| TTL | 5분 (ephemeral) / 1시간 (opt-in) | 짧음, 사용 빈도에 따라 자동 | 짧음 | 사용자가 지정 (초 단위) |
| 사용량 확인 | cache_read_input_tokens | cached_tokens | cached_content_token_count | 같음 |
| 가격 직관 | write 약간 비쌈, read 매우 쌈 | cached input은 약 절반 | 동일 | input의 약 1/4 + 저장 비용 |
| 대용량 입력 | 가능 (이미지/PDF 캐시) | 자동 | 자동 | 특히 강함 (책, 동영상) |
| 함정 | breakpoint 위쪽 한 글자 변화 | 매번 재조립되는 messages 순서 | 짧은 prompt는 caching 안 됨 | 리소스 관리, TTL |
실무 권장
세 가지 provider를 동시에 다 쓰는 회사라면 다음 원칙이 안전하다.
1. layout은 provider-agnostic하게 설계
system, tools, few-shot은 무조건 위로. 가변값(시간, request id, retrieved docs)은 무조건 아래로.
이 규칙은 어느 provider에 가져가도 깨지지 않는다.
2. Claude는 cache_control을 의식적으로 박는다
자동 캐시가 없으니 안 박으면 일이 안 일어난다.
# 안전한 위치 두 군데
[ tools ] → cache_control
[ system + few-shot ] → cache_control3. Gemini는 "큰 문서가 있는 RAG"라면 explicit caching 검토
수십~수백 페이지짜리 매뉴얼을 매번 prompt에 끼워 보내는 RAG라면 explicit caching이 압도적이다. TTL을 1일 정도로 잡고, 매뉴얼이 업데이트되면 캐시를 새로 만든다.
4. cache 효율은 항상 계측
Langfuse trace에서 매번 cached_tokens(또는 cache_read)를 본다.
한 trace에서 cached_input이 60% 이상 → 잘 동작
cached_input이 0%로 떨어지면 alert
알람으로 "캐시가 갑자기 0%로 떨어졌다"가 잡혀야 비용 폭발 사고를 막을 수 있다.
결론
Provider마다 prompt cache 정책이 다르기 때문에, Agent framework가 알아서 최적화해준다고 믿으면 안 된다. system prompt, tool definition, dynamic context, retrieved docs, conversation history를 어디에 배치할지 직접 설계해야 한다.
LangChain이든 LangGraph든 prompt 위치를 자동으로 최적화해주지 않는다. 이건 결국 백엔드 설계의 책임이고, 그 결과는 월별 청구서로 즉시 드러난다.
캐시를 다음 세 줄로 요약하면.
1. prefix를 길고 고정되게 만든다
2. 동적값을 prefix에 절대 섞지 않는다
3. provider별 캐시 사용량을 항상 계측한다
이 세 줄을 지키면 같은 LLM 앱이 같은 사용자에게 같은 답을 내면서, 비용은 절반이 된다.