본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 01. 09:17

API 비용을 70% 절감하는 5가지 Anthropic 프롬프트 캐싱 (Prompt Caching) 패턴

요약

Anthropic API의 프롬프트 캐싱을 활용하여 비용을 최대 70% 절감할 수 있는 5가지 패턴을 소개합니다. 시스템 프롬프트와 도구 정의를 캐싱 중단점으로 최적화하여 반복적인 토큰 비용을 획기적으로 줄이는 방법을 다룹니다.

핵심 포인트

  • 시스템 프롬프트를 최상단에 배치하고 캐싱하여 반복 비용 절감
  • 도구 정의(Tool definitions)를 별도로 캐싱하여 에이전트 루프 최적화
  • 캐시는 접두사(Prefix) 방식으로 작동하므로 내용의 순서가 매우 중요
  • Claude Sonnet 기준 최소 1,024 토큰 이상의 블록에 캐싱 적용 권장
  • 시스템 프롬프트 (System-prompt) 캐싱만으로 반복 호출 비용을 절반으로 절감

  • 도구 정의 (Tool definitions)는 별도로 캐싱되어 에이전트 루프 (Agent loops)에 최적화

  • 대화 기록 (Conversation history) 캐싱은 세 번째 턴 이후부터 효과 발생

  • 배치 작업 (Batch jobs)의 경우 기본 5분보다 1시간의 TTL이 더 유리

지난달 저의 Anthropic API 비용은 모델을 단 하나도 바꾸지 않고도 70% 감소했습니다. 제가 바꾼 것은 캐시 중단점 (Cache breakpoints)의 위치였습니다. 제가 출시하는 모든 Claude 통합 서비스에 현재 사용 중인 다섯 가지 패턴을 소개합니다.

패턴 1: 시스템 프롬프트를 가장 먼저 캐싱하라

시스템 프롬프트 (System prompt)는 가장 저렴하게 얻을 수 있는 이득이지만, 대부분의 사람들이 이를 건너뜁니다. 제가 사용하는 에이전트들은 역할, 출력 형식, 안전 규칙, 그리고 몇 가지 예시를 설명하는 4,000 토큰 규모의 시스템 프롬프트로 작동합니다. 이 프롬프트는 세션 내에서 절대 변하지 않습니다. 캐싱을 적용하기 전에는 매 호출마다 이 4,000 토큰에 대해 전체 입력 가격을 지불했습니다. 작업을 완료하기 위해 30번 루프를 도는 에이전트의 경우, 이는 120,000 토큰의 순수한 반복이었습니다.

해결책은 파라미터 하나입니다. 시스템 프롬프트 배열의 마지막 콘텐츠 항목에 type: "ephemeral"이 포함된 cache_control 블록을 추가합니다. 첫 번째 호출은 캐시를 작성하며 비용이 약간 더 발생합니다 (캐시 쓰기에는 약간의 프리미엄이 붙습니다). 그 이후의 모든 호출은 입력 가격의 약 10분의 1 수준으로 캐시를 읽어옵니다.

제가 따르는 규칙은 다음과 같습니다: Claude Sonnet의 경우 캐싱된 블록이 최소 1,024 토큰 이상이어야 하며, 그렇지 않으면 조용히 무시됩니다. 저의 4,000 토큰 프롬프트는 이 조건을 쉽게 충족합니다. 만약 시스템 프롬프트가 짧다면 이 패턴은 아무런 효과가 없으므로, 200 토큰 정도의 지침에 중단점을 추가하느라 애쓸 필요는 없습니다.

순서는 사람들이 예상하는 것보다 더 중요합니다. 캐시는 접두사 (Prefix)로서 작동합니다. 중단점 이전의 모든 것은 저장됩니다. 중단점 이후의 모든 것은 새로 읽힙니다. 따라서 저는 안정적인 내용(역할, 규칙, 예시)을 상단에 배치하고, 변동성이 큰 내용(사용자 쿼리, 현재 날짜)을 중단점 아래에 배치합니다. 순서를 잘못 설정하면 매 호출마다 접두사가 바뀌기 때문에 캐시 적중률 (Cache hit rate)이 무너집니다.

