← 목록으로

모델만 바꿨는데 LLM 비용이 3배가 된 이유

모델만 바꿨는데 LLM 비용이 3배가 된 이유

요약

안녕하세요, 당근 커뮤니티홈팀에서 백엔드를 개발하는 제이(Jay)예요. 당근 앱의 커뮤니티 탭에는 이웃이 알려 주는 동네 정보부터 소소한 일상과 취미까지 나누는 동네생활, 모임, 카페, 아파트가 모여 있어요. 커뮤니티홈팀은 이 서비스들의 글을 한데 모아 보여 주는 피드를 맡고 있어요.

피드를 맡으며 가장 고민하는 건 사용자에게 더 알맞은 글을 어떻게 보여 줄지예요. 그래서 기존 머신러닝 추천에 더해, LLM으로 글의 다양한 메타데이터를 뽑아 추천에 쓰고 있어요. 그중 하나가 LLM에 카테고리 목록과 글을 주고 어느 주제에 속하는지 고르게 하는 주제 분류예요. 추천은 보여 줄 글 후보를 모으는 단계와 그 순서를 정하는 단계로 나뉘는데, 글마다 주제가 붙어 있으면 첫 단계부터 사용자가 관심 있는 주제의 글을 골라 올 수 있어요.

주제 분류를 할 때 LLM에 보내는 요청을 열어 보면 대부분이 매번 똑같은 지시문과 카테고리 목록이고, 글 본문은 끝에 조금 붙어 있을 뿐이에요. 이 글에서는 요청마다 똑같은 이 부분을 고정부라고 부를게요.

2026년 8월, 이 작업의 모델을 Gemini에서 단가가 20% 낮은 OpenAI GPT-5.6으로 바꿨어요. 응답은 정상이었고 에러도 한 건 없었어요.

닷새 뒤 보니 단가는 내렸는데 요청당 비용은 3.2배가 돼 있었어요. 원인은 프롬프트 캐시였어요. 입력 중 캐시에서 읽어 온 비율인 캐시 히트율이 85.8%에서 2.3%로 떨어져 있었죠.

교체 전후의 응답을 나란히 놓으면 달라진 곳은 사용량 정보 안의 캐시 필드 하나뿐이었어요. 캐시가 깨져도 에러는 나지 않아요. 비용만 늘어요.

이 글은 그 일을 해결하며 알게 된 것을 정리한 기록이에요. 캐시가 무엇을 재사용하는지, 비용은 어떻게 매겨지는지, 언제 켜지고 어디까지 저장되는지, 캐시에서 읽었는지는 어떻게 확인하는지를 OpenAI, Anthropic, Google Gemini를 나란히 놓고 살펴봐요. 같은 지시문에 입력만 바꿔 LLM을 대량으로 부르거나, 챗봇이나 에이전트처럼 앞부분이 같은 요청을 되풀이해 보내는 분이라면 참고가 될 거예요.

프롬프트 캐시의 원리

신입 검수자에게 매번 규정집 20쪽과 오늘 볼 글 한 편을 함께 준다고 해 볼게요. 규정집은 늘 같고 글만 바뀌는데, 이 검수자는 기억을 못 해서 글마다 규정집을 처음부터 다시 읽어요. 하루 10만 편이면 규정집을 10만 번 읽는 셈이에요.

LLM도 똑같이 요청마다 프롬프트 전체를 처음부터 다시 읽어요. 이 낭비를 줄이는 게 프롬프트 캐시예요.

앞 토큰을 돌아보는 어텐션

LLM은 글을 토큰 단위로 하나씩 이어 써요. 다음 토큰을 고르기 전에는 앞 토큰들을 돌아보며 무엇을 참고할지 따지는데, 이 계산을 어텐션(attention)이라고 해요.

