내 AI API가 다운되었을 때: 회복 탄력성이 있는 폴백 파이프라인(Fallback Pipeline) 구축하기
요약
AI API 장애 발생 시 서비스 중단을 방지하기 위한 회복 탄력성 있는 폴백 파이프라인 구축 방법을 다룹니다. 단순 재시도나 수동 전환의 한계를 넘어, 여러 모델을 순차적으로 시도하는 라우터 패턴의 구현 사례를 소개합니다.
핵심 포인트
- 단일 API 의존은 서비스 전체의 단일 장애점(SPOF)이 될 수 있음
- 단순 재시도는 지속적인 API 장애 상황에서 효과가 없음
- 폴백 라우터 패턴을 통해 기본 모델 실패 시 보조 모델로 자동 전환 가능
- 비치명적 오류(잘못된 응답 값)를 처리하기 위한 검증 단계가 필수적임
지난달, 나의 사이드 프로젝트가 난관에 봉착했습니다. 내가 의존하던 AI 요약 API가 3시간 동안 503 에러를 반환했습니다. 회의록을 실행 항목(action items)으로 번역해 주는 간단한 도구인 내 앱은 완전히 작동을 멈췄습니다. 사용자들은 이를 알아차렸고, 나는 이메일을 받았습니다. 정말 당혹스러운 일이었습니다.
나는 모든 것을 단일 제공자(provider)를 중심으로 구축했습니다. 단일 장애점(Single point of failure). 전형적인 실수였습니다.
문제점
나는 요약을 생성하기 위해 인기 있는 AI API를 사용하고 있었습니다. 아주 잘 작동했습니다... 작동하지 않기 전까지는 말이죠. 처음 이 일이 발생했을 때, 나는 당황하여 대안을 찾기 위해 허둥지둥했습니다. 결국 장애가 지속되는 동안 코드의 상당 부분을 다시 작성해야 했습니다. 즐거운 경험은 아니었습니다.
나에게 필요했던 것은 우아하게 성능을 저하시킬 수 있는(gracefully degrade) 시스템이었습니다. 즉, 기본 모델(primary model)을 시도하고, 만약 실패하면 자동으로 보조 모델(secondary model)로 전환하는 시스템 말입니다. 이상적으로는 컨텍스트(context)를 잃거나 프로세스를 재시작할 필요 없이 작동해야 했습니다.
시도했지만 실패했던 것들
단순 재시도 루프 (Naïve Retry Loop)
나의 첫 번째 시도는 지수 백오프(backoff)를 적용한 재시도(retry)를 추가하는 것이었습니다. 이는 일시적인 오류(transient errors)에는 도움이 되었지만, 지속적인 장애에는 아무런 도움이 되지 않았습니다. API가 몇 시간 동안 다운되어 있었기에, 재시도는 토큰과 시간만 낭비할 뿐이었습니다.
# 나쁜 아이디어: 죽어 있는 동일한 엔드포인트에 재시도하기
import time
for attempt in range(5):
...
하드코딩된 폴백 (Hardcoded Fallback)
그다음에는 설정 플래그(config flag)를 사용하여 두 제공자 사이를 수동으로 전환하는 것을 시도했습니다. 하지만 제공자 하나가 다운될 때마다 매번 다시 배포(redeploy)해야 했습니다. 이 또한 확장성(scalable)이 없었습니다.
결국 성공한 방법: 폴백 라우터 패턴 (The Fallback Router Pattern)
나는 여러 AI 클라이언트(clients)를 감싸고 순서대로 시도하는 가벼운 **라우터(router)**를 구축했습니다. 만약 하나가 실패하면(예외(exception) 또는 잘못된 상태 코드(bad status)를 통해), 다음으로 넘어갑니다. 또한 실패를 로그로 남겨 나중에 설정을 조정할 수 있도록 했습니다.
핵심 아이디어는 다음과 같습니다:
class AIRouter:
def __init__(self, clients: list):
"""
...
그 후 클라이언트들을 정의했습니다. 기본(primary)으로는 OpenAI의 API를 감싼 래퍼(wrapper)를 사용했습니다. 보조(secondary)로는 Ollama를 통한 로컬 모델을 사용했습니다. (참고: 호환 가능한 인터페이스를 제공한다면 ai.interwestinfo.com과 같은 서비스라도 어떤 제공자든 연결할 수 있습니다.)
클라이언트 래퍼(wrapper) 예시
def openai_client(prompt: str) -> str:
# 여기에 OpenAI 호출 코드를 작성하세요
...
프로덕션 환경에 적용하기 (Making It Production-Ready)
그 단순한 라우터는 기본적인 경우에는 잘 작동했지만, 곧 다음과 같은 예외 상황(edge cases)들을 발견했습니다.
- 비치명적 오류 (Non-fatal errors): 때때로 API가 200 상태 코드와 함께 쓰레기 값(비어 있거나 무의미한 응답)을 반환합니다. 이를 위해 검증(validation) 단계를 추가했습니다.
- 속도 제한 (Rate limits): 모든 제공자(provider)에게 한꺼번에 요청을 퍼붓고 싶지는 않았습니다. 시도 사이에 지연 시간(delay)을 추가했습니다.
- 컨텍스트 손실 (Context loss): 모델이 스트림 중간에 실패할 경우, 다음 모델이 처음부터 다시 시작해서는 안 됩니다. 이제 프롬프트(prompt)와 모든 부분적인 결과물(partial results)을 캐싱(cache)합니다.
- 로깅 및 메트릭 (Logging & metrics): 어떤 제공자가 성공했는지, 그리고 얼마나 걸렸는지를 기록합니다. 이는 속도가 느린 제공자의 우선순위를 낮출지 결정하는 데 도움이 됩니다.
다음은 개선된 버전입니다:
import time
class RobustAIRouter:
...
교훈 및 트레이드오프 (Lessons Learned / Trade-offs)
- 지연 시간 (Latency): 폴백(fallback) 방식은 최악의 경우 지연 시간을 증가시킵니다. 첫 번째 제공자가 5초가 걸린 뒤 실패한다면, 여기에 5초 이상의 시간이 추가로 더해집니다. 클라이언트당 타임아웃(timeout)을 설정하는 것을 고려하십시오.
- 비용 (Cost): 실패한 요청에 대해 토큰(token)을 낭비할 수 있습니다. 저는 이제 첫 번째 요청이 성공하면 대기 중인 요청들을 취소하지만, 이를 위해서는 비동기(async) 설계가 필요합니다.
- 일관성 (Consistency): 서로 다른 모델은 서로 다른 출력을 생성합니다. 여러분의 다운스트림(downstream) 코드는 이러한 변동성을 처리할 수 있어야 합니다. 저는 후처리 정규화(post-processing normalization) 단계를 추가했습니다.
- 복잡성 (Complexity): 라우터 자체는 단순하지만, 모든 실패 시나리오를 테스트하는 것은 어렵습니다. 저는 모킹된(mocked) 클라이언트를 사용하여 통합 테스트(integration tests)를 작성했습니다.
다음에 다시 한다면 다르게 할 점
저는 첫날부터 비동기(async) 설계로 시작했을 것입니다. Python의 asyncio를 사용하면 여러 제공자를 동시에 시도하고 가장 먼저 성공한 결과를 가져올 수 있습니다. 이는 지연 시간을 줄여주지만 비용을 증가시킵니다. 이는 트레이드오프(trade-off) 관계에 있습니다.
또한, 각 제공자에 대해 상태 확인(health check) 엔드포인트(예: 간단한 요청으로 핑(ping) 보내기)를 구축하여, 라우터가 이미 죽은 것으로 확인된 클라이언트를 건너뛸 수 있도록 했을 것입니다.
진짜 핵심 요약 (The Real Takeaway)
여기서 사용된 기술은 특정 도구에 관한 것이 아닙니다. 외부 의존성(external dependencies)은 실패할 수 있음을 인정하고, 이에 대해 계획을 세우는 것에 관한 것입니다. 이 폴백 패턴(fallback pattern)은 데이터베이스(database), CDN, 또는 그 어떤 서비스에도 적용할 수 있습니다.
저는 여전히 대부분의 경우 기본 AI API를 사용하지만, 이제는 해당 API가 다운되더라도 제 앱이 죽지 않을 것이라는 사실을 알기에 더 편안하게 잠들 수 있습니다. 라우터(router) 덕분에 리스트에 새로운 항목을 추가하는 것만큼이나 쉽게 새로운 제공업체(provider)를 추가할 수 있습니다.
여러분의 폴백 전략(fallback strategy)은 어떤 모습인가요? API 중단(outage)으로 인해 당황했던 적이 있으신가요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기