본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 05. 28. 22:20

매달 나가는 AI API 비용, 줄일 수 있는 부분이 아직 남아있지 않나요? — Prompt Caching으로 월 비용을 1/3~1/10로

요약

Prompt Caching을 활용하여 LLM API 비용을 획기적으로 절감하는 방법을 다룹니다. KV 캐시의 원리와 접두사 일치(Prefix Match)의 중요성을 설명하며, Anthropic, OpenAI, Gemini의 캐싱 방식 차이를 비교 분석합니다.

핵심 포인트

  • Prompt Caching은 재계산된 KV 캐시를 재사용하여 비용과 속도를 개선함
  • 접두사 일치(Prefix Match)가 필수이며, 앞부분이 다르면 캐시가 무효화됨
  • 효율적인 비용 절감을 위해 프롬프트 앞부분에 고정된 내용을 배치해야 함
  • Anthropic, OpenAI, Gemini는 캐싱 활성화 및 작동 방식이 서로 다름

매달 생성 AI (Generative AI) API 청구서를 볼 때마다 "어라, 생각보다 비싼데..."라고 느끼는 분들이 꽤 많으실 것이라 생각합니다.

Claude Code를 업무에 적극적으로 활용하게 되었고, 사내 지식을 RAG (Retrieval-Augmented Generation)로 읽히는 Bot을 구축했으며, AI 에이전트를 직접 만들었습니다. 서비스는 순조롭게 돌아가고 있습니다. 다만, 비용만 예상의 두 배로 불어나고 있습니다.

솔직히 말씀드리겠습니다. 이것은 대부분의 경우 Prompt Caching을 사용하지 않고 있기 때문입니다.

이 기사에서는,

  • 애초에 "캐시"란 무엇이 캐시되는 것인가
  • Anthropic / OpenAI / Gemini의 사양이 어떻게 다른가
  • 어떻게 작성해야 캐시 히트 (Cache Hit)가 발생하는가, 어떻게 작성하면 반대로 비용이 높아지는가
  • 히트율을 운영 측면에서 어떻게 측정하는가
  • 설계 시 하지 말아야 할 것은 무엇인가

까지 한꺼번에 정리합니다. 다 읽으실 때쯤에는, 내일부터 프롬프트의 순서를 바꾸는 것만으로도 월간 API 비용을 절반 이하로 줄일 수 있는 구체적인 이미지를 갖게 될 것이니 잠시만 시간을 내어 읽어주시기 바랍니다.

먼저, 이 부분을 확실히 이해하지 못하면 cache_control 코드를 보더라도 의미를 제대로 파악할 수 없습니다.

LLM은 내부적으로 "매일 아침, 어제의 회의록을 첫 페이지부터 다시 소리 내어 읽은 뒤에 오늘의 대화에 들어가는 신입 사원" 같은 면이 있습니다.

구체적으로는 다음과 같습니다. LLM은 토큰 열을 하나씩 처리하면서, **KV 캐시 (KV Cache, Key-Value 캐시. Attention의 중간 상태를 유지하는 메모리)**를 채워 나갑니다. 100토큰의 system 프롬프트를 넣으면, 그 100토큰 분량의 중간 상태를 계산하여 유지하고, 다음 user 메시지로 이어가며 answer를 생성합니다.

여기서 잔혹한 점은, 이 중간 상태는 한 번의 요청이 끝나면 버려진다는 것입니다. 다음에 동일한 system 프롬프트를 보내면, 다시 똑같은 100토큰을 처음부터 계산합니다. 이것이 "서두를 매번 재계산하는" 것의 정체입니다.

Prompt Caching은 간단히 말해 **"지난번에 계산한 KV 캐시를 재사용하게 해달라"**는 기능입니다. GPU 측에 이미 정답인 중간 상태가 남아 있다면, 재계산하지 않고 재사용합니다. 그래서 저렴하고 빨라집니다.