제 로그에서 확인한 실제 수치 하나를 말씀드리자면, 하루에 2,000번 실행되는 문서 분류 (document-classification) 작업이 있습니다. 시스템 프롬프트 (system prompt)는 3,800 토큰입니다. 이를 캐싱함으로써 매일 약 680만 개의 과금 대상 입력 토큰 (billed input tokens)을 절약했습니다. 이는 제가 줄인 항목 중 단일 항목으로는 가장 컸습니다. 만약 이 글에서 단 한 가지만 실천하신다면, 시스템 프롬프트를 캐싱하십시오. 이를 추가하는 데는 4분밖에 걸리지 않았고, 절감 효과는 바로 다음 결제 주기부터 나타났습니다.

저는 이러한 호출을 처음부터 어떻게 구조화하는지 단계별로 설명하는 Claude Blueprint에서 더 광범위한 설정을 다루었습니다.

패턴 2: 도구 정의 (Tool Definitions)를 별도로 캐싱하기

도구 정의 (tool definitions)는 두 번째로 간과되는 비용 항목입니다. 만약 함수 호출 (function calling) 기능을 사용하여 Claude를 에이전트 (agent)로 운영한다면, 도구 스키마 (tool schemas)가 매우 커질 수 있습니다. 제가 가진 에이전트 중 하나는 14개의 도구를 가지고 있는데, JSON 스키마만으로도 9,200 토큰에 달합니다. 이러한 정의들은 시스템 프롬프트나 다른 어떤 것보다도 요청의 맨 앞부분에 위치합니다. 또한 세션 내내 완전히 정적 (static)입니다.

마지막 도구 정의 바로 뒤에 cache_control 중단점 (breakpoint)을 배치할 수 있습니다. 이렇게 하면 도구 블록과 시스템 프롬프트가 모두 캐싱되지만, 원한다면 두 개의 별도 접두사 (prefixes)로 나눌 수 있습니다. 실제로 저는 도구 뒤에 첫 번째 중단점을, 시스템 프롬프트 뒤에 두 번째 중단점을 설정했습니다. Anthropic은 요청당 최대 4개의 중단점을 설정할 수 있도록 허용하므로, 이를 적극 활용하십시오.

왜 하나의 큰 블록 대신 두 개로 나누나요? 바로 유연성 때문입니다. 때때로 저는 동일한 도구는 유지하면서 A/B 테스트를 위해 시스템 프롬프트만 교체하곤 합니다. 중단점을 분리해 두면, 시스템 프롬프트가 변경되어도 도구 캐시는 유지됩니다. 실제로 변경된 부분에 대해서만 전체 가격으로 다시 과금됩니다. 도구 접두사는 계속해서 캐시로부터 읽어옵니다.

에이전트 루프 (agent loops)의 경우, 이 계산 결과는 사용자에게 압도적으로 유리합니다. 단일 에이전트 작업은 추론하고, 도구를 호출하고, 결과를 읽고, 다시 추론하는 과정에서 20~40번의 모델 호출을 발생시킬 수 있습니다. 이 호출 하나하나가 전체 도구 정의를 다시 전송합니다. 9,200 토큰의 도구와 30번의 호출이 있다면, 이는 276,000 토큰에 대해 전체 가격을 지불해야 함을 의미합니다. 캐싱을 사용하면, 단 한 번의 쓰기 (write)와 29번의 저렴한 읽기 (read)로 해결됩니다.

저는 일주일 동안 고객 지원 에이전트(customer-support agent)를 측정했습니다. 이 에이전트는 1,400개의 세션을 처리했습니다. 세션당 평균 18회의 모델 호출(model calls)이 발생했습니다. 도구 정의(tool definitions)를 캐싱함으로써 그 주에 약 2억 1,500만 개의 과금 대상 입력 토큰(billed input tokens)을 절약할 수 있었습니다. 모든 세션에 걸친 캐시 쓰기(cache write) 오버헤드는 각 세션에서 한 번만 쓰기 때문에 그에 비하면 매우 미미했습니다.

함정: 만약 도구 스키마(tool schemas)를 동적으로 생성하고 호출 사이에 JSON 키 순서가 바뀐다면, 접두사(prefix)가 깨지게 됩니다. 저는 이제 매번 바이트(bytes)가 동일하도록 정렬된 키(sorted keys)로 도구를 직렬화(serialize)합니다. 이 작은 습관 하나가 제가 이틀 동안이나 쫓았던 미스터리한 캐시 미스(cache miss)율 문제를 해결해 주었습니다.

