AI API가 기대에 미치지 못할 때: 회복 탄력성이 있는 프록시 계층(Proxy Layer) 구축하기
요약
AI API의 속도 제한(Rate Limit)과 타임아웃 문제를 해결하기 위해 Python asyncio 기반의 경량 프록시 계층을 구축하는 방법을 소개합니다. 큐잉, 속도 제한 관리, 폴백(Fallback) 전략을 통해 시스템의 회복 탄력성을 높이는 실무적인 접근법을 다룹니다.
핵심 포인트
- 단순 재시도(Backoff)는 시스템 과부하 시 오히려 독이 될 수 있음
- 우선순위 큐와 토큰 버킷 방식을 활용한 요청 관리 필요
- API 장애 시 로컬 모델이나 다른 제공업체로 전환하는 폴백 전략 구축
- asyncio와 aiohttp를 이용한 비동기 프록시 구현의 효율성
하지만 제대로 이루어지지 않았습니다. 처음 몇 주 동안은 괜찮았지만, 사용량이 늘어나면서 한계에 부딪혔습니다. 무작위적인 429 Rate Limit(속도 제한), 산발적인 503 오류, 그리고 무엇보다 최악이었던 것은 전체 배치를 중단시켜 버리는 무음 타임아웃(silent timeouts)이었습니다. 제 사용자들(좋아요, 제 팀원 두 명입니다)은 요약이 누락되거나 느리다고 불평하기 시작했습니다.
애플리케이션 전체를 다시 작성하지 않고도 API 호출을 신뢰할 수 있게 만드는 방법이 필요했습니다. 제가 시도했던 것들, 실패한 것들, 그리고 결국 성공했던 방법을 소개합니다.
효과가 없었던 방법들 (막다른 길)
지수 백오프(Exponential Backoff)를 이용한 재시도
물론 tenacity나 단순한 time.sleep() 루프가 일시적인 오류에는 도움이 되지만, 시스템적인 과부하 문제를 해결하지는 못합니다. 어느 날은 AI 제공업체의 백엔드 자체가 모든 사용자에게 느려졌는데, 이때 재시도를 하는 것은 도움이 되지 않았고 오히려 대기열(queue)에 부하만 더했습니다.
제공업체 교체
몇 가지 대안을 시도해 보았습니다. 각 업체마다 고유한 API 특성, 서로 다른 가격 체계, 그리고 서로 다른 장애 패턴이 있었습니다. 하나는 더 저렴했지만 긴 텍스트에 대해 최악의 지연 시간(latency)을 보였습니다. 결국 저는 두 개의 별도 통합 경로를 유지하게 되었고, 여전히 통합된 신뢰성 전략은 갖추지 못한 상태였습니다.
요청 배치(Request Batching)
일부 API는 배칭(batching)을 지원하지만, 배치 제한(예: 20개 항목)이 낮고 단 하나의 느린 항목이 전체 배치를 지연시킬 수 있습니다. 게다가 배칭은 모든 요청을 미리 수집해야 하므로, 스트리밍(streaming)이나 실시간에 가까운 시나리오에는 적합하지 않습니다.
성공한 접근 방식: 경량 프록시 + 큐 계층 (Lightweight Proxy + Queue Layer)
각 API와 직접 싸우는 대신, 저는 애플리케이션과 AI 서비스 사이에 위치하는 작은 Python asyncio 프록시를 구축했습니다. 이 프록시는 세 가지 역할을 수행합니다:
- 요청 큐잉(Queues requests): 우선순위 힙(priority heap)과 타임아웃을 사용하여 요청을 대기열에 넣습니다.
- 속도 제한 관리(Manages rate limits): API 엔드포인트별로 속도 제한을 관리합니다 (토큰 버킷(token bucket) 방식).
- 폴백(Fallback) 제공: 재시도 후에도 기본 API가 실패하면 보조 모델(예: 더 작은 로컬 모델 또는 다른 제공업체)을 시도합니다.
이것이 혁명적인 것은 아닙니다. 메시지 큐(message queue)와 결합된 표준적인 서킷 브레이커(circuit breaker) 패턴입니다. 하지만 소규모 팀에게 있어 이를 처음부터 구현하는 데는 주말 이틀이 걸렸고, 우리의 대부분의 골칫거리를 해결해 주었습니다.
코드: 핵심 프록시 (Core Proxy)
다음은 asyncio와 aiohttp를 사용한 단순화된 핵심 구조입니다. 이 버전은 단일 AI API를 처리하지만, 여러 백엔드(backends)로 확장할 수 있습니다.
import asyncio
import aiohttp
import time
...
사용법:
proxy = AIProxy(api_key="sk-...", base_url="https://ai.interwestinfo.com")
result = await proxy.query("Summarize this article: ...")
폴백 모델 (Fallback Model) 추가하기
기본 API가 지속적으로 실패하거나 저품질의 응답을 반환하는 경우, transformers를 통해 로컬 모델을 통합할 수 있습니다. 다음은 외부 API가 다운되었을 때 더 작은 모델(예: DistilBART)을 실행하는 간단한 폴백(fallback) 예시입니다.
from transformers import pipeline
class FallbackSummarizer:
...
그 다음 메인 핸들러(main handler)에서 다음과 같이 사용합니다:
async def get_summary(text):
try:
result = await proxy.query(f"Summarize: {text}")
...
트레이드오프 (Trade-offs) 및 교훈
- 지연 시간(Latency) vs 신뢰성(Reliability): 프록시는 호출당 약 50ms의 오버헤드를 추가하지만, 이는 2
5초에 달하는 API 왕복 시간(round trip)에 비하면 무시할 수 있는 수준입니다. 폴백 모델은 로컬에서 310배 더 느리게 작동하지만, 적어도 작업이 완전히 실패하지는 않습니다. - 비용(Cost): CPU에 폴백 모델을 로드해 두는 것은 추가 비용이 들지 않지만, GPU 사용량은 누적됩니다. 저는 CPU 전용 서버를 사용했기 때문에 로컬 모델의 요약은 더 느리지만 더 저렴합니다.
- 이 방식을 사용하지 말아야 할 때: 사용자 요청당 단 한 번의 API 호출만 발생하고 다운타임(downtime)이 허용 가능한 수준이라면, 단순한 재시도 루프(retry loop)만으로도 충분합니다. 프로토타입 단계에서 프록시 계층은 과할 수 있습니다. 하지만 하루 호출량이 100회를 넘거나 사용자가 여러 명인 경우에는 충분한 가치가 있습니다.
다음에 다시 한다면 다르게 할 점
수동 큐(queue) 구현은 건너뛰고 Redis Streams와 같은 기존 메시지 브로커(message broker)나 asyncio.Queue의 간단한 비동기 큐(async queue)를 사용할 것입니다. 또한, 첫날부터 구조화된 로깅(structured logging)을 추가할 것입니다. 로그 없이 비동기 프록시 체이닝(async proxy chaining)을 디버깅하는 것은 매우 고통스럽기 때문입니다.
폴백(fallback)의 경우, 로컬 모델 대신 더 저렴하고 빠른 API를 보조(secondary) 수단으로 사용하는 것을 고려하겠습니다. 로컬 모델은 오프라인 회복 탄력성(offline resilience) 측면에서는 훌륭하지만, 품질 격차가 여전히 눈에 띄기 때문입니다.
핵심 요약 (The Takeaway)
회복 탄력성이 있는 프록시 계층(proxy layer)을 구축하는 것은 끊임없는 화재 진압(fire-fighting) 상황으로부터 저를 구해준 주말 프로젝트였습니다. 반드시 복잡할 필요는 없습니다. asyncio를 활용한 수백 줄의 Python 코드와 폴백(fallback) 기능만으로도 불안정한 API를 신뢰할 수 있는 백엔드(backend)로 바꿀 수 있습니다.
사용하는 정확한 도구(OpenAI, Anthropic 또는 Interwest AI와 같은 것)보다는 패턴이 더 중요합니다. 단순하게 시작하여 속도 제한(rate limiting)을 추가하고, 그다음 폴백(fallback)을 추가하면 훨씬 마음 편히 잠들 수 있을 것입니다.
API 장애를 처리하기 위해 여러분은 어떤 설정을 사용하시나요? 제공업체의 기본 재시도(native retries) 기능에 의존하시나요, 아니면 직접 계층(layer)을 구축하셨나요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기