여기서 중요한 것은, 접두사 일치 (Prefix Match)가 절대 조건이라는 점입니다.

토큰 열의 앞부분부터 단 한 글자(정확히는 1토큰)라도 다르면, 그 시점에서 캐시는 전부 무효화됩니다. 중간 부분만 같더라도 앞부분이 다르면 뒷부분도 전부 별개의 것으로 취급됩니다. Attention의 구조상, 앞부분이 바뀌면 모든 중간 상태가 바뀌기 때문에 이는 어쩔 수 없는 부분입니다.

즉, Prompt Caching으로 비용을 낮추고 싶다면, "앞부분부터 가능한 한 길게, 동일한 문자열을 유지하는" 프롬프트 설계가 필요하다는 뜻입니다.

똑같은 "캐시"라고 해도, Anthropic / OpenAI / Gemini는 사고방식이 완전히 다릅니다. 표 하나로 비교해 보겠습니다.

관점Anthropic (Claude)OpenAI (GPT)Google (Gemini)
활성화 방법cache_control을 명시적으로 배치완전 자동 (코드 변경 없음)암묵적 (2.5+ 자동) + 명시적 (cachedContents API)
최소 토큰Sonnet 계열 1024 / Haiku 계열 20481024 토큰프리픽스 (Prefix) 공유가 성립하는 양
증분 매칭breakpoint 단위128 토큰 단위암묵적 방식은 프리픽스 (Prefix) 공유만 가능
할인율 (읽기)약 90% 할인 (기본 요금의 10%)50% 할인 (모델에 따라 최대 90%)최대 75~90% 할인
쓰기 (최초) 비용+25% (5분 TTL) / +100% (1시간 TTL)추가 비용 없음암묵적: 무료 / 명시적: 스토리지 별도 산정
TTL5분 / 1시간 중 선택5~10분 (유휴 시간)명시적은 초 단위 지정 가능, 암묵적은 자동
모니터링 필드cache_creation_input_tokens / cache_read_input_tokensprompt_tokens_details.cached_tokenscachedContentTokenCount
breakpoint 상한최대 4개까지없음 (자동)1 요청당 1개 캐시 (명시적)
공유 범위동일 API 키 내에서만동일 조직 내동일 프로젝트 내

여기서 알 수 있는 점은,

  • OpenAI는 알아서 자동으로 처리해주므로, 이미 운영 중인 앱은 usage에서 cached_tokens를 확인하는 것만으로도 효과를 검증할 수 있습니다.
  • Anthropic은 지시 방식이어서 breakpoint를 배치하는 기술이 필요하지만, 그만큼 절감 폭이 가장 큽니다.
  • Gemini는 2.5 이후 암묵적 캐싱이 기본 탑재되어 있으며, 명시적 캐싱은 긴 문서(동영상, PDF, 코드 전체)를 재사용할 때 적합합니다.

자신의 앱에서 한 달에 얼마나 사용하는지, 그리고 프롬프트의 앞부분(Prefix)이 얼마나 무거운지에 따라 기준이 되는 프로바이더가 달라집니다.

이 부분이 바로 이 글에서 가장 전달하고 싶은 핵심입니다.

Prompt Caching은 '기능을 활성화하는 것'보다 '프롬프트의 순서를 설계하는 것'이 본질입니다. 순서가 잘못되었다면 SDK 옵션을 아무리 추가해도 캐시 히트 (Cache Hit)가 발생하지 않습니다.

이것이 대원칙입니다. 맨 앞에는 절대 변하지 않는 정보를 두고, 뒤로 갈수록 동적인 것을 배치해야 합니다.

[앞부분]
└─ 불변: 역할 정의 (Role Definition), 출력 포맷 지시, 도구 정의 (Tool Definition), 사내 용어 사전
└─ 반고정: 대화 이력 (최근 N건), RAG로 가져온 청크 (Chunk)
...

이 순서만 지켜도 히트율은 단번에 올라갑니다.