“어제 시장에서 산 사과를 깎아서” 다음에 올 토큰을 고른다고 해 볼게요. 사람은 바로 “먹었다”를 떠올려요. “사과”와 “깎아서”가 가장 관련 있고 “어제”나 “시장에서”는 덜 중요하다는 걸 알기 때문이에요. 모델은 이 판단을 마지막 토큰 “깎아서” 자리에서 숫자로 해요.

어텐션에는 토큰마다 세 가지 값이 쓰여요. 도서관에서 자료를 찾는 일에 비유해 볼게요.

  • Q(Query, 질문): 마지막 토큰이 다음 토큰을 고르려고 들고 있는 질문이에요. “무엇을 했는지 이어 쓰려면 어떤 정보가 필요하지?” 같은 것이에요.
  • K(Key, 분류표): 앞 토큰마다 붙은 꼬리표예요. 책의 분류표처럼 “먹을 수 있는 물건”, “때를 나타내는 말” 같은 정보를 담아요.
  • V(Value, 내용): 앞 토큰이 실제로 건네주는 정보예요. 책의 본문에 해당해요.

마지막 토큰 “깎아서”는 자기 Q를 앞 토큰들(자기 자신 포함)의 K와 하나씩 맞춰 보고 점수를 매겨요. “사과”와 “깎아서”는 높은 점수를, “어제”는 낮은 점수를 받아요. 그다음 점수 비율대로 V를 섞고 그 결과로 다음 토큰 “먹었다”를 골라요. 분류표(K)로 관련 있는 책을 고른 뒤, 그 책들의 본문(V)을 점수만큼 읽는 것과 같아요.

실제 Q, K, V는 사람이 읽을 수 있는 문장이 아니라 숫자 묶음(벡터)이고, 위 설명은 그 역할을 말로 푼 거예요.

한 번 만들면 계속 쓰는 K와 V

“먹었다”를 고른 뒤에는 “먹었다”가 Q를 들고 같은 일을 해요. 이때 “어제”부터 “깎아서”까지의 K와 V는 앞 단계에서 만든 것과 똑같아요. 뒤에 어떤 토큰이 붙어도 앞 토큰의 K와 V는 바뀌지 않거든요.

그래서 모델은 한 번 만든 K와 V를 캐시에 쌓아 두고 다시 꺼내 써요. 이 캐시를 KV 캐시라고 해요. 새로 계산하는 건 새 토큰의 Q, K, V뿐이고, Q는 그 토큰이 앞을 볼 때 한 번 쓰고 끝나서 쌓아 두지 않아요. 캐시가 없다면 토큰을 하나 쓸 때마다 앞 토큰 전부를 처음부터 다시 계산해야 해요.

입력도 마찬가지예요. 모델은 답을 쓰기 전에 입력 전체를 한꺼번에 읽으며 K와 V를 만들어 캐시를 채워요. 이 단계를 prefill이라고 하는데, 입력이 길수록 오래 걸려요.

KV 캐시를 다음 요청에 물려주는 프롬프트 캐시

KV 캐시는 원래 요청 하나가 끝나면 함께 사라져요. 프롬프트 캐시는 그 앞부분을 요청이 끝난 뒤에도 서버에 남겨 두는 기능이에요. 다음 요청의 앞부분이 같으면 prefill을 건너뛰고 남겨 둔 것을 그대로 꺼내 써요. 캐시에서 읽은 앞부분은 Q, K, V 어느 것도 다시 계산하지 않고 새로 들어온 토큰만 계산해요. 규정집을 읽으며 만든 메모를 다음 글에도 쓰는 셈이에요. 제공사는 아낀 계산만큼 값을 깎아 주는데, OpenAI는 캐시에서 읽은 입력 토큰을 최대 95%까지 할인해요.

