
AI 앱을 프로덕션 환경에 배포하기 (비용, 확장성 및 예상치 못한 문제 방지)
요약
AI 애플리케이션을 로컬 테스트 환경에서 실제 프로덕션 환경으로 전환할 때 발생하는 비용, 확장성, 품질 관리 문제를 다룹니다. 사용자 규모가 커짐에 따라 발생하는 예상치 못한 비용 폭증과 지연 시간 문제를 방지하기 위한 전략을 제시합니다.
핵심 포인트
- 로컬 테스트와 프로덕션 환경의 운영 규모 차이 이해
- 사용자 증가에 따른 프롬프트 비용 및 모델 호출 비용 관리의 중요성
- 확장 시 발생하는 지연 시간(Latency) 및 병목 현상 대응
- 대규모 요청 처리를 위한 체계적인 품질 관리(Quality Control) 시스템 필요
AI 앱을 프로덕션 환경에 배포하기 (비용, 확장성 및 예상치 못한 문제 방지)
작성자: Syed Muhammad Ali Raza
이 시리즈의 일곱 번째 글을 쓰는 지금까지 저는 가장 화려하지 않지만 가장 비용이 많이 드는 교훈을 의도적으로 피하고 있었습니다. 지금까지 우리가 구축한 모든 것, 즉 RAG, 에이전트(agents), 다중 에이전트 파이프라인(multi-agent pipelines) 등은 저 혼자 사용자일 때 제 노트북에서 실행되는 것은 아주 잘 작동했습니다. 응답당 몇 초를 기다리는 것도 괜찮았고, 하루에 아마 50번 정도 요청을 보내는 수준이라 비용에는 신경 쓰지 않았습니다.
하지만 프로덕션 환경은 완전히 다른 동물입니다. 저는 예상치 못한 청구서를 확인하고 그 앞에서 한 분 동안 멍하니 있다가 무슨 일이 일어났는지 깨달으면서 이 사실을 알게 되었습니다. 이 글은 제가 그 순간 전에 알았으면 좋았을 모든 것, 즉 실제 수치, 실제 코드, 실제 실수에 대한 내용이므로, 여러분이 비싼 교훈을 건너뛰고 바로 유용한 정보로 넘어갈 수 있도록 돕고자 합니다.
무서운 청구서가 나오기 전의 실생활 예시
자신 가족을 위해 저녁 식사를 준비하는 것과 300명 규모의 결혼식 케이터링을 하는 것 사이의 차이점을 생각해 보세요.
네 사람을 위한 요리는 여러 가지를 임시방편으로 처리할 수 있습니다. 재료가 떨어지면 가게에 가서 사 오고, 큰일 아닙니다. 주의가 산만해져서 식사가 10분 정도 더 걸려도 아무도 신경 쓰지 않습니다. 서빙하기 전에 모든 것을 직접 맛볼 수 있으니 품질 관리는 그저 '나 자신'입니다.
하지만 300명 규모의 결혼식 케이터링은 완전히 다른 운영 방식입니다. 서비스 도중에 무언가가 떨어졌다고 해서 그냥 '가게에 갈' 수는 없습니다. 사전에 필요한 모든 재료의 양을 버퍼(buffer)를 포함하여 정확하게 계산해야 합니다. 만약 접시 하나를 꾸미는 데 10초가 더 걸린다면, 그것을 300번 반복할 경우 계획하지 못한 50분이 생기고 손님들은 배고파서 서성거리게 됩니다. 모든 접시를 나갈 때마다 직접 맛볼 수는 없으며, 시스템이 필요합니다. 문제점을 발견하는 현장 점검을 하는 수석 요리사(head chef)나, 제가 개인적으로 모든 서빙을 검사하지 않아도 문제가 포착되는 프로세스가 필요한 것입니다.
이 글에서 다루는 모든 문제는 4인분을 요리하던 것에서 300인분을 대접하는 것으로 넘어가는 것과 같은 동일한 도약의 변형들입니다. 예전에는 요청 수가 적어 중요하지 않았던 비용이, 규모가 커지면 갑자기 엄청나게 중요해집니다. 한 번은 괜찮게 느껴졌던 지연 시간 (Latency)이 수천 개의 요청이 발생하는 상황에서는 실제 병목 현상 (Bottleneck)이 됩니다. 그리고 예전에는 단순히 출력물을 눈으로 훑어보는 것만으로 충분했던 품질 관리 (Quality control)가 이제는 진정으로 실제 시스템을 필요로 하게 됩니다. 이것이 바로 이 시리즈의 지난 글이 평가 (Evals)에 관한 것이었던 이유이며, 이는 우연이 아니라 이번 글을 위한 직접적인 전제 조건입니다.
문제 1, 당신을 주저앉게 만드는 청구서
여기에 제가 개인적으로 빠졌던 함정이 있습니다. 로컬에서 테스트할 때는 하루에 50개 정도의 요청을 보냈고, 그 정도 규모에서는 약간 낭비되는 프롬프트 (Prompt)라 해도 비용이 몇 푼 되지 않았기에 정말로 눈치채지 못했습니다. 그러다 제가 만든 작은 기능에 실제 일일 사용자(Daily users)가 생겼습니다. 수백 명의 사용자가 생겼고, 각 사용자는 세션당 여러 번의 모델 호출 (Model calls)을 발생시켰습니다. 그러자 제가 내내 실행해 왔던 바로 그 "약간 낭비되는" 프롬프트가 갑자기 엄청난 금액으로 빠르게 쌓이기 시작했습니다.
해결책은 비용에 놀라기 전, 즉 사후가 아니라 사전에 실제로 얼마를 쓰고 있는지 측정하는 것부터 시작됩니다.
해결책, 토큰 사용량과 요청당 비용을 실제로 추적하세요
import anthropic
client = anthropic.Anthropic(api_key="your-api-key-here")
...
모든 호출마다 이 수치를 눈앞에 두고 확인하게 되자, 응답을 눈으로만 훑어볼 때는 보이지 않았던, 매우 쉽게 해결 가능한 몇 가지 문제들이 즉시 눈에 띄었습니다.
해결책, 필요 없는 것을 다시 보내는 것을 중단하세요
개인적으로 가장 큰 해결책은 제가 매 프롬프트(prompt)마다 얼마나 많은 반복적이고 불필요한 콘텐츠를 집어넣고 있었는지를 깨달은 것이었습니다. 만약 시스템 프롬프트(system prompt)가 길고 요청마다 거의 동일하거나, 대화의 매 호출(call)마다 동일한 참조 문서(reference documents)를 다시 보내고 있다면, 여러분은 동일한 토큰(tokens)에 대해 계속해서 전체 비용을 지불하고 있는 것입니다.
# 이전 방식, 낭비적임, RAG 스타일 설정에서
# 모든 질문마다 전체 참조 문서를 다시 보냄
def answer_wastefully(question, full_document):
...
만약 이 시리즈의 이전 RAG 관련 글을 건너뛰셨다면, 이것이 바로 해당 패턴이 중요한 또 다른 이유입니다. 이는 단순히 정확도(accuracy)의 문제만이 아니라, 요청당 비용(cost per request)을 직접적으로 제어하기 때문입니다.
해결책, 다시 질문받을 가능성이 있는 응답을 캐싱(cache)하세요
실제 트래픽의 상당 부분은 반복됩니다. 서로 다른 사용자가 동일하거나 매우 유사한 질문을 던지는 것이죠. 매번 동일한 답변을 재생성하기 위해 전체 비용을 지불하는 것은 돈을 허공에 날리는 것과 같습니다.
import hashlib
import json
...
정말로 역동적인 대화형 어시스턴트(conversational assistant)의 경우, 정확히 일치하는 반복이 드물기 때문에 이 방법의 가치는 제한적일 수 있습니다. 하지만 FAQ 스타일의 질문, 일반적인 조회(lookups), 또는 반복되는 도구 입력(tool inputs)과 같은 경우에는 캐싱(caching)만으로도 품질을 전혀 건드리지 않고 청구서의 상당 부분을 줄일 수 있습니다.
해결책, 모든 작업에 가장 비싼 모델을 사용하지 마세요
이것은 지나고 보니 당연하게 느껴졌지만, 청구서를 확인하기 전까지는 정말로 생각하지 못했던 부분입니다. 모든 작업에 가장 유능하고 가장 비싼 모델이 필요한 것은 아닙니다. 단순한 분류(classification), 기본적인 추출(extraction), 짧은 사실 조회(factual lookups) 등은 종종 더 작고 저렴한 모델에서도 완벽하게 작동하며, 이를 통해 가장 비싼 모델은 실제로 필요한 진정으로 어려운 추론(reasoning) 작업에만 아껴둘 수 있습니다.
def route_to_appropriate_model(task_type, prompt):
# 단순한 작업은 더 작고, 저렴하며, 빠른 모델로 보냄
simple_tasks = ["classification", "extraction", "simple_lookup"]
...
쉬운 작업은 더 저렴한 모델로 라우팅하고, 비싼 모델은 진정으로 어려운 작업에만 남겨두는 이 단 한 번의 변화는 제가 수행한 가장 큰 비용 절감 중 하나가 되었으며, 솔직히 실제로 구현하기 가장 쉬운 방법 중 하나입니다.
두 번째 문제: 테스트할 때는 괜찮았지만, 실제 트래픽이 몰릴 때
로컬에서 테스트하는 단일 개발자는 요청을 하나 보내고, 응답을 기다린 다음, 다음 요청을 보냅니다. 하지만 실제 프로덕션 (Production) 트래픽은 그렇게 예의 바르게 줄을 서서 기다려주지 않습니다. 수십 또는 수백 개의 요청이 거의 동시에 시스템에 도달할 수 있으며, AI API에는 분당 보낼 수 있는 최대 요청 수 또는 토큰 수인 속도 제한 (Rate limits)이 존재합니다.
해결책: 단순히 충돌하는 대신 속도 제한을 우아하게 처리하기
import time
import random
...
대기 시간의 이러한 무작위성은 보기보다 훨씬 중요합니다. 이를 지터 (Jitter)라고 부르며, 이는 특히 천둥 치는 들소 떼 문제 (Thundering herd problem)를 방지하기 위해 존재합니다. 이 문제는 수많은 요청이 한꺼번에 실패한 후 정확히 같은 순간에 모두 재시도하여, 즉시 다시 속도 제한을 유발하는 루프에 빠지는 현상을 말합니다.
해결책: 하나의 느린 요청이 다른 모든 것을 붙잡지 않도록 하기
만약 앱이 여러 사용자를 처리한다면, 하나의 느린 AI 응답이 기다리고 있는 다른 모든 사람을 위해 애플리케이션 전체를 반드시 멈추게 해서는 안 됩니다. 이것이 바로 비동기 처리 (Async handling)와 백그라운드 작업 큐 (Background job queues)가 해결하는 문제이며, 요청이 단일 차단 라인 (Blocking line)에서 하나씩 처리되는 대신 진정으로 병렬적으로 발생할 수 있게 해줍니다.
import asyncio
from anthropic import AsyncAnthropic
...
세 번째 문제: 프로덕션 환경에서 무슨 일이 일어나고 있는지 정말 모르는 경우
이것은 제가 진지하게 생각해 봤을 때 가장 무서웠던 부분입니다. 로컬에서는 문제가 생기면 터미널에 바로 보이죠. 하지만 실제 사용자, 지구 반대편, 새벽 3시에 프로덕션 환경에서 무언가가 조용히 고장 날 수 있고, 사용자가 나중에 불평하기 전까지는 알지 못할 수도 있습니다. 아니면 그냥 떠나버릴 수도 있고요.
해결책: 디버깅에 실제로 사용할 수 있도록 모든 호출을 충분한 세부 정보와 함께 로깅하세요
import logging
import json
import time
...
그 request_id는 보이는 것보다 훨씬 중요합니다. 사용자가 “뭔가 이상하게 일어났다”고 보고했을 때, 정확한 요청(request), 정확한 프롬프트(prompt), 정확한 응답(response), 정확한 타이밍을 추적할 수 있다면, 모호한 불만 사항이 단순히 어렴풋한 설명에서 무엇이 잘못되었는지 추측하는 것 대신 실제로 조사할 수 있는 무언가로 바뀝니다.
해결책: 실제 알림 시스템을 구축하고, 스스로 문제를 알아차리는 것에 의존하지 마세요
아무도 읽지 않는 파일에 쌓여있는 로그는 기본적으로 장식품일 뿐입니다. 당신이 실제로 중요하다고 판단한 임계값(threshold)을 넘어서 무언가가 발생했을 때 자동 알림을 받고 싶습니다.
def check_and_alert(recent_calls):
total = len(recent_calls)
if total == 0:
...
오류율 5%, 평균 응답 시간 8초와 같은 특정 임계값 숫자를 정하는 것은 첫날부터 완벽하게 알 수 있는 것이 아닙니다. 그것들은 당신의 특정 사용자에게 실제로 중요한 것에 기반하여 조정되어야 합니다. 하지만
네 번째 문제, 아무도 알아채지 못하는 품질의 점진적 저하
이 문제는 이 시리즈의 바로 이전 글인 평가 (evals) 관련 기사와 직접적으로 연결됩니다. 프로덕션 (production) 환경에서는 제공업체에 의해 모델이 업데이트되고, 시간이 지나면서 미세한 수정을 거치며 당신의 프롬프트 (prompt) 또한 드리프트 (drift)하며, 사용자들이 원래 설계하지 않았던 질문을 던짐에 따라 트래픽 패턴 (traffic patterns)이 변화합니다. 시스템이 충돌하거나 에러를 발생시키지는 않지만, 단순한 업타임 체크 (uptime check)로는 절대 잡아낼 수 없는 방식으로 전체 시스템이 서서히 악화됩니다.
해결책, 배포 전뿐만 아니라 라이브 트래픽에 대해 지속적으로 평가 스위트 (eval suite)를 실행하세요
import random
def sample_and_evaluate_production_traffic(recent_requests, sample_rate=0.05):
...
이것은 대부분의 팀이 실제로 건너뛰는 부분이며, 가장 조용하고 위험한 종류의 실패를 잡아내는 부분입니다. 모든 대시보드가 여전히 초록색(정상)을 나타내는 동안, 아무런 충돌도 에러도 없이 그저 서서히 나빠지는 상황 말입니다.
종합하기, 진정으로 프로덕션 준비가 된 호출 (call)
다음은 이 모든 요소들이 실제 트래픽을 믿고 맡길 수 있는 하나의 함수로 어떻게 쌓이는지를 대략적으로 보여줍니다.
def production_ready_call(prompt, user_id, system=None):
# 1, 먼저 캐시 (cache)를 확인하여 호출 자체를 완전히 절약할 수 있는지 확인
cache_key = get_cache_key(prompt, system or "")
...
여기 있는 개별 요소 중 어느 하나도 그 자체로 복잡하지 않습니다. 비용 추적 (cost tracking), 캐싱 (caching), 재시도 (retries), 로깅 (logging), 알림 (alerting), 지속적인 품질 샘플링 (continuous quality sampling) 등 각각은 진정으로 작고 이해 가능한 추가 사항들입니다. 실제로 무엇이...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기