에이전트 개발 시 놓치기 쉬운 점은, 도구 정의 (Function Calling / Tool Use의 스키마)가 사실상 가장 큰 캐시 대상이라는 것입니다.

도구가 10개인 경우, 그 JSON Schema를 합치면 보통 5,000~10,000 토큰에 달합니다. 이를 매번 다시 계산하는 것은 매우 아까운 일입니다. 반드시 맨 앞에 고정하여 변하지 않도록 해야 합니다.

멀티 턴 (Multi-turn) 대화에서 과거 메시지를 요약으로 대체하거나, 삭제하거나, 편집하면, 그 시점에서 프리픽스 (Prefix)가 바뀌어 캐시가 완전히 실패합니다.

이력은 **'뒤에 추가하기만 하는 것'**이 철칙입니다. 압축하고 싶다면 별도의 경로를 통해 요약본을 시스템 프롬프트 (System Prompt)로 이동시키는 등 설계를 분리해서 생각해야 합니다.

이 부분은 의외로 자주 실수하는 대목입니다. **'세션 정보는 system에 모아두자'**라고 생각해서 session_idrequest_timestamp를 시스템 프롬프트 맨 앞에 넣는 순간, 해당 사용자나 해당 요청에만 국한된 프리픽스 (Prefix)가 되어 다른 요청과는 절대 히트되지 않는 상태가 완성됩니다.

사용자 고유 정보나 시간 정보는 user 메시지 내부 혹은 시스템 프롬프트의 맨 마지막 동적 블록으로 밀어내십시오.

이것도 함정입니다.

당신은 ○○ 어시스턴트입니다. 오늘은 2026년 5월 28일입니다. ← 잘못된 방식

이렇게 하면, 날짜가 바뀌는 순간 과거의 모든 요청과의 캐시(Cache)가 완전히 끊깁니다. 날짜가 필요하다면, user 메시지 측이나 system의 마지막 동적 블록 측으로 옮기십시오. 이것만으로도 다음 날에도 캐시가 적용됩니다.

패턴Before (캐시 소멸)After (캐시 유지)
날짜system 상단에 오늘은 {date}user 하단에 (현재 시각: {date})
user_idsystem 상단에 담당: {user_id}user 측에서 참조
RAG 청크 (RAG Chunk)매번 다른 순서로 셔플안정적인 ID 순서로 고정
도구 정의 (Tool Definition)요청마다 동적 생성실행 시 고정, 불변

3사의 SDK로 최단 코드를 나열해 보겠습니다. "2번 요청했을 때, 2번째의 usage가 변하면 캐시 성공"인 패턴입니다.

import anthropic
client = anthropic.Anthropic()
LONG_SYSTEM = open("knowledge_base.md").read() # 10k 토큰을 가정한 고정 문맥
...

2번째의 cache_creation_input_tokens0이고, cache_read_input_tokens큰 숫자로 바뀌어 있다면 성공입니다.

OpenAI는 코드 변경이 필요 없습니다. cached_tokens를 확인하기만 하면 됩니다.

from openai import OpenAI
client = OpenAI()
LONG_SYSTEM = open("knowledge_base.md").read()
...

포인트는, system 프롬프트 (System Prompt)를 안정화시키고, user 측만 변경하는 것입니다. 이렇게 하면 자동으로 캐시가 적용됩니다.

from google import genai
from google.genai import types
client = genai.Client()
...

암묵적 캐시 (Implicit Cache)만으로도 충분한 경우가 많지만, 하루에 수백 번씩 동일한 거대 문서를 참조한다면 명시적 캐시 (Explicit Cache)로 설정하여 TTL (Time To Live)을 길게 가져가는 것이 더 안정적입니다.

"전부 캐시해라"는 함정입니다.