패턴 3: 대화 기록이 성장함에 따라 캐싱하기

이 패턴은 가장 많은 주의가 필요하지만, 긴 채팅에서 가장 큰 보상을 제공합니다. 멀티 턴(multi-turn) 대화에서는 매 턴마다 기록이 늘어납니다. 첫 번째 턴에서는 500 토큰을 보내지만, 열 번째 턴에서는 누적된 대화로 인해 8,000 토큰을 보낼 수도 있습니다. 캐싱이 없다면, 매 턴마다 계속 늘어나는 전체 기록에 대해 전체 비용을 지불해야 합니다.

비결은 이동하는 중단점(moving breakpoint)입니다. 어시스턴트(assistant)의 응답이 있을 때마다, 저는 메시지 목록 내의 가장 최신 안정 지점으로 cache_control 마커를 이동시킵니다. 해당 지점까지의 전체 대화가 캐싱된 접두사(cached prefix)가 됩니다. 다음 사용자 턴은 이 중단점 뒤에 내용을 추가하며, 중단점 이전의 모든 내용은 캐시에서 읽어옵니다.

여기에는 매우 미묘한 차이가 있습니다. 캐시는 접두사 일치(prefix match) 방식입니다. 따라서 중단점 이전의 메시지들은 이전 호출과 바이트 단위로 완전히 동일해야 합니다. 대화 중간에 기록을 다듬거나(trim), 요약하거나(summarize), 순서를 바꾸면(reorder) 그 시점부터 캐시가 무효화되며, 이를 다시 구축하기 위해 전체 비용을 지불해야 합니다. 저는 기록을 제자리에서 수정하지 않는 법을 배웠습니다. 오직 추가(append)만 합니다.

저의 규칙: 중단점을 마지막 메시지가 아닌, 마지막에서 두 번째 메시지에 배치합니다. 이렇게 하면 최신 교환 내용은 캐싱되지 않은 상태로 유지되어 다음 라운드에서 쓰기 비용이 저렴하게 유지되면서도 안정적인 경계선을 유지할 수 있습니다. 세 번째 턴에 도달하면 캐시는 확실히 비용 효율을 냅니다. 세 번째 턴 이전에는 쓰기 오버헤드가 읽기 절감액을 약간 초과할 수 있으므로, 매우 짧은 상호작용의 경우에는 기록 캐싱을 완전히 생략합니다.

구체적인 결과: 제가 운영하는 코딩 어시스턴트(coding assistant)는 세션당 평균 11턴을 수행하며, 마지막에는 히스토리가 약 14,000 토큰(tokens)에 도달합니다. 증가하는 접두사(prefix)를 캐싱함으로써, 모든 내용을 매번 새로 전송하는 것과 비교했을 때 세션당 입력 비용을 약 60% 절감했습니다. 한 달에 600개 세션을 운영하는 입장에서 이는 청구서의 상당 부분을 차지하는 금액이었습니다.

Claude를 장기 실행 에이전트(long-lived agent)로 운영하는 것에 대한 더 깊은 맥락이 필요하다면, Claude Agent SDK in Production을 참조하세요. 해당 글에서는 메시지 관리(message-management) 측면을 더 자세히 다룹니다. 요약하자면, '추가 전용 히스토리(append-only history)와 이동하는 중단점(moving breakpoint)'의 조합이 효과적이라는 것입니다.

패턴 4: 배치(Batch) 및 간헐적(Bursty) 작업을 위한 1시간 TTL 사용

기본 캐시 수명(cache lifetime)은 5분입니다. 캐시를 읽을 때마다 해당 타이머가 초기화되므로, 활발한 대화는 스스로 캐시를 따뜻하게(warm) 유지합니다. 문제는 간헐적(bursty)이거나 예약된 작업입니다. 만약 작업이 배치(batch) 사이에 7분 동안 중단된다면, 캐시는 만료되고 다음 배치에서 다시 쓰기 프리미엄(write premium) 비용을 지불해야 합니다.

Anthropic은 1시간의 TTL(Time To Live)을 제공합니다. cache_control 객체에 TTL을 설정함으로써 캐시 블록별로 이를 선택할 수 있습니다. 쓰기 비용은 5분 설정보다 약간 더 높지만, 캐시는 읽기가 전혀 없는 상태에서도 최대 1시간까지 공백을 견뎌냅니다.