헷갈리기 쉬운 것

  • 프롬프트 캐시는 응답 캐시가 아니에요. 응답 캐시는 같은 질문에 저장해 둔 답을 그대로 돌려줘요. 뜻이 비슷한 질문에도 답을 돌려주는 시맨틱 캐싱도 응답 캐시의 한 종류예요. 프롬프트 캐시는 입력을 읽는 계산만 재사용하고 출력은 매번 새로 만들어요.
  • 캐시를 켜도 답의 품질은 그대로예요. 답을 만드는 과정은 캐시가 없을 때와 똑같다고 OpenAI와 Anthropic 문서 모두 적어 두었어요.
  • 캐시는 입력 비용만 깎아요. 출력 토큰은 캐시와 상관없이 정가예요.
  • 응답도 빨리 시작돼요. 입력을 처리하는 시간이 줄어서예요. 다만 공식 문서에 일반적인 수치는 없어요.

앞부분이 글자 하나까지 같아야 하는 이유

모델 구조 때문이에요. 모델은 여러 층으로 되어 있고, 층마다 가중치가 달라서 같은 어텐션이라도 층마다 다른 질문을 던져요. 아래층의 결과는 위층의 입력이 돼요. 예를 들어 아래층에서 “깎아서”가 “사과를”과 묶이면, 위층의 “깎아서”는 이미 “사과를 깎아서”라는 뜻을 담고 다시 앞을 봐요. 이렇게 층을 지날수록 앞 문맥이 쌓이니, 위층의 K와 V는 그 토큰 하나가 아니라 앞 토큰 전체에 묶여 있어요. 앞에서 토큰 하나만 달라져도 그 뒤의 K와 V는 모두 달라져요.

위치도 따져요. K에는 “몇 번째 토큰인가”라는 정보가 섞여 있어서 같은 문장도 100번째와 200번째에서 K가 달라요.

그래서 프롬프트 캐시가 다시 쓸 수 있는 건 처음부터 똑같은 부분뿐이에요. 중간에 한 번이라도 다른 토큰이 나오면, 그 뒤가 아무리 똑같아도 다시 계산해요.

예를 들어 프롬프트 맨 앞의 시각 2026-09-04 09:00이 1분 뒤 09:01로 바뀌면, 토큰 하나 차이인데도 그 뒤 지시문 전체를 다시 계산해요. 시각을 맨 뒤로 옮기면 끝의 몇 토큰만 새로 계산해요.

앞부분을 흔드는 것들

OpenAI 문서에 따르면 캐시되는 앞부분에는 OpenAI가 자체로 넣는 지시문, 개발자 메시지, 도구 정의, 대화 이력까지 들어가요. 그래서 프롬프트 문구를 고치지 않아도, 요청마다 끼어드는 값이나 함께 보내는 설정이 바뀌면 앞부분이 달라져요.

  • 앞쪽에 넣은 시각과 날짜. OpenAI 문서도 캐시가 깨지는 예로 지시문 속 타임스탬프를 들어요.
  • 요청 ID, UUID, 난수. 추적용으로 프롬프트에 넣었다면 맨 뒤로 옮기세요.
  • 키 순서가 흔들리는 JSON. 같은 데이터라도 직렬화할 때마다 키 순서가 바뀌면 다른 글자가 돼요. Anthropic 문서는 도구의 input_schema JSON이 매번 다르게 직렬화되는 경우를 도구 변경 원인으로 들어요.
  • 도구 목록과 그 순서. 도구를 더하거나 빼거나 순서만 바꿔도 앞부분이 달라져요. OpenAI는 어떤 요청에서 도구를 쓰지 않게 하려면 정의를 지우지 말고 tool_choice를 "none"으로 두라고 권해요.
  • 출력 형식과 스키마. 구조화 출력 스키마는 입력에 함께 들어가요.
  • 추론 강도 같은 설정. OpenAI는 reasoning.effort와 text.verbosity를 앞부분을 바꿀 수 있는 설정으로, Anthropic은 thinking과 effort 설정 변경을 캐시가 깨지는 원인으로 꼽아요.

캐시를 켜면 오히려 비싸지는 경우