단 한 번만 사용하는 긴 문장에 cache_control을 붙이면, 쓰기 비용 (Write Cost)만큼 오히려 더 비싸집니다. Anthropic의 경우, 5분 TTL 시 +25%, 1시간 TTL 시 +100%의 쓰기 비용이 발생합니다. 이를 회수하지 못하면 손해가 됩니다.

동일한 서문을 34회 이상 재사용한다면 캐시하십시오. 12회라면 캐시하지 마십시오.

이것이 9할의 정답입니다.

정확히 계산하고 싶은 분들을 위해.

손익분기점 횟수 = (쓰기 비용 − 읽기 비용) ÷ (일반 비용 − 읽기 비용)

예: Anthropic 5분 TTL의 경우, 쓰기=1.25 / 읽기=0.1 / 일반=1 이라고 가정하면,

(1.25 − 0.1) ÷ (1 − 0.1) = 1.28 회

2회 이상 동일한 서문을 사용하는 것만으로도 이미 본전을 뽑는 계산이 나옵니다. 따라서 "3~4회 이상"이라는 규칙은 안전측으로 잡은 기준이라는 의미입니다.

대상캐시해야 하는가이유
system 프롬프트 (역할·출력 규정)✅ 필수요청마다 불변
...

효과가 보이지 않으면 지속될 수 없습니다.

우선은 "적중률 (Hit Rate) 1개"만 대시보드에 띄우는 것부터 시작해도 충분합니다.

def hit_rate(usage) -> float:
    read = getattr(usage, "cache_read_input_tokens", 0) or 0
    write = getattr(usage, "cache_creation_input_tokens", 0) or 0
    ...

이것을 Prometheus나 Datadog의 메트릭 (Metrics)으로 보내는 것만으로도, **기능별·모델별 적중률 (Hit Rate)**을 확인할 수 있게 됩니다.

조금 더 진보된 운영 방식으로는, OpenTelemetry GenAI Semantic Conventions의 gen_ai.usage.input_tokens

와 함께, 사용자 정의 속성으로 gen_ai.usage.cache_read_tokens를 span에 포함하면 Observability (관측성) 기반과 통합할 수 있습니다.

span.set_attribute("gen_ai.usage.input_tokens", usage.input_tokens)
span.set_attribute("gen_ai.usage.cache_read_input_tokens",
getattr(usage, "cache_read_input_tokens", 0))

실제 운영 측면에서의 감각으로는,

hit rate 80% 이상: 상당히 좋은 상태. 설계가 깔끔함 -
hit rate 50~80%: 동적 블록(dynamic block)의 위치를 재검토할 여지 있음 -
hit rate 30% 미만: 선두에 동적 데이터가 혼입되었을 가능성 매우 높음, 리뷰 필요

를 기준으로 삼으면, 알람(alert) 설계의 출발점이 됩니다.

이 부분이 아마 이 글에서 가장 중요한 부분일 것입니다.

먼저 대전제로서, 캐시는 동일 API 키 내·동일 프로젝트 내에서만 히트(hit)됩니다. 공유 풀(shared pool)에서 타사의 프리픽스(prefix)와 충돌하여 정보가 유출되는 일은 없습니다. 3사 모두 공식적으로 명시하고 있습니다.

그 점을 바탕으로, 빠지기 쉬운 지뢰(주의사항)를 정리합니다.

증상예상 원인확인 명령어 / 관점
cached_tokens가 항상 0프롬프트 선두에 동적 데이터 혼입system 선두를 diff로 비교하여 가변 요소 탐색
월초에만 갑자기 비용 급증TTL 만료 후 쓰기(write) 작업 집중cache_creation_input_tokens의 시계열 시각화
동일한 system임에도 히트되지 않음breakpoint 위치가 너무 뒤에 있음Anthropic: cache_control을 '재사용할 덩어리' 직후에 배치
Anthropic 과금이 예상보다 높음1시간 TTL 선택 + 충분히 사용하지 않음TTL 5분으로 충분한지 재평가
Gemini로 구현했으나 효과가 미미함암묵적 캐시(implicit cache)의 prefix가 짧음크고 안정적인 내용을 맨 앞으로 이동
기밀 정보 유출 리스크가 걱정됨system에 PII(개인정보)가 장기간 체류TTL 단축 + PII 제거 레이어를 전단에 배치

