첫 번째 프로덕션 장애를 겪기 전에 가졌더라면 좋았을 LLM API 장애 대응 정책
요약
LLM API 장애 발생 시 단순한 HTTP 에러 핸들링을 넘어선 체계적인 장애 대응 정책의 필요성을 다룹니다. 에러 코드의 의미를 분석하여 재시도, 모델 전환, 기능 저하 등 구체적인 조치 사항을 결정하는 '장애 정책(failure policy)' 계층 구축을 권장합니다.
핵심 포인트
- 단순 HTTP 에러 핸들링은 실제 프로덕션 장애 대응에 한계가 있음
- 429 에러도 요청 수, 토큰 수, 할당량 등 원인에 따라 대응 방식이 달라야 함
- SDK는 에러를 감지할 뿐, 시스템의 다음 행동을 결정하지 않음
- 제공자별 에러를 내부 조치 사항으로 매핑하는 '장애 정책' 계층이 필요함
대부분의 LLM API 에러 핸들링 (error handling)은 일반적인 HTTP 에러 핸들링과 비슷하게 시작됩니다.
만약 429라면, 나중에 다시 시도하세요.
만약 500이라면, 백오프 (backoff)를 적용하여 다시 시도하세요.
만약 타임아웃 (timeout)이라면, 한 번 정도 다시 시도해 볼 수 있습니다.
이 방식은 첫 번째 실제 프로덕션 장애를 겪기 전까지는 잘 작동합니다. 하지만 장애가 발생하면 질문은 더 이상 다음과 같지 않습니다.
"API가 에러를 반환했는가?"
질문은 다음과 같이 변합니다.
"이제 시스템이 무엇을 해야 하는가?"
바로 이 지점에서 일반적인 HTTP 에러 핸들링은 너무 얕게 느껴지기 시작합니다.
문제는 에러 코드가 아니다
LLM 제공업체로부터 받는 429는 제공업체, 계정, 모델, 그리고 워크로드 (workload)에 따라 매우 다른 의미를 가질 수 있습니다.
당신은 다음 중 하나에 직면했을 수 있습니다:
- 분당 요청 수 (requests per minute)
- 분당 토큰 수 (tokens per minute)
- 일일 할당량 (daily quota)
- 동시성 제한 (concurrency limits)
- 제공업체 측의 일시적인 스로틀링 (throttling)
- 모델별 용량 제한 (model-specific capacity limit)
이것들은 서로 동일한 운영상의 문제가 아닙니다.
만약 분당 요청 수 제한에 걸렸다면, 요청 빈도를 늦추는 것이 도움이 될 수 있습니다.
만약 분당 토큰 수 제한에 걸렸다면, 동일한 대규모 프롬프트 (prompt)를 즉시 다시 시도하는 것은 아마도 무용지물일 것입니다.
만약 제공업체가 과부하 상태라면, 공격적으로 재시도하는 것은 장애를 더 악화시킬 수 있습니다.
만약 당신의 자체 큐 (queue)가 쌓이고 있다면, 모델을 전환하는 것은 실제 문제가 계속 커지는 동안 증상만 숨기는 결과가 될 수 있습니다.
HTTP 상태 코드는 실패의 광범위한 분류를 알려줄 뿐, 올바른 대응 방법을 알려주지는 않습니다.
SDK 에러 핸들링은 장애 대응 정책이 아니다
대부분의 SDK는 요청을 보내고 예외 (exception)를 포착할 수 있도록 설계되었습니다.
그것은 유용하지만, 프로덕션 정책 (production policy)과는 다릅니다.
SDK는 다음과 같은 내용을 알려줄 수 있습니다:
- 요청이 실패했다
- 제공업체가
429를 반환했다 - 제공업체가
529를 반환했다 - 요청이 타임아웃 되었다
- 응답 본문 (response body)에 특정 에러 형태가 있다
하지만 당신의 애플리케이션은 여전히 다음을 결정해야 합니다:
- 이것을 재시도해야 하는가?
- 요청을 큐에 넣어야 하는가?
- 모델을 전환해야 하는가?
- 기능을 저하시켜야 (degrade) 하는가?
- 사용자에게 특정 메시지를 보여줘야 하는가?
- 누군가에게 페이지를 넘겨야 (page) 하는가?
- 이 작업이 부수 효과 (side effects)를 일으키기 때문에 재시도를 중단해야 하는가?
그러한 의사결정 트리 (decision tree)는 보통 SDK에 들어있지 않습니다.
많은 팀에서, 그것은 첫 번째 고통스러운 장애를 겪은 후 누군가가 작성한 런북 (runbook)에 들어있습니다.
그것이 아무것도 없는 것보다는 낫지만, 격차 (gap)를 만들어냅니다. 코드는 에러를 가장 먼저 감지하는 반면, 런북은 나중에 인간이 무엇을 해야 하는지를 설명하기 때문입니다.
장애 정책 (failure policy)은 그 사이의 계층 (layer)입니다.
장애 정책 (failure policy)의 의미
장애 정책이란 LLM 클라이언트 근처에 위치하며, 제공자별 (provider-specific) 에러를 내부 장애 카테고리 및 조치 사항으로 매핑하는 작은 계층입니다.
애플리케이션 전반에 다음과 같이 로직을 퍼뜨리는 대신:
if (error.status === 429) {
retry();
}
저는 다음과 유사한 방식을 선호합니다:
const decision = classifyLlmFailure(error, requestContext);
if (decision.action === "retry") {
...
중요한 점은 정책이 단순한 라벨 (label)이 아니라 의사결정 (decision)을 반환한다는 것입니다.
유용한 장애 정책은 보통 네 가지 질문에 답합니다:
- 무슨 일이 일어났는가?
- 우리의 확신도 (confidence)는 어느 정도인가?
- 시스템이 가장 먼저 무엇을 해야 하는가?
- 이 상황이 지속된다면 인간이 무엇을 확인해야 하는가?
작은 예시
다음은 단순화된 JavaScript 예시입니다.
이것은 모든 제공자나 SDK를 다루기 위한 것이 아닙니다. 핵심은 이 계층의 형태 (shape)입니다.
function classifyLlmFailure(error, context = {}) {
const status = error.status || error.response?.status;
const headers = normalizeHeaders(error.headers || error.response?.headers || {});
...
정확한 카테고리는 애플리케이션마다 다를 것입니다.
유용한 부분은 특정 코드가 아닙니다. 제공자의 에러를 내부적인 운영 의사결정 (operational decisions)으로 전환하는 습관입니다.
에러뿐만 아니라 정책 결정 사항을 로그로 남기세요
장애 발생 시, 가공되지 않은 스택 트레이스 (stack trace)만으로는 충분한 경우가 거의 없습니다.
저는 다음과 같은 질문에 답할 수 있는 하나의 로그 이벤트 (log event)를 원합니다:
- 어떤 제공자 (provider)가 실패했는가?
- 어떤 모델 (model)이 사용되었는가?
- 어떤 상태 코드 (status code)가 반환되었는가?
- 어떤 가공되지 않은 응답 헤더 (raw response headers)가 중요했는가?
- 입력 및 출력 토큰 (input and output tokens)이 각각 얼마나 추정되었는가?
- 어떤 장애 카테고리 (failure category)를 할당했는가?
- 시스템이 어떤 조치 (action)를 취했는가?
- 인간이 어떤 런북 (runbook)을 열어봐야 하는가?
이와 같은 로그 라인은 "LLM request failed"라는 메시지보다 훨씬 더 유용합니다:
logger.warn("llm_request_failed", {
provider: "example-provider",
model: "example-model",
...
목표는 간단합니다. 온콜 (on-call) 담당자가 이 문제가 요청 제한 (request-rate) 문제인지, 토큰 제한 (token-rate) 문제인지, 제공자 과부하 (provider overload)인지, 타임아웃 (timeout)인지, 아니면 잘못된 설정 (bad config)인지 파악하는 데 20분을 허비하지 않도록 하는 것입니다.
장애 발생 시 첫 번째로 던져야 할 유용한 질문이 "이것은 어떤 종류의 실패인가?"라면, 로그는 그 질문에 직접적으로 답할 수 있어야 합니다.
모든 재시도 가능한 오류를 동일한 방식으로 재시도하지 마세요
이 지점에서 많은 LLM API 코드가 지나치게 낙관적으로 작성됩니다.
"재시도 가능함 (Retryable)"이 "즉시 재시도"를 의미하는 것은 아닙니다.
분당 토큰 제한 (token-per-minute limit), 분당 요청 제한 (request-per-minute limit), 타임아웃 (timeout), 그리고 제공자 과부하 (provider overload)는 모두 재시도가 가능할 수 있지만, 동일한 재시도 동작 (retry behavior)을 공유해서는 안 됩니다.
예를 들어:
rate_limit_requests: 리셋 윈도우 (reset window) 이후에 재시도하며, 필요 시 큐잉 (queueing) 수행rate_limit_tokens: 토큰 사용량을 줄이거나, 큐에 넣거나, 더 작은 요청으로 전환provider_overloaded: 지터 (jitter)를 적용하여 재시도하되, 동기화된 재시도 폭풍 (retry storms)은 방지timeout: 해당 작업이 반복 수행되어도 안전한 경우에만 재시도stream_interrupted: 부분적인 출력이 사용 가능한지 아니면 폐기해야 하는지 결정tool_call_uncertain: 사이드 이펙트 (side effects)를 맹목적으로 두 번 실행하지 말 것
마지막 항목은 에이전트 워크플로우 (agentic workflows)에서 매우 중요합니다.
LLM 호출이 단순히 텍스트를 생성하는 것이었다면, 재시도는 대개 괜찮습니다.
하지만 LLM 호출이 외부 작업을 계획하거나 트리거하는 것이었다면, 재시도는 의도치 않게 실제 작업을 중복 실행할 수 있습니다. 이메일을 두 번 보내거나, 티켓을 두 개 생성하거나, 두 번 결제하거나, 동일한 웹훅 (webhook)을 두 번 호출하는 것은 "답변이 너무 오래 걸림"과는 완전히 다른 실패 모드 (failure mode)입니다.
런북 (runbook)을 코드와 가깝게 유지하세요
장애 대응 런북 (incident runbook) 전체가 코드베이스 내부에 포함되어야 한다고 생각하지는 않습니다.
인간의 맥락 (human context)은 여전히 중요합니다.
하지만 첫 번째 결정은 클라이언트 (client) 근처에 인코딩되어 있어야 합니다.
실질적인 절충안은 다음과 같습니다:
- 코드가 장애를 분류합니다.
- 코드가 첫 번째 조치를 선택합니다.
- 로그에 런북 (runbook) ID가 포함됩니다.
- 런북 (runbook)은 조사 단계와 비즈니스 컨텍스트 (business context)를 설명합니다.
예를 들어:
{
category: "rate_limit_tokens",
action: "queue",
...
그러면 런북 (runbook)은 다음과 같은 내용을 설명할 수 있습니다:
- 토큰 사용량을 어디서 확인해야 하는지
- 어떤 대시보드 (dashboard)가 중요한지
- 최대 출력 토큰 (max output tokens)을 줄여야 하는지
- 폴백 모델 (fallback models) 사용이 승인되었는지
- 할당량 (quota) 증설 권한이 누구에게 있는지
- 사용자에게 어느 정도의 성능 저하 (degradation)까지 허용 가능한지
이렇게 하면 애플리케이션 코드를 거대한 위키 (wiki) 페이지로 만들지 않으면서도, 조직의 지식 (institutional knowledge)을 장애가 시작되는 지점과 가깝게 유지할 수 있습니다.
내가 현재 사용하는 체크리스트
새로운 LLM 제공업체 (provider)나 모델 경로 (model path)를 배포하기 전에, 저는 다음 질문들에 답하려고 노력합니다:
- 제공업체의 원시 에러 메타데이터 (raw provider error metadata)를 로그에 남기는가?
- 요청 제한 (request limits)과 토큰 제한 (token limits)을 구분할 수 있는가?
- 과부하 (overload)를 속도 제한 (rate limits)과 별도로 분류하는가?
- 이 워크플로 (workflow)에서 타임아웃 재시도 (timeout retries)가 안전한가?
- 스트리밍 장애 (streaming failures) 발생 시 의도적으로 부분적인 상태 (partial state)를 보존하는가?
- 툴 호출 (tool calls)에 멱등성 키 (idempotency keys) 또는 작업 ID (operation IDs)가 있는가?
- 모든 장애 카테고리에 첫 번째 조치가 정의되어 있는가?
- 모든 일반적인 카테고리가 짧은 런북 (runbook)을 가리키고 있는가?
- 온콜 (on-call) 담당자가 단 하나의 로그 이벤트만으로 장애를 이해할 수 있는가?
만약 대답이 '아니오'라면, 해당 통합 (integration)은 개발 단계에서는 작동할지 몰라도 프로덕션 트래픽 (production traffic)을 처리할 준비는 되지 않았을 가능성이 높습니다.
마지막 생각
LLM API는 실제 워크로드 (workload) 하에서 장애가 발생하기 전까지는 일반적인 API처럼 보입니다.
그때 진짜 어려운 부분은 예외 (exception)를 잡아내는 것이 아닙니다. 진짜 어려운 부분은 재시도 (retry)를 할지, 속도를 늦출지, 큐에 넣을지 (queue), 폴백 (fallback)을 할지, 실패 시 차단할지 (fail closed), 아니면 누군가를 깨울지를 결정하는 것입니다.
그 결정이 잊혀진 런북 (runbook) 속에만 머물러서는 안 됩니다.
LLM 클라이언트 (client) 옆에 작은 장애 대응 정책 (failure policy)을 두십시오.
미래의 온콜 (on-call) 담당자인 당신이 훨씬 덜 짜증 날 것입니다.
저는 TokenBay에서 일하고 있으며, 따라서 멀티 모델 (multi-model) API 동작, 라우팅 (routing), 그리고 장애 처리 (failure handling)에 대해 많은 시간을 할애하여 고민합니다. 이 포스트는 특정 제공업체에 관한 것이라기보다, 더 많은 LLM 애플리케이션이 첫날부터 갖추기를 바라는 프로덕션 레이어 (production layer)에 관한 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기