캐시에서 읽으면 싸지만, 캐시에 저장할 때 추가 비용을 받는 제공사도 있어요. 이걸 모르면 캐시는 켜 두기만 하면 무조건 이득이라고 착각하기 쉬워요.

읽기는 대부분 입력 정가의 0.1배예요. OpenAI GPT-6.1 Sol, Anthropic Claude Opus 5.5와 Sonnet 5.5는 0.05배, Claude Fable 5.1은 0.025배로 더 싸요. Gemini는 캐시에서 읽은 토큰을 90% 할인해요.

쓰기 요금은 제공사마다 달라요. 캐시에 저장할 때 발생하는 추가 비용을 이 글에서는 쓰기 할증이라고 할게요.

  • OpenAI: GPT-5.6 이후 모델은 정가의 1.25배예요. GPT-5.5 이하는 할증이 없어요.
  • Anthropic: 기본 5분짜리 캐시는 1.25배, 1시간짜리 캐시는 2배예요.
  • Gemini: 자동으로 거는 implicit 캐시는 할증이 없어요. 미리 올려 두는 explicit 캐시는 저장해 둔 시간만큼 저장료를 따로 내요.

쓰기 할증이 있으면 캐시를 켠 쪽이 더 비쌀 수도 있어요. 고정부 토큰 하나는 캐시에서 읽을 때 0.1배, 못 읽고 새로 저장할 때 1.25배를 내요. 고정부 토큰 가운데 캐시에서 읽는 비율을 h라고 하면 평균 배율은 아래와 같아요.

0.1 × h + 1.25 × (1 − h)

h가 약 22%보다 낮으면 이 값이 1을 넘어, 캐시가 없을 때보다 오히려 비싸져요. 프롬프트 길이와는 상관없고, 읽기가 0.05배인 모델도 약 21%로 거의 같아요. OpenAI 문서도 요청 열 번 중 처음 한 번만 저장하고 아홉 번을 읽으면 2.15배로, 캐시 없이 열 번 처리하는 10배보다 훨씬 싸다고 설명해요.

캐시 상태별 비용 비교

고정부가 요청의 95%인 작업에서, 쓰기 할증이 1.25배인 모델의 입력 비용을 캐시가 없을 때를 100으로 두고 계산했어요.

같은 요청인데도 캐시 상태에 따라 비용이 여섯 배 넘게 벌어져요. 비용을 견적할 때 단가표와 함께 예상 히트율이 필요한 이유예요. 쓰기 할증이 없는 모델이라면 못 읽어도 캐시 없음과 같은 100이에요.

캐시를 켜는 법과 구분점 위치

캐시는 구분점(breakpoint) 단위로 저장돼요. 구분점은 “처음부터 여기까지를 한 덩어리로 저장해 두라”는 표시이고, 다음 요청은 앞부분이 그 덩어리 전체와 같아야 읽어요. 캐시를 켜는 방법과 구분점을 누가 어디에 찍는지가 제공사마다 달라요.

OpenAI: 모델마다 다른 자동 구분점 위치

OpenAI는 캐시가 기본으로 켜져 있고 구분점도 자동으로 찍어요. 그런데 GPT-5.6에서 자동 구분점의 위치가 바뀌었어요.

  • GPT-5.5: 2,048토큰 간격으로 찍어요.
  • GPT-5.6 이후: 가장 최근 메시지의 끝에 하나 찍어요. GPT-6 계열도 같아요.

저희 비용이 3배가 된 원인

