Claude Code 구축에서 얻은 교훈 - 프롬프트 캐싱이 전부다
목차
개요
“Cache Rules Everything Around Me” - 엔지니어링에서 자주 인용되는 이 문구는 AI 에이전트에도 그대로 적용됩니다. Claude Code와 같은 장시간 실행 에이전트 제품은 프롬프트 캐싱 없이는 현실적인 비용과 속도로 운영하기 어렵습니다. 프롬프트 캐싱은 이전 API 호출의 연산을 재사용해 지연 시간과 비용을 크게 줄여줍니다. Claude Code 팀은 캐시 적중률을 서비스 업타임처럼 모니터링하며, 낮아지면 즉시 인시던트로 처리합니다. 이 글은 Claude Code를 구축하며 얻은 프롬프트 캐싱 최적화의 핵심 교훈을 공유합니다.
프롬프트 캐싱의 원리
프롬프트 캐싱은 접두사 매칭(prefix matching) 방식으로 작동합니다. API는 요청의 시작부터 각 cache_control 브레이크포인트까지의 내용을 캐시합니다. 이 원리가 의미하는 것은 하나입니다. 접두사 어디서든 변경이 생기면, 그 이후의 모든 캐시가 무효화됩니다. 따라서 프롬프트의 순서 배치가 시스템 전체 성능을 결정합니다.
프롬프트 레이아웃 설계
정적 콘텐츠를 앞에, 동적 콘텐츠를 뒤에
캐싱을 최대화하려면 변하지 않는 내용을 앞에, 변하는 내용을 뒤에 배치해야 합니다. Claude Code의 프롬프트 구조는 다음과 같습니다.
| 순서 | 콘텐츠 | 캐시 범위 |
|---|---|---|
| 1 | 정적 시스템 프롬프트 + 도구 정의 | 전역 캐시 |
| 2 | CLAUDE.md | 프로젝트 내 캐시 |
| 3 | 세션 컨텍스트 | 세션 내 캐시 |
| 4 | 대화 메시지 | 매번 갱신 |
캐시를 깨는 흔한 실수들
이 순서는 생각보다 깨지기 쉽습니다. 실제 Claude Code 팀이 겪었던 캐시 무효화 사례들입니다.
- 시스템 프롬프트에 상세한 타임스탬프를 포함시킨 경우
- 도구 정의 순서가 비결정적으로 셔플된 경우
- AgentTool이 호출할 수 있는 에이전트 목록이 업데이트된 경우
이런 사소한 변화 하나가 전체 캐시를 무효화합니다.
캐시를 지키는 실전 전략
시스템 프롬프트 대신 메시지 사용
세션 도중 프롬프트의 정보가 오래될 때(예: 현재 시간, 파일 변경 사항), 시스템 프롬프트를 수정하고 싶은 충동이 생깁니다. 하지만 이는 캐시 미스를 유발하고 비용이 크게 증가합니다. 대신, 다음 턴의 사용자 메시지나 도구 결과에 <system-reminder> 태그로 업데이트된 정보를 삽입하는 방식을 사용합니다. 캐시를 건드리지 않고 모델에게 최신 정보를 전달할 수 있습니다.
모델을 세션 중간에 바꾸지 말 것
프롬프트 캐시는 모델별로 독립적으로 존재합니다. 이것이 직관에 반하는 수학을 만들어냅니다. Opus와 10만 토큰짜리 대화를 진행 중일 때, 간단한 질문을 Haiku에게 넘기고 싶을 수 있습니다. 하지만 이 경우 Haiku가 전체 컨텍스트를 새로 처리해야 하므로, Opus가 답하는 것보다 실제로 더 비쌀 수 있습니다.
모델을 변경해야 한다면, 서브에이전트 방식을 사용합니다. 상위 모델이 하위 모델에게 필요한 정보를 담은 “핸드오프 메시지”를 준비해 전달합니다. Claude Code의 Explore 에이전트가 이 방식으로 Haiku를 활용합니다.
도구를 추가하거나 제거하지 말 것
세션 중간에 도구 세트를 변경하는 것은 캐시를 깨는 가장 흔한 실수입니다. “지금 필요한 도구만 제공해야 한다”는 직관이 있지만, 도구는 캐시된 접두사의 일부이므로 변경하면 전체 대화의 캐시가 무효화됩니다.
Plan Mode와 Tool Search의 캐시 설계
Plan Mode: 도구 교체 대신 상태 전환 도구
Plan Mode의 직관적인 구현은 이렇습니다. 플랜 모드 진입 시 읽기 전용 도구만 남기고 나머지를 제거합니다. 하지만 이는 캐시를 깨뜨립니다.
Claude Code의 실제 구현은 다릅니다. 모든 도구를 항상 요청에 포함시키되, EnterPlanMode와 ExitPlanMode를 도구로 만들었습니다. 플랜 모드 진입 시에는 시스템 메시지로 현재 플랜 모드임을 알리고, 제약 사항을 설명합니다. 도구 정의는 절대 변하지 않습니다.
이 설계의 보너스 효과도 있습니다. EnterPlanMode가 도구이므로, 모델이 어려운 문제를 감지했을 때 캐시 손상 없이 자율적으로 플랜 모드에 진입할 수 있습니다.
Tool Search: 제거 대신 지연 로딩
MCP 도구가 수십 개라면 모든 스키마를 항상 포함하는 것은 비용이 큽니다. 그렇다고 도구를 제거하면 캐시가 깨집니다. 해결책은 defer_loading입니다.
도구를 제거하는 대신, 도구 이름만 담은 경량 스텁(defer_loading: true)을 항상 포함합니다. 모델이 실제로 해당 도구를 쓰겠다고 선택하면, 그때 ToolSearch 도구를 통해 전체 스키마를 로드합니다. 캐시된 접두사는 항상 동일한 스텁으로 안정적으로 유지됩니다.
컨텍스트 포킹과 컴팩션
컴팩션의 숨겨진 비용
컨텍스트 창이 가득 차면 대화를 요약하고 새 세션으로 이어가는 컴팩션을 실행합니다. 직관적인 구현은 다른 시스템 프롬프트와 도구 없이 별도 API 호출로 요약을 생성하는 것입니다. 하지만 이 경우 캐시된 접두사가 전혀 매칭되지 않아, 모든 입력 토큰에 대해 전액을 지불하게 됩니다.
캐시 안전 포킹 (Cache-Safe Forking)
Claude Code의 컴팩션은 다릅니다. 부모 대화와 동일한 시스템 프롬프트, 사용자 컨텍스트, 시스템 컨텍스트, 도구 정의를 사용합니다. 부모 대화의 메시지들을 앞에 붙이고, 컴팩션 프롬프트를 새로운 사용자 메시지로 마지막에 추가합니다. API 관점에서 이 요청은 부모 대화의 마지막 요청과 거의 동일하게 보여 캐시 적중이 발생합니다. 새롭게 처리해야 하는 토큰은 컴팩션 프롬프트뿐입니다.
핵심 교훈 요약
Claude Code 팀이 정리한 핵심 교훈들입니다.
| 교훈 | 상세 내용 |
|---|---|
| 접두사 매칭 이해 | 접두사 어디서든 변경이 생기면 이후 모든 캐시가 무효화된다 |
| 메시지로 업데이트 | 시스템 프롬프트 변경 대신 메시지에 업데이트 정보를 삽입한다 |
| 도구/모델 변경 금지 | 도구 상태는 도구로, 모델 전환은 서브에이전트로 처리한다 |
| 캐시 적중률 모니터링 | 업타임처럼 모니터링하고 이상 시 인시던트로 처리한다 |
| 포크 시 접두사 공유 | 컴팩션, 요약 등 파생 작업은 부모와 동일한 접두사를 사용한다 |
결론
프롬프트 캐싱은 에이전트 시스템의 비용과 속도를 결정하는 핵심 인프라입니다. Claude Code는 처음부터 프롬프트 캐싱을 중심으로 전체 아키텍처를 설계했습니다. 정적 콘텐츠를 앞에 배치하면 대부분의 캐싱은 자연스럽게 동작합니다. Plan Mode의 도구 기반 상태 전환, defer_loading을 통한 도구 지연 로딩, 캐시 안전 컴팩션은 이 원칙을 실제 제품에 적용한 구체적인 사례입니다. 에이전트를 구축한다면, 캐싱은 후에 추가하는 최적화가 아니라 첫날부터 설계에 포함해야 합니다.