AI API가 다운되었을 때: 실전 폴백 전략 (Fallback Strategy)
요약
AI API 장애 발생 시 서비스 중단을 방지하기 위한 실전 폴백(Fallback) 전략을 다룹니다. 단순한 예외 처리를 넘어 가중치 기반 라우팅, 지수 백오프, 서킷 브레이커를 활용한 안정적인 시스템 구축 방법을 제안합니다.
핵심 포인트
- 단일 API 의존성을 탈피하여 멀티 제공업체 환경 구축 필요
- 가중치 기반 라운드 로빈을 통한 지능적 트래픽 분산
- 지터(Jitter)를 포함한 지수 백오프로 재시도 효율화
- 서킷 브레이커 패턴을 통한 장애 전파 차단
두 달 전, 사용자들이 제 앱과 대화하던 도중 AI API 제공업체로부터 503 에러가 발생하는 것을 지켜보고 있었습니다. 세션은 끊겼고, 로그는 온통 빨간색으로 가득 찼으며, 제 휴대폰은 화가 난 사용자들의 메시지로 진동하고 있었습니다. 그때 저는 뼈아픈 교훈을 얻었습니다. 단 하나의 AI API에만 의존하는 것은 마치 하나의 말뚝 위에 집을 짓는 것과 같습니다.
저는 한동안 챗봇, 요약, 콘텐츠 생성 등 AI 기반 기능을 구축해 왔습니다. 많은 이들과 마찬가지로 저도 OpenAI의 API로 시작했습니다. 대부분의 경우 신뢰할 수 있고 품질도 훌륭합니다. 하지만 사용자들이 24시간 가용성을 기대하는 프로덕션(Production) 환경에서는 "대부분의 경우"라는 말로는 충분하지 않습니다.
문제점 (The Problem)
제 앱은 실시간 응답 생성을 위해 GPT-4를 사용하고 있었습니다. OpenAI에 부분적인 장애가 발생하기 전까지는 모든 것이 잘 작동했습니다. 요청이 타임아웃(Timeout)되기 시작하더니, 결국 실패했습니다. 한 번 시도하고 에러를 보여주는 저의 순진한 접근 방식은 사용자를 막다른 길에 가두었습니다. 저는 서둘러 다른 제공업체로 전환하려고 했지만, 코드를 수동으로 업데이트하고 다시 배포해야 했습니다. 그 과정은 한 시간이 걸렸습니다. 한 시간 동안의 다운타임(Downtime)이었습니다.
저는 여러 AI 제공업체에 걸쳐 장애를 자동으로 처리하면서, 폴백(Fallback), 재시도(Retries), 그리고 이상적으로는 비용 균형(Cost balancing)을 맞출 수 있는 시스템이 필요했습니다. 품질을 잃고 싶지는 않았지만, 저렴한 모델이 대부분의 경우 잘 작동한다고 해서 파산하고 싶지도 않았습니다.
처음 시도했던 것 (What I Tried First)
저의 첫 번째 시도는 간단했습니다. 제공업체 A를 시도하고, 실패하면 제공업체 B를 시도하는 것이었습니다. 리스트를 하드코딩하고 try-except 블록을 사용했습니다.
import openai
import anthropic
...
이것은 아무것도 안 하는 것보다는 나았지만, 심각한 결함들이 있었습니다:
- 일시적인 에러(Transient errors)에 대한 재시도(Retries)가 없음.
- 단일한 폴백 순서에 고정됨 — A가 다운되면 B가 모든 부하를 떠맡게 되는데, 만약 B마저 실패한다면 어떻게 될까요?
- 타임아웃(Timeouts) 없음: 느린 제공업체가 전체 시스템을 중단시킬 수 있음.
- 실패율에 대한 통찰력 없음; 저는 눈을 감고 비행하는 것과 같았습니다.
결국 성공한 방법: 가중치 기반 멀티 제공업체 라우터 (A Weighted Multi-Provider Router)
결국 저는 세 가지 기능을 수행하는 작은 Python 라이브러리를 구축하게 되었습니다:
- 가중치 기반 라운드 로빈 선택 (Weighted round-robin selection) – 제공자에게 가중치를 할당합니다 (예: GPT-4에 3, Claude에 1, 무료 모델에 1). 요청은 비례적으로 분산되지만, 특정 제공자가 반복적으로 실패하면 해당 제공자의 가중치는 일시적으로 감소합니다.
- 지터(Jitter)를 포함한 지수 백오프 (Exponential backoff with jitter) – 실패한 요청을 지연 시간을 늘려가며 재시도하되, 천둥 치는 들판(thundering herd) 문제를 방지하기 위해 무작위성을 부여합니다.
- 서킷 브레이커 (Circuit breaker) – 특정 제공자가 Y초 동안 X번 실패하면, 냉각 기간(cooldown period) 동안 해당 제공자에게 요청을 보내는 것을 중단합니다.
다음은 핵심적인 내용만 추린 접근 방식의 핵심 코드입니다:
import asyncio
import random
import time
...
이를 사용하려면 실제 API 호출을 비동기 함수(async functions)로 래핑하면 됩니다:
async def call_openai(prompt: str) -> str:
# 실제 구현부
...
...
또한 메트릭(metrics) 기능도 추가했습니다. 모든 성공/실패를 간단한 Prometheus 카운터(counter)와 히스토그램(histogram)에 기록합니다. 이를 통해 가중치를 조정할 수 있는 실제 데이터를 얻을 수 있었습니다.
교훈 / 트레이드오프 (Lessons Learned / Trade-offs)
- 품질 vs 비용 (Quality vs. cost): GPT-4에 더 높은 가중치를 부여함으로써 높은 품질을 유지했습니다. 하지만 GPT-4가 느려지면 라우터가 더 저렴한 모델을 사용하게 되어 비용을 절감할 수 있었습니다. 트레이드오프는 장애 발생 시 간혹 품질이 낮은 응답이 나올 수 있다는 점입니다.
- 서킷 브레이커 튜닝 (Circuit breaker tuning): 너무 민감하면(임계값이 낮으면) 너무 자주 전환되어 컨텍스트(context)를 잃게 됩니다. 반대로 너무 관대하면 작동하지 않는 제공자에게 계속 요청을 보내게 됩니다. 저는 60초 동안 3번의 실패를 기준으로 결정했습니다.
- 멱등성 (Idempotency): 라우터는 정확히 한 번(exactly-once) 전달을 보장하지 않습니다. 요청이 타임아웃되었지만 실제로는 성공했다면, 다운스트림(downstream)에서 중복 데이터를 받을 수 있습니다. 이 부분은 사용자 측에서 처리해야 합니다.
- 디버깅의 어려움 (Debugging is harder): 응답이 이상할 경우, 이제 어떤 제공자가 해당 응답을 제공했는지 확인해야 합니다. 저는 응답에
X-Provider헤더를 추가했습니다.
다음에 다시 한다면 다르게 할 점
저는 간단한 폴백 (Fallback)부터 시작하여, 전체 라우터 (Router)를 구축하기 전에 메트릭 (Metrics)을 먼저 추가했을 것입니다. 서킷 브레이커 (Circuit Breaker)와 가중치 (Weights)는 실제 장애 패턴을 목격하며 얻은 결과였습니다. 또한, 이를 대신 처리해 주는 호스팅 서비스를 사용하는 것도 고려해 볼 만합니다. ai.interwestinfo.com과 같이 몇 가지 서비스가 존재합니다 (비록 제가 직접 사용해 본 것은 아니지만). 직접 구축하든 구매하든 기술적인 원리는 동일합니다.
하지만 현재 저의 라우터는 수동 개입 없이 하루 10,000개 이상의 요청을 처리하고 있습니다. 6시간 동안 지속되었던 단 한 번의 장애 상황에서도, 사용자는 거의 알아채지 못했습니다. 라우터가 조용히 Anthropic으로 전환한 뒤, 다시 로컬 모델 (Local Model)로 전환했기 때문입니다.
핵심 요약 (The Real Takeaway)
회복 탄력성 (Resilience)은 장애를 제거하는 것이 아니라, 장애로부터 우아하게 살아남는 것에 관한 것입니다. 스마트한 폴백 전략은 구현 비용이 저렴하며, 기본 API가 다운되는 첫 순간에 그 가치를 충분히 증명합니다. 화난 사용자들의 연락으로 전화가 울릴 때까지 기다리지 마세요.
AI API 장애에 대비한 여러분의 백업 플랜은 무엇인가요? 단순한 폴백, 멀티 프로바이더 (Multi-provider), 혹은 완전히 다른 방식인지 여러분의 설정에 대해 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기