쓰기 비용이 추가되기 때문에, 회수할 수 없습니다. 3회 이상 재사용할 확신이 없는 한 추가하지 마세요.

원칙 4에서 쓴 것처럼, 매 요청마다 실패합니다. 동적인 것은 맨 뒤로 보내세요.

캐시 계층에 짧은 시간이라도 데이터가 남습니다. 로그와 캐시 양쪽 모두에서 **PII 경계 전단 마스킹(masking)**을 수행하는 것이 안전합니다.

변체(variant) A와 B가 번갈아 나오면, 쓰기만 계속 발생하고 읽기는 적은 상태가 됩니다. A/B는 사용자(user) 단위로 고정하는 것이 원칙입니다.

역할 분담을 표로 나타내면 다음과 같습니다.

할 일인간AI
프롬프트 순서 설계지원
cache_creation 급증 원인 가설 수립감독
재배치 리팩토링 안 도출감독
당신은 LLM API 최적화 리뷰어입니다.
다음 프롬프트를 「선두=고정 / 중간=반고정 / 말미=동적」의 3개 계층으로 분류하고,
prefix 일치를 깨뜨리고 있는 부분을 지적하여, 재배치 안을 unified diff 형식으로 제시하세요.
...
최근 24시간 동안의 cache_read_tokens 시계열 CSV를 전달합니다.
- 비정상적인 저하 구간을 탐지하여 시작 시각과 지속 시간 추출
- 동일 시점의 deploy · config 변경과의 상관관계 가설 수립
...
다음 프롬프트 블록에 대하여,
- 추정 토큰 수
- 요청당 재사용 예상 횟수
...

인간이 할 일은 **「순서 설계」와 「경계 판단」**이며, AI에게 시킬 일은 **「관측과 가설 수립 및 재배치 제안」**입니다. 이 역할 분담을 적용하면 운영이 원활해집니다.

마지막으로, 철학적인 이야기를 하나 덧붙이자면.

Prompt Caching은 단순한 비용 절감 기능처럼 보이지만, 시간이 지나며 살펴보면 이것은 **「설계의 거울」**입니다.

동일한 서문을 길게 재사용할 수 있다는 것은, 그만큼 「변하지 않는 것」과 「변하는 것」이 깔끔하게 분리되어 있다는 뜻입니다. 반대로 캐시(Cache)가 전혀 작동하지 않는 앱은 대개 내부도 무질서합니다. 동적인 것과 고정된 것이 뒤섞여 있어, 누가 언제 무엇을 작성했는지 알 수 없는 상태인 것이죠.

즉, 캐시 설계는 코드 정리 정돈과 같은 작업을 프롬프트(Prompt) 측면에서 수행하는 것이라고도 할 수 있습니다.

그리고 중요한 점은, 이 정리는 한 번만 해두면 끝난다는 것입니다. 오늘 시스템 프롬프트(System Prompt)의 맨 앞부분에 숨어 있던 오늘의 날짜를 한 줄 아래로 옮기는 것만으로도, 다음 달 청구서가 쑥 하고 절반으로 줄어듭니다. 오늘의 3분이 다음 달의 나에게 배당금으로 돌아오는 셈입니다.

저는 이것이 코드와 자본의 관계와 조금 닮았다고 생각합니다. 제대로 설계된 코드는 그 이후로 계속해서 나를 대신해 일해줍니다. 손을 움직일 때마다 대가가 발생하는 것이 아니라, 제대로 정돈된 구조가 매 요청(Request)마다 조용히 할인을 계속 뱉어내는 것이죠.

내일의 내가 과거의 나에게

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0