저희 요청은 고정된 지시문을 앞에, 글 본문을 뒤에 두고 메시지 하나로 보냈어요. Gemini를 쓸 때는 대부분의 요청이 같은 지시문 부분을 캐시에서 읽었어요. GPT-5.5처럼 2,048토큰 간격으로 구분점을 찍는 모델이었어도 긴 지시문 안에 구분점이 여러 개 생겨, 글이 달라도 지시문 쪽 덩어리는 다시 읽을 수 있었을 거예요. 하지만 GPT-5.6에서는 구분점이 글 본문 뒤에 하나만 찍혀서, 저장되는 덩어리는 “지시문 + 글 A” 전체예요. 다음 요청 “지시문 + 글 B”는 지시문까지는 같지만 글에서 갈라지니, 이 덩어리 전체와 같지 않아 읽지 못해요. “지시문까지”만 담은 덩어리는 한 번도 저장되지 않았고요. 거의 모든 요청이 1.25배를 내고 저장만 했어요. 앞의 비용 그림에서 본 125가 바로 이 상태예요.

OpenAI 문서도 이 함정을 주의할 점(Gotchas)의 첫 항목으로 적어 두었어요.

A shared prefix is not always a cached prefix. This is particularly prevalent when migrating from earlier models to GPT-5.6 or later due to the change in implicit caching behaviour.
(앞부분이 같다고 해서 그 앞부분이 캐시돼 있는 건 아니에요. 자동 캐시 동작이 바뀌어서 이전 모델에서 GPT-5.6 이후로 옮길 때 특히 자주 생겨요.)

고정부 끝에 찍는 명시 구분점

해결 방법도 문서에 있어요. GPT-5.6 이후로 옮길 때의 안내는 “자동 구분점에 요청마다 바뀌는 부분이 들어가면, 고정부 뒤에 명시 구분점을 더하라”고 써요. 고정부가 끝나고 바뀌는 부분이 시작되기 직전이 그 자리예요. OpenAI API로 직접 보낸다면 아래처럼 써요.

{
"model": "gpt-6.1-sol",
"prompt_cache_options": { "mode": "explicit" },
"input": [
{
"role": "developer",
"content": [
{
"type": "input_text",
"text": "(매번 같은 지시문, 규칙, 예시)",
"prompt_cache_breakpoint": { "mode": "explicit" }
}
]
},
{ "role": "user", "content": "(요청마다 바뀌는 입력)" }
]
}

prompt_cache_breakpoint가 붙은 블록까지가 한 덩어리로 저장되므로 입력이 바뀌어도 지시문 덩어리는 다시 읽혀요. prompt_cache_options.mode를 explicit으로 두면 자동 구분점이 꺼지고 직접 찍은 곳에서만 저장해서, 매번 바뀌는 입력에는 쓰기 할증이 붙지 않아요. 자동 구분점과 함께 쓸 수도 있고, 요청 하나가 만드는 캐시 쓰기는 최대 4개예요.

저희는 프롬프트를 사내 LLM 플랫폼의 템플릿으로 관리하고 코드는 변수 값만 넘겨요. 그래서 플랫폼이 템플릿의 첫 변수 앞에 구분점을 찍도록 고쳤어요. 고친 뒤 주제 분류의 히트율은 95% 안팎으로 올랐고, 요청당 비용은 약 5분의 1로 줄었어요.

Anthropic: 직접 켜야 하는 캐시

Anthropic은 캐시가 기본으로 켜져 있지 않아서 요청에 cache_control을 붙여야 해요. 붙이는 자리는 두 가지예요.

  • 요청 맨 바깥: model, messages와 같은 최상위 단계에 하나만 붙이면 마지막 블록 끝에 구분점이 찍혀요. 대화가 이어지면 구분점도 새 메시지 끝으로 옮겨 가고, 직전 요청까지 저장된 부분은 그대로 읽혀요. 이전 대화에 새 메시지만 덧붙이는 챗봇에 잘 맞아요. 반대로 지시문 뒤에 매번 다른 글을 붙이는 작업에서는 구분점이 바뀌는 글 끝에 찍혀 캐시를 읽지 못한다고 문서가 경고해요. GPT-5.6에서 격은 것과 같은 함정이에요.
  • 블록에 직접: 아래 예시처럼 고정부가 끝나는 블록 안에 붙여요. 4개까지 찍을 수 있어요.

