내 AI 챗봇이 계속 타임아웃(Timeout)이 발생했던 이유와 해결 방법
요약
LLM API 호출 시 발생하는 타임아웃과 Rate-limit 문제를 해결하기 위한 실전적인 엔지니어링 접근법을 다룹니다. 단순 재시도를 넘어 서킷 브레이커와 폴백 모델을 활용한 3단계 계층 구조 설계 방법을 소개합니다.
핵심 포인트
- API 호출 시 지수 백오프를 포함한 재시도 전략 적용
- 서킷 브레이커를 통한 실패 엔드포인트 차단 및 시스템 보호
- 기본 모델 장애 시 백업 모델로 전환하는 폴백 메커니즘 구축
- API의 불완전성을 전제로 한 방어적 프로그래밍의 중요성
사내 도구용 챗봇을 만들고 있었습니다. 아이디어는 간단했습니다. 사용자가 질문을 하면, 봇이 LLM API를 호출하고 응답을 반환하는 것이었죠. 무엇이 잘못될 수 있었을까요?
알고 보니, 아주 많은 것들이 잘못될 수 있었습니다. API가 무작위로 타임아웃(Timeout)이 발생하거나, 429 에러(Too Many Requests)를 반환하거나, 혹은 연결 오류를 내뱉기 전까지 30초 동안 그냥 멈춰 있기도 했습니다. 제 봇은 좌절감을 주는 회전하는 로딩 바가 되어버렸습니다.
처음에는 단순히 API 제공업체가 별로라고 생각했습니다. 다른 업체를 써봤지만 문제는 동일했습니다. 그러다 깨달았습니다. 문제는 제공업체가 아니라, 제가 그들을 호출하는 방식에 있었다는 것을요.
시도했던 것들 (그리고 왜 실패했는가)
먼저 루프(Loop) 안에서 단순한 requests.post()를 사용하는 것으로 시작했습니다. 실패하면 한 번 더 재시도(Retry)하도록 했습니다. 그것은 어리석은 일이었습니다. 속도 제한(Rate-limit) 에러가 발생하면 즉시 다시 실패할 것이기 때문입니다.
다음으로 time.sleep()을 사용하여 지수 백오프(Exponential backoff)를 추가했습니다. 어느 정도 도움이 되었지만, 만약 API가 실제로 몇 분 동안 다운된다면, 봇은 사용자가 떠날 때까지 계속 재시도하며 그 자리에 머물러 있을 뿐이었습니다.
Python의 requests 세션 풀링(Session pooling)을 사용하는 것도 시도해 보았습니다. 더 나아졌지만, 서버에 과부하가 걸리면 동일한 요청들이 여전히 멈춰 있었습니다. 진짜 문제는 "이 모델이 과부하 상태라면, 다른 모델을 시도하라"라고 말할 방법이 없었다는 점이었습니다.
결국 성공한 방법
저에게는 세 가지 계층의 접근 방식이 필요했습니다:
- 백오프를 포함한 재시도 (Retry with backoff) – 속도 제한(Rate limits)과 일시적인 오류(Transient errors)를 존중합니다.
- 서킷 브레이커 (Circuit breaker) – 실패하는 엔드포인트(Endpoint)를 한동안 계속 두드리는 것을 중단합니다.
- 다른 모델로의 폴백 (Fallback to a different model) – 기본 API가 다운되면 백업(아마도 더 저렴하고 작은 모델)으로 전환합니다.
저는 모든 LLM 호출을 감싸는(Wrap) 작은 Python 클래스를 만들었습니다. 그 핵심은 다음과 같습니다:
import time
import random
from functools import wraps
...
이 클래스는 API 호출을 감쌉니다. 한 번 인스턴스화(Instantiate)한 뒤 chat(messages)를 호출하면 됩니다. 만약 기본 엔드포인트가 연속으로 세 번 실패하면, 서킷(Circuit)을 열고(Open) 30초 동안 모든 요청을 폴백(Fallback)으로 라우팅합니다. 그 후, 기본 엔드포인트에 핑(Ping)을 보내 확인하며, 복구되면 서킷을 닫습니다(Close).
사용 방법
client = LLMClient(BASE_URL, FALLBACK_URL)
# 일반적인 사용 — 실패에 대해 고민할 필요 없음
...
교훈 (Lessons Learned)
- 어떤 API도 100% 신뢰할 수 있다고 가정하지 마세요. 유명한 서비스들도 다운될 수 있습니다. 미리 대비책을 세워두세요.
- 무한 재시도 (Endless retries)보다는 서킷 브레이커 (Circuit breaker)가 더 낫습니다. 서킷 브레이커는 API가 회복할 시간을 주고, 여러분의 애플리케이션이 응답성을 유지할 수 있게 해줍니다.
- 폴백 모델 (Fallback models)이 반드시 완벽한 대체재일 필요는 없습니다. 저는 폴백용으로 성능이 약간 낮은 모델을 사용하지만, 대부분의 쿼리에는 충분합니다. 로컬 모델 (Local model)을 보유하고 있다면 그것으로 폴백할 수도 있습니다.
- 제가 사용 중인 도구 (Interwest AI)는 실제로 가동 시간 (Uptime)이 매우 뛰어납니다. 제가 겪은 문제들은 제 코드 자체가 잘못되었을 때만 발생했습니다. 적절한 재시도 (Retry) 및 폴백 (Fallback) 로직을 구현한 후, 제 챗봇은 매우 견고해졌습니다.
트레이드오프 (Trade-offs) 및 사용하지 말아야 할 경우
이 방식은 복잡성을 증가시킵니다. 단순히 프로토타입을 만드는 단계라면 단순한 재시도 (Retry)만으로도 충분합니다. 또한, 스트리밍 (Streaming)과 같이 실시간 응답이 필요한 경우에는 이 패턴의 비동기 (Asynchronous) 버전이 필요할 것입니다. 서킷 브레이커를 사용한다는 것은 일정 기간 동안 더 성능이 낮은 모델을 제공할 수도 있음을 의미하며, 이는 서비스 수준 협약 (SLA)에서 항상 최상급의 응답을 요구하는 경우에는 허용되지 않을 수 있습니다.
또 다른 주의 사항은 폴백 엔드포인트 (Fallback endpoints)의 API가 서로 다를 수 있다는 점입니다. 저는 여기서 동일한 페이로드 (Payload) 구조를 하드코딩했지만, 실제로는 제공업체 간의 파라미터 (Parameters) 매핑이 필요할 것입니다.
다음에 다시 한다면 다르게 할 점
챗봇 코드에 임시방편으로 끼워 넣는 대신, 처음부터 재사용 가능한 라이브러리로 구축했을 것입니다. 또한 메트릭 (Metrics)을 추가하여 모든 폴백 이벤트를 로그로 남김으로써, 기본 모델이 어려움을 겪는 시점을 모니터링할 수 있도록 했을 것입니다. 그리고 이벤트 루프 (Event loop)가 차단되는 것을 방지하기 위해 처음부터 비동기 (Async) 방식을 구현했을 것입니다.
만약 처음부터 다시 시작한다면, 재시도를 위해 tenacity 같은 라이브러리를 사용하고 이를 간단한 폴백 데코레이터 (Fallback decorator)와 결합했을 것입니다. 하지만 현재로서는 이 클래스가 제 역할을 충분히 해주고 있습니다.
함께 논의해 봅시다
여러분의 AI 애플리케이션에서는 API 장애를 어떻게 처리하시나요? 서킷 브레이커 패턴 (Circuit breaker pattern)을 사용하시나요, 아니면 인프라를 그대로 신뢰하시나요? 댓글을 통해 여러분의 경험담을 들려주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기