저는 세 가지 종류의 작업에 1시간 TTL을 사용합니다. 첫째, 동일한 시스템 프롬프트(system prompt)를 대상으로 몇 분마다 실행되는 예약된 배치 작업(scheduled batch jobs)입니다. 5분 캐시는 공백 사이에 계속 만료되었지만, 1시간 캐시는 전체 배치 기간 동안 유지됩니다. 둘째, 사용자가 답변을 읽는 시간이 긴 사용자 세션입니다. 예를 들어, 누군가 2,000단어 분량의 답변을 10분 동안 읽은 후 답장하는 연구 도구와 같은 경우입니다. 5분 캐시는 사용자의 다음 메시지가 올 때쯤이면 항상 식어(cold) 있었습니다. 셋째, 1단계가 완료된 후 별도의 프로세스가 몇 분 동안 작업을 수행하고, 그 다음 2단계가 동일한 캐시된 컨텍스트(cached context)를 재사용하는 다단계 파이프라인(multi-stage pipelines)입니다.

결정 방법은 머릿속으로 간단한 산수를 해보는 것입니다. 호출 사이의 평균 간격이 5분 미만이라면, 기본 설정(default)이 무료이며 충분합니다. 만약 간격이 정기적으로 5분에서 1시간 사이라면, 더 높은 쓰기 비용(write cost)에도 불구하고 1시간 TTL(Time To Live)이 거의 항상 유리합니다. 반복적인 전체 비용 재구축(full-price rebuilds)을 피할 수 있기 때문입니다. 만약 간격이 1시간을 초과한다면, 어떤 캐싱 전략도 도움이 되지 않으므로 새로운 비용(fresh cost)을 감수해야 합니다.

저를 당황하게 했던 세부 사항 하나는 TTL이 특정 중단점(breakpoint)에 적용된다는 점입니다. 즉, 제가 1시간짜리 시스템 프롬프트 캐시(system prompt cache)와 5분짜리 히스토리 캐시(history cache)를 가지고 있다면, 이들은 각각 독립적으로 만료됩니다. 히스토리 캐시는 만료되었지만 시스템 캐시는 살아있는 작업이 있었는데, 이 부분적인 캐시 히트(cache hit)가 두 캐시를 모두 1시간으로 맞추기 전까지는 비용 대시보드(cost dashboard)를 혼란스럽게 만들었습니다.

이 모든 과정의 소셜 미디어 측면을 배치 윈도우(batch windows)에 맞춰 스케줄링하기 위해, 저는 Buffer를 통해 게시물을 실행하여 발행 주기(publishing cadence)가 무거운 API 실행과 충돌하지 않도록 합니다.

패턴 5: 캐시 친화성을 위해 전체 프롬프트 구조화하기

마지막 패턴은 단일한 기술이 아닙니다. 이는 나머지 네 가지 패턴이 함께 작동할 수 있도록 만드는 레이아웃 규율(layout discipline)입니다. 요청을 가장 안정적인 것부터 가장 불안정한 것 순서로 위에서 아래로 배치하십시오. 도구(tools)를 먼저(거의 변경되지 않음), 그다음 시스템 프롬프트(세션당 안정적), 그다음 대화 히스토리(성장하지만 추가만 가능(append-only)), 마지막으로 현재 사용자 입력(항상 새로움) 순으로 배치합니다. 이러한 레이어 사이의 경계에 중단점(breakpoints)을 설정하십시오.

이러한 순서가 중요한 이유는 캐시가 시작 지점부터 연속된 접두사(contiguous prefix)만 일치시키기 때문입니다. 단 1바이트라도 변경되는 순간, 그 이후의 모든 내용은 캐싱되지 않습니다. 따라서 변동성이 큰 콘텐츠는 가능한 한 아래에 배치하고, 안정적인 콘텐츠는 가능한 한 위쪽에 배치해야 합니다. 저는 이를 스택(stack)처럼 생각합니다. 무겁고 변하지 않는 것을 바닥에, 가볍고 변하는 것을 맨 위에 두는 것입니다.