앞부분은 도구 정의(tools), 시스템 프롬프트(system), 메시지(messages) 순서로 만들어지므로 system 끝에 붙이면 도구 정의와 지시문이 함께 저장돼요.

{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"system": [
{
"type": "text",
"text": "(매번 같은 지시문, 규칙, 예시)",
"cache_control": { "type": "ephemeral" }
}
],
"messages": [
{ "role": "user", "content": "(요청마다 바뀌는 입력)" }
]
}

Gemini: 자동인 implicit과 미리 올려 두는 explicit

Gemini의 implicit 캐시는 기본으로 켜져 있어서 따로 할 일이 없어요. 다만 문서는 “캐시에서 읽었을 때(when cache hits occur)” 할인된다고만 써서 읽는다는 보장은 없어요. 구분점을 찍는 방법도 없어서 Google 문서는 대신 이렇게 권해요.

Place large and common contents at the beginning of your prompt. Send requests with a similar prefix in a short amount of time.
(크고 공통된 내용을 프롬프트 앞에 두세요. 앞부분이 비슷한 요청을 짧은 시간 안에 보내세요.)

explicit 캐시는 고정부를 미리 서버에 올려 두고 요청에서는 그 이름만 부르는 방식이에요. OpenAI의 explicit 모드와는 다른 기능이에요. 올려 둔 내용 전체가 구분점 하나와 같은 역할을 해요. 문서는 이 방식이 할인을 보장한다(ensures)고 쓰지만, 저장해 둔 시간만큼 저장료를 따로 내요. Gemini 4부터는 explicit 캐시를 쓸 수 없어요. Gemini API 문서에 그렇게 적혀 있고, Google Cloud 문서의 지원 모델 목록에도 Gemini 4는 없어요.

# 1) 고정부를 미리 올리기 (응답의 name이 캐시 이름)
POST https://LOCATION-aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/cachedContents
{
"model": "projects/PROJECT_ID/locations/LOCATION/publishers/google/models/gemini-3.5-flash",
"contents": [{ "role": "user", "parts": [{ "text": "(매번 같은 지시문, 규칙, 예시)" }] }],
"ttl": "3600s"
}

# 2) 요청에서는 캐시 이름만 부르고 바뀌는 부분만 보내기
POST https://LOCATION-aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/publishers/google/models/gemini-3.5-flash:generateContent
{
"cachedContent": "projects/PROJECT_NUMBER/locations/LOCATION/cachedContents/CACHE_ID",
"contents": [{ "role": "user", "parts": [{ "text": "(요청마다 바뀌는 입력)" }] }]
}

최소 길이와 유지 시간

캐시를 켜고 구분점을 맞게 찍어도, 고정부가 충분히 길고 같은 앞부분이 캐시가 사라지기 전에 다시 와야 캐시에서 읽어요.

최소 길이

  • OpenAI: GPT-5.6 이후는 1,024토큰이에요. 문서는 모델을 바꿀 때마다 고정부 길이를 새 모델 기준으로 다시 재 보라고 권해요.
  • Anthropic: 같은 세대라도 모델마다 달라요. 2026년 10월 문서 기준으로 512토큰(Opus 5.5, Opus 5, Sonnet 5.5, Fable 5.1, Haiku 5.5), 1,024토큰(Opus 4.8, Sonnet 5), 2,048토큰(Opus 4.7), 4,096토큰(Opus 4.6, Opus 4.5, Haiku 4.5)이에요. 3,000토큰짜리 프롬프트는 Opus 5에서는 캐시되고 Opus 4.6에서는 캐시되지 않아요. 최소 길이보다 짧으면 cache_control을 붙여도 캐시하지 않고 에러도 돌려주지 않아요.
  • Gemini: Gemini 2 계열은 2,048, Gemini 3과 4 계열은 4,096토큰이에요. 일부 모델은 문서마다 값이 달라요. Google Cloud 문서는 Gemini 3.7 Flash, 3.8 Flash 등의 implicit 최소를 6,144로, Gemini API 문서는 4,096으로 적어요. 쓰는 경로의 문서로 확인하세요.

