상태 비저장(Stateless) AI API의 숨겨진 비용
요약
AI API의 상태 비저장(Stateless) 특성으로 인해 발생하는 누적 토큰 비용 문제를 분석합니다. 대화가 길어질수록 이전 메시지를 매번 다시 전송해야 하므로 입력 비용이 기하급수적으로 증가하는 구조를 설명합니다.
핵심 포인트
- AI API는 이전 대화 내용을 기억하지 않는 Stateless 방식임
- 멀티턴 대화 시 이전 메시지를 모두 재전송하여 입력 비용 발생
- 대화 턴이 길어질수록 입력 토큰 비용이 복리로 증가함
- Stateless 설계는 API 제공자의 확장성에는 유리하나 개발자 비용을 높임
만약 여러분이 AI API를 사용하여 챗봇, 에이전트(Agent), 멀티턴 어시스턴트(Multi-turn assistant)와 같은 대화형 서비스를 구축하고 있다면, 대부분의 개발자가 청구서를 받기 전까지는 미처 생각하지 못하는 비용이 눈앞에 숨어 있습니다.
모든 AI API는 상태 비저장(Stateless) 방식입니다. 모델은 호출(Call) 사이에 아무런 기억을 유지하지 않습니다. 전혀 없습니다. Claude, GPT, Gemini 등 어떤 제공업체를 사용하든 상관없습니다. API는 여러분이 이전 메시지에서 무엇을 말했는지 알지 못합니다. 만약 5번째 메시지를 보낼 때 AI가 1번째 메시지에서 사용자의 이름이 Ravi였다는 사실을 기억하게 하고 싶다면, 매번 1번부터 4번까지의 메시지를 다시 보내야 합니다.
그리고 여러분은 매번 다시 보내는 것에 대해 비용을 지불합니다.
아무도 말하지 않는 수학적 계산
이것이 실제로 어떻게 나타나는지 과정을 살펴보겠습니다.
여러분은 Claude Sonnet을 사용하여 고객 지원 챗봇을 구축하고 있습니다. 평균적인 대화는 10번의 주고받기(Turn)로 이루어집니다. 각 사용자 메시지는 대략 50개의 입력 토큰(Input tokens)이고, 각 어시스턴트 응답은 대략 200개의 출력 토큰(Output tokens)입니다. 지원용 봇으로서는 꽤 전형적인 수치입니다.
10번째 메시지에서 여러분이 지불하고 있다고 생각하는 비용은 다음과 같습니다:
- 50 입력 토큰 (새로운 사용자 메시지)
- 200 출력 토큰 (응답)
하지만 10번째 메시지에서 실제로 지불하는 비용은 다음과 같습니다:
- 50 (사용자의 10번째 메시지)
- 50 + 200 (사용자 및 어시스턴트의 1번째 메시지)
- 50 + 200 (2번째 메시지)
- 50 + 200 (3번째 메시지)
- 50 + 200 (4번째 메시지)
- 50 + 200 (5번째 메시지)
- 50 + 200 (6번째 메시지)
- 50 + 200 (7번째 메시지)
- 50 + 200 (8번째 메시지)
- 50 + 200 (9번째 메시지)
- 새로운 응답을 위한 200 출력 토큰
이는 50과 200이 아니라, 2,300개의 입력 토큰과 200개의 출력 토큰입니다. 여러분은 10번째 메시지에서 생각했던 것보다 입력 비용을 46배 더 많이 지불하고 있는 것입니다.
그리고 이후의 모든 메시지에서 상황은 더 악화됩니다. 15번째 메시지는 훨씬 더 많은 비용이 듭니다. 20번째 메시지는 진정으로 비쌉니다. Claude Sonnet 가격(입력 100만 토큰당 $3, 출력 100만 토큰당 $15) 기준으로 30턴의 대화는 단일 교환 비용보다 대략 15~20배의 비용이 발생합니다.
하루에 수천 건의 대화를 처리하는 프로덕션 챗봇(Production chatbot)의 경우, 이러한 복리 효과(Compounding effect)는 모델 선택보다, 프롬프트 최적화(Prompt optimization)보다, 그 무엇보다도 API 비용을 발생시키는 가장 큰 요인이 됩니다.
API가 이런 방식으로 작동하는 이유
상태 비저장(Stateless) 설계는 버그나 실수로 인한 것이 아닙니다. 이는 의도적인 아키텍처 설계(Architectural choice)이며, 제공자(Provider)의 관점에서는 합리적입니다.
상태 비저장 API는 수평적 확장(Scale horizontally)이 가능합니다. 어떤 서버도 이전 요청에 대해 알 필요가 없기 때문에 어떤 요청이든 어떤 서버로든 전달될 수 있습니다. 운영이 더 간단하고 디버깅이 쉬우며, 모델의 연산 프로필(Compute profile)을 예측 가능하게 유지합니다. 모든 요청은 처음부터 처리되는 새로운 컨텍스트 윈도우(Context window)입니다.
하지만 "제공자에게 더 간단하다"는 것은 개발자에게 "더 비싸다"는 의미로 직결됩니다. 개발자는 제공자의 아키텍처적 편의성을 위해 매 요청마다 토큰 단위로 비용을 지불하게 됩니다.
대부분의 개발자가 구축하는 우회 방법들
채팅 애플리케이션을 구축하는 모든 개발자는 보통 첫 번째 큰 청구서를 받은 후에 이 문제를 스스로 깨닫게 됩니다. 그리고 나서 다음과 같은 네 가지 우회 방법 중 하나를 구축합니다.
슬라이딩 윈도우 (Sliding window). 마지막 N개의 메시지만 유지하고 나머지는 버립니다. 구현은 간단하지만, AI가 이전의 컨텍스트(Context)를 실제로 잊어버리게 됩니다. 사용자는 했던 말을 반복해야 할 때 좌절감을 느낍니다.
요약 (Summarization). 오래된 메시지들을 주기적으로 압축된 컨텍스트 블록(Context block)으로 요약합니다. 슬라이딩 윈도우보다 품질은 좋지만, 복잡성이 증가하고 또 다른 모델 호출(Model call)이 발생하며, 요약 자체에도 토큰 비용이 발생합니다.
대화 데이터베이스 (Conversation database). 메시지를 Postgres나 Redis에 저장하고, 매 요청마다 이를 검색하여 컨텍스트를 조립한 뒤 API로 전송합니다. 이는 프로덕션 앱을 위한 올바른 정답이지만, 스키마(Schema), TTL 관리, 동시 액세스(Concurrent access), 정리(Cleanup), 모니터링 등 상당한 엔지니어링 작업이 필요합니다.
프롬프트 캐싱 (Prompt caching). Anthropic과 OpenAI 모두 이제 프롬프트 캐싱 (Prompt caching)을 지원하며, 프롬프트의 일부를 캐싱 가능하도록 표시할 수 있고, 캐시 히트 (Cache hit) 시 제공업체는 일반 입력 요율의 10%만 청구합니다. 이는 진정으로 유용하며 비용을 크게 절감해 줍니다. 하지만 캐싱 의미론 (Caching semantics)을 이해해야 하고, 프롬프트를 올바르게 구조화해야 하며, 캐시 중단점 (Cache breakpoints)을 관리해야 합니다. 또한 이는 동일하게 반복되는 콘텐츠에 대해서만 도움이 됩니다. 동적인 대화 기록 (Dynamic conversation history)은 매 턴마다 변경되기 때문에 캐싱하기가 더 어렵습니다.
이 네 가지 접근 방식 모두 작동합니다. 하지만 네 가지 방식 모두 개발자가 제품의 핵심이 아닌 메모리 관리 코드를 작성, 유지 관리 및 디버깅해야 합니다. 고객 지원 챗봇을 만드는 사람 중 누구도 대화 기록 압축 (Conversation history compression) 전문가가 되고 싶어 하지 않습니다.
메모리 서비스형 (Memory-as-a-service)의 모습
여기 대안이 있습니다. 메모리 문제를 애플리케이션 외부로 완전히 옮기는 것입니다.
코드에서 대화 기록을 관리하는 대신, 프록시 (Proxy)가 애플리케이션과 AI 제공업체 사이에 위치합니다. 당신은 프록시에 새로운 메시지만 보냅니다. 프록시가 대화 저장, 다음 호출 시 대화 검색, 컨텍스트 (Context) 조립, 제공업체로의 전달, 캐싱 가능한 항목의 캐싱 등 나머지 모든 것을 처리합니다.
당신의 코드는 더 단순해집니다. 훨씬 더 단순해집니다. 세션 메모리 (Session memory)가 없을 때의 클라이언트 코드는 다음과 같습니다:
# 메모리 없음 — 직접 기록을 관리함
conversation = [
{"role": "system", "content": "You are a helpful assistant."}
...
그리고 프록시 수준에서 세션 메모리를 사용할 때의 모습은 다음과 같습니다:
# 메모리 있음 — 프록시가 기록을 처리함
def chat(user_message):
response = openai.chat.completions.create(
...
그게 전부입니다. 대화 배열 (Conversation array)도, 기록 관리도, 데이터베이스 쿼리 (Database queries)도 필요 없습니다. 당신은 메시지 하나만 보냅니다. 프록시가 전체 컨텍스트를 제공업체에 보내고 평소대로 비용을 청구합니다. 복잡성은 추상화됩니다.
솔직한 트레이드오프 (Tradeoff)
메모리 서비스형 (Memory-as-a-service)이 무엇을 해결하고 무엇을 해결하지 못하는지에 대해 명확히 하고 싶습니다.
해결하는 문제: 애플리케이션 코드 내에서 대화 기록 (conversation history)을 관리하는 복잡성을 해결합니다. 더 이상 메모리 관리 코드를 작성할 필요가 없습니다. 컨텍스트 윈도우 (context window) 오버플로를 디버깅할 필요도 없습니다. 대화 데이터베이스를 구축할 필요도 없습니다. 당신의 채팅 기능은 단일 API 호출을 감싸는 5줄짜리 래퍼 (wrapper)가 됩니다.
해결하지 못하는 문제: 근본적인 토큰 비용 (token cost)입니다. 제공업체는 재주입된 기록을 포함하여 모든 입력 토큰에 대해 여전히 비용을 청구합니다. 왜냐하면 제공업체는 여전히 해당 토큰들을 처리해야 하기 때문입니다. 메모리를 프록시 (proxy)로 옮긴다고 해서 토큰이 무료가 되지는 않습니다.
하지만 이는 애플리케이션 코드에서 수행하기 어려운 최적화 (optimizations)를 가능하게 합니다. 프록시는 세션 전반에 걸쳐 프롬프트 캐싱 (prompt caching)을 자동으로 적용할 수 있습니다. 프록시는 대화가 길어질 때, 비싼 모델로 전달하기 전에 저렴한 모델을 사용하여 컨텍스트를 요약함으로써 오래된 메시지를 압축할 수 있습니다. 프록시는 유사한 시스템 프롬프트 (system prompts)를 사용하는 사용자들 간에 캐시 히트 (cache hits)를 공유할 수 있습니다. 이들은 모두 개별 개발자가 직접 구현하기에는 엔지니어링 노력이 아까워 거의 구현하지 않는 최적화들입니다.
규모가 커지면, 수백만 개의 세션을 처리하는 프록시는 개별 앱이 할 수 없는 방식으로 메모리 계층을 최적화할 수 있습니다. 이것이 진정한 비용 이점입니다. 메모리가 존재한다는 사실이 아니라, 메모리가 이를 관리하기 위해 특별히 설계된 인프라에 의해 관리된다는 사실 말입니다.
이것이 필요한 경우
모든 AI 애플리케이션에 세션 메모리 (session memory)가 필요한 것은 아닙니다. 분류 (classification), 추출 (extraction), 번역 (translation), 단일 문서 요약 (summarization)과 같은 상태 비저장 (stateless) 작업을 수행하고 있다면 유지해야 할 대화가 없습니다. Prism의 세션 메모리 기능이 선택 사항 (opt-in)인 이유는 바로 대부분의 API 호출에 이것이 필요하지 않기 때문입니다.
하지만 대화형(conversational)이거나, 멀티 턴(multi-turn) 방식이거나, 호출 간에 컨텍스트를 유지하는 에이전트형(agentic) 애플리케이션을 구축하고 있다면 메모리 전략이 필요합니다. 문제는 그 전략을 직접 구축할 것인지, 아니면 인프라가 처리하도록 맡길 것인지입니다.
주말 동안 진행하는 사이드 프로젝트라면, 직접 슬라이딩 윈도우 (Sliding Window)를 구현해도 괜찮습니다. 하지만 수천 명의 사용자를 지원하는 프로덕션 챗봇(Production Chatbot)이라면 더 견고한 무언가가 필요할 것입니다. 그리고 이 "더 견고한 것"이야말로 미뤄지고, 우선순위에서 밀려나다가, 결국 새벽 2시에 프로덕션 환경에서 시스템을 망가뜨리는 바로 그 종류의 엔지니어링 문제입니다.
Memory-as-a-service (서비스형 메모리)가 존재하는 이유는 이 문제가 충분히 흔하고 고통스러워서, 모든 개발자가 각자 해결할 필요가 없도록 중앙에서 한 번 해결할 가치가 있기 때문입니다.
핵심 요약 (The takeaway)
상태 비저장 (Stateless) AI API는 제공자에게는 편리하며, 비용 측면의 영향을 고려하지 않는 개발자들에게는 눈에 보이지 않습니다. 하지만 모든 멀티 턴 대화 (Multi-turn conversation)는 숨겨진 세금을 지불하고 있습니다. 즉, 전체 대화 기록을 위한 입력 토큰 (Input tokens)이 매 호출마다 영원히 다시 전송되고 있는 것입니다.
여러분은 슬라이딩 윈도우 (Sliding windows), 요약 (Summarization), 대화 데이터베이스 (Conversation databases), 프롬프트 캐싱 (Prompt caching), 또는 이들을 조합하여 이에 맞설 수 있습니다. 이 방법들은 모두 작동합니다. 하지만 이 모든 방법은 실제 제품과는 무관한 엔지니어링 노력을 요구합니다.
또는 메모리를 애플리케이션 코드에서 완전히 분리하여 프록시 (Proxy)가 처리하도록 할 수 있습니다. 그러면 코드는 더 단순해지고, 사고 모델 (Mental model)은 더 깔끔해집니다. 복잡성은 이를 처리하도록 설계된 곳으로 옮겨집니다.
어떤 방식이든, 첫 번째 단계는 이러한 세금이 존재한다는 사실을 인식하는 것입니다. 대부분의 개발자는 청구서가 도착하기 전까지는 이를 인식하지 못합니다.
저는 대화 데이터베이스를 구축하지 않고도 세션 메모리 (Session memory)를 사용하고 싶어서 Prism을 만들었습니다. OpenAI 호환 요청에 헤더 하나(X-Prism-Session)만 추가하면 Prism이 대화 기록을 대신 처리해 줍니다. Anthropic, OpenAI, Google 모두에서 작동합니다. 무료 티어를 사용할 수 있습니다. 문서 읽기.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기