완벽한 순서를 갖추었더라도 캐시 히트(cache hits)를 망치는 두 가지 요소가 있습니다. 바로 안정적인 레이어(stable layers)에 주입되는 타임스탬프(timestamps)와 무작위 ID(random IDs)입니다. 저는 한때 해롭지 않다고 생각하여 시스템 프롬프트(system prompt)에 현재 날짜를 넣은 적이 있습니다. 이로 인해 매일 자정마다 접두사(prefix)가 변경되었고, 제 캐시 히트율은 소리 없이 절반으로 떨어졌습니다. 이제 시간 민감적인 모든 값은 가장 낮은 중단점(breakpoint) 아래, 즉 원래 있어야 할 위치인 사용자 턴(user turn)에 배치합니다.

두 번째 방해 요소는 비결정론적 직렬화(non-deterministic serialization)입니다. JSON 라이브러리가 항상 동일한 순서로 키(key)를 출력하는 것은 아닙니다. 부동 소수점(floating point) 형식도 다를 수 있습니다. 저는 캐시 블록(cached block)에 들어가는 모든 것을 정규화(normalize)합니다. 즉, 키 정렬, 고정된 숫자 형식, 불필요한 공백 차이 제거 등을 수행합니다. 이를 강제한 후, 동일한 워크로드에서 제 캐시 히트율은 좌절스러웠던 71%에서 안정적인 96%로 올라갔습니다.

히트율을 측정하십시오. API 응답은 cache_creation_input_tokenscache_read_input_tokens를 반환합니다. 저는 매 호출마다 이 두 값을 로그로 남기고 매일 밤 읽기 비율(read ratio)을 계산합니다. 그 비율이 떨어지면 안정적인 레이어 중 무언가가 변경되었다는 뜻이므로 원인을 찾아 나섭니다. 이러한 모니터링 덕분에 캐싱은 일회성 설정이 아닌, 제가 신뢰할 수 있는 체계로 자리 잡았습니다. 배경 지식: Claude Blueprint에서는 제가 이 로깅(logging)을 나머지 스택에 어떻게 연결하는지 자세히 다룹니다.

만약 이미지 파이프라인(image pipeline) 또한 API를 호출한다면, 동일한 접두사 로직이 적용됩니다. 저는 업스케일링(upscaling)을 위해 Magnific을 사용하며, 동일한 이유로 정적 설정(static config)을 이미지별 페이로드(per-image payload)에서 분리하여 관리합니다.

결론 (Bottom Line)

캐싱은 제가 Claude 청구서에 적용한 변화 중 가장 높은 수익률을 가져다준 변화였으며, 실제 로직에는 전혀 영향을 주지 않았습니다. 다섯 가지 패턴을 쌓아 올리십시오: 시스템 프롬프트를 캐싱하고, 도구 정의(tool definitions)를 별도의 중단점에 캐싱하며, 이동하는 추가 전용 마커(append-only marker)와 함께 대화 기록을 캐싱하십시오. 또한 호출이 간헐적(bursty)일 때는 1시간 TTL(Time To Live)을 활용하고, 접두사가 유지될 수 있도록 모든 요청을 안정적인 것부터 휘발성인 것 순으로 배치하십시오.

제 워크로드에 미친 결합 효과는 청구된 입력 토큰(input tokens)의 70% 감소였습니다. 그 중 대부분은 시스템 프롬프트(system prompt)와 도구 정의(tool definition) 패턴에서 발생했는데, 이는 에이전트 루프(agent loops)가 매 호출마다 이를 반복하기 때문입니다. 히스토리(history) 및 TTL 패턴이 나머지 부분을 해결했습니다.

이번 주에는 시스템 프롬프트부터 시작해 보십시오. 단 하나의 파라미터이며 4분 정도의 작업이면 충분합니다. 그다음 에이전트를 실행한다면 도구 캐싱(tool caching)을 추가하십시오. cache_read_input_tokens 수치가 올라가는 것을 지켜보며, 해당 숫자를 통해 경계(boundaries)가 올바르게 설정되었는지 확인하십시오. 제가 Claude 작업을 엔드 투 엔드(end-to-end)로 어떻게 구조화하는지 전체적인 그림을 보고 싶다면, Claude Blueprint에서 제가 기록 중인 노트를 확인하실 수 있습니다.

이 기사에는 제휴 링크가 포함되어 있습니다. 이 링크를 통해 가입하시면 귀하에게 추가 비용 없이 저에게 소정의 수수료가 지급될 수 있습니다. (광고)

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0