유지 시간

  • OpenAI: GPT-5.6 이후는 마지막으로 쓰거나 읽은 뒤 최소 30분 유지돼요. prompt_cache_options.ttl로 정하는데, 지금 고를 수 있는 값은 기본값인 "30m" 하나예요.
  • Anthropic: 기본 5분이고 읽을 때마다 연장돼요. "cache_control": { "type": "ephemeral", "ttl": "1h" }로 1시간짜리를 쓸 수 있지만 쓰기가 2배라서, 요청 간격이 5분보다 길고 1시간보다 짧을 때 따져 볼 만해요.
  • Gemini: implicit은 Google Cloud 문서 기준으로 길어야 24시간이고, 대부분은 그 전에 사라진다고 해요. explicit은 기본 60분이고 늘릴 수 있지만 늘린 만큼 저장료도 늘어요.

같은 앞부분이 유지 시간보다 드물게 오면 캐시는 저장만 되고 읽히지 않아요. 쓰기 할증이 있는 모델이라면 저장할 때마다 할증만 내서, 캐시를 쓰지 않을 때보다 비싸질 수 있어요.

캐시에서 읽었는지 확인하기

응답의 캐시 필드

앞부분은 같고 끝의 입력만 다른 요청 두 개를 연달아 보내고, 두 번째 응답에서 캐시로 읽은 토큰이 0보다 큰지부터 보세요. 똑같은 요청을 두 번 보내면 구분점이 잘못 찍혀 있어도 캐시에서 읽은 것처럼 보여요. 필드 이름은 제공사마다 달라요. 숫자는 예시예요.

// OpenAI (Responses API)
"usage": {
"input_tokens": 5000, // 전체 입력
"input_tokens_details": {
"cached_tokens": 4800, // 캐시에서 읽음
"cache_write_tokens": 0 // 캐시에 새로 저장함 (쓰기 할증)
}
}

// Anthropic
"usage": {
"input_tokens": 200, // 캐시를 거치지 않은 나머지
"cache_read_input_tokens": 4800, // 캐시에서 읽음
"cache_creation_input_tokens": 0 // 캐시에 새로 저장함 (쓰기 할증)
}
// Anthropic은 세 값을 더해야 전체 입력이에요

// Gemini
"usageMetadata": {
"promptTokenCount": 5000, // 전체 입력
"cachedContentTokenCount": 4800 // 캐시에서 읽음
}

구분점을 직접 찍었는데도 읽기 필드가 계속 0이라면, 게이트웨이나 SDK 래퍼가 그 구분점을 제공사까지 그대로 전달하는지 확인하세요. 저희도 요청을 조립하는 중간 계층이 일부 요청에서 cache_control을 빠뜨려, 그 요청들은 캐시를 전혀 읽지 못한 적이 있어요.

히트율의 두 가지 기준

요청 기준 히트율은 캐시를 한 토큰이라도 읽은 요청의 비율이고, 토큰 기준 히트율은 캐시에서 읽은 토큰을 전체 입력 토큰으로 나눈 값이에요. 앞부분 6,000토큰 중 1,000토큰만 읽어도 요청 기준으로는 히트로 세서, 요청 기준이 대개 더 후하게 나와요. OpenAI 문서는 토큰 기준 히트율을 사용자나 날짜 단위로 묶어 보라고 권해요. 이 글에 나온 히트율도 모두 토큰 기준이에요.

제공사 진단 기능

OpenAI와 Anthropic은 이전 응답과 이번 요청을 비교해 캐시가 어디서 갈렸는지 알려 주는 진단 기능도 제공해요. 요청에 OpenAI는 prompt_cache_options.comparison_response_id를, Anthropic은 diagnostics를 넣으면 응답에 tools_changed(도구 정의가 바뀜) 같은 미스 사유가 와요. 쓰는 법은 OpenAI 문서와 Anthropic 문서에 있어요. Gemini에는 아직 이런 기능이 없어요.

캐시에서 못 읽을 때 보는 순서

캐시가 의심되면 위에서부터 차례로 확인하고 해당하는 단계에서 오른쪽에 적은 대로 고쳐요.

마치며

모델 하나를 바꿨을 뿐인데 에러 한 줄 없이 비용이 세 배가 됐어요. 원인은 모델이 바뀌면서 달라진 캐시 규칙이었고, 그 규칙은 문서에 적혀 있었어요.

앞부분이 매번 같은지, 그 길이가 내 모델의 최소 길이를 넘는지, 유지 시간 안에 같은 앞부분이 다시 오는지 물어보세요. 셋 다 “예”라면 캐시가 비용을 크게 바꿔요. 그때는 고정부를 앞에, 바뀌는 부분을 뒤에 두고 구분점이 그 경계에 찍히는지 확인하세요. 시각이나 요청 ID, JSON 키 순서, 도구 목록 순서처럼 앞부분을 몰래 흔드는 것들도 치우세요.

돌아보면 Gemini에서 히트율이 높았던 건 저희가 요청을 잘 짜서가 아니었어요. 끝이 달라도 같은 앞부분까지는 알아서 읽어 주던 제공사 동작 덕분이었는데, 저희는 거기에 기대고 있다는 걸 몰랐어요. 잘 돌아가던 이유를 모르면 그 이유가 사라져도 알아채기 어려워요.

그래서 모델 교체는 코드 한 줄이 아니라 운영 조건을 바꾸는 일로 다뤄야 해요. 응답이 맞게 나오는지에 더해 요청당 비용, 응답 시간, 캐시 히트율을 교체 전과 나란히 비교하세요. 이 비교는 몇 건으로는 할 수 없어요. 캐시는 앞선 요청이 데워 둬야 읽히고 요청이 얼마나 자주 오느냐에 따라 달라져서, 몇 건만 보내 보면 실제와 다른 숫자가 나와요. 운영 트래픽에서 하루 이상 잴 값으로 보고, 배포한 뒤에도 이 지표를 계속 지켜보세요. 저희도 배포 다음 날 히트율만 봤다면 닷새가 아니라 하루 만에 알아챘을 거예요.

이 경험은 한 모델의 캐시 정책에서 끝나지 않아요. LLM을 프로덕션에서 쓰면 모델이나 제공사 동작, 프롬프트 구조가 바뀔 때 에러 없이 비용과 품질이 달라지고, 그 변화는 운영 지표에서만 드러나요. 그래서 지켜야 할 규칙은 사람보다 시스템에 두고, 나머지는 지표로 지켜봐야 해요. 저희도 구분점은 플랫폼이 찍게 하고, 사람은 “바뀌는 부분은 템플릿 끝에 둔다” 하나만 지키게 했어요.

마지막으로 세 제공사의 차이를 한 장에 모았어요.

참고한 자료

이 글에 쓴 제공사 동작은 2026년 10월 공식 문서 기준이에요. 캐시 규칙은 모델 버전마다 바뀌니, 적용하기 전에 다시 확인해 보세요.

당근 커뮤니티실은 누구나 자신의 동네에서 소속감을 느끼며 살아갈 수 있도록, 이웃을 잇는 지역 커뮤니티를 만들고 있어요. 이 글의 주제 분류처럼 AI도 제품 곳곳에 직접 붙여 가고 있고요. 서버부터 화면까지, 제품이 사용자에게 닿는 끝까지 함께 고민할 백엔드 엔지니어와 프론트엔드 엔지니어를 찾고 있어요. 관심이 있다면 공고를 확인해 주세요.

모델만 바꿨는데 LLM 비용이 3배가 된 이유 was originally published in 당근 기술 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.

← 목록으로