4 min read

Part 3. 신뢰성 설계: Retry, Timeout, Fallback, Circuit Breaker

LLM 시스템에서 재시도, 타임아웃, 폴백, 서킷브레이커를 일반 API와 다르게 설계해야 하는 이유와 운영 패턴을 정리한다.

LLM 기능을 제품에 붙이면 가장 먼저 부딪히는 질문은 "정확도"인 것처럼 보인다. 실제 운영에서는 그보다 먼저 "언제, 어떻게 실패하는가"가 문제를 만든다. 같은 모델이라도 재시도 정책과 타임아웃 예산을 어떻게 잡는지에 따라 장애 반경이 완전히 달라진다. 이 편에서는 LLM 워크로드를 신뢰성 관점에서 분해하고, 운영에서 바로 적용할 수 있는 설계 패턴을 정리한다.

문제 제기

기존 REST API 운영 경험만으로 LLM 시스템 신뢰성을 설계하면 다음 문제가 자주 발생한다.

  • "실패하면 3회 재시도"를 일괄 적용해 모델 과부하를 스스로 증폭시킨다.
  • 타임아웃 기준이 없는 상태에서 긴 추론을 기다리다가 전체 요청 SLA를 깨뜨린다.
  • 폴백 모델 전환 기준이 없어 고비용 모델로 트래픽이 급격히 쏠린다.
  • 도구 호출(tool call) 실패와 모델 실패를 구분하지 않아 원인 분석이 불가능해진다.

실전 예시 A: 전자상거래 챗봇

프로모션 기간에 트래픽이 평시 대비 4배로 증가했고, 1차 모델 응답 지연이 증가했다. 시스템은 모든 실패를 동일하게 재시도했고, 재시도 요청이 다시 지연을 만들어 큐가 급격히 밀렸다. 결과적으로 사용자 입장에서는 "답변 품질" 문제가 아니라 "응답 없음" 문제가 됐다.

실전 예시 B: 사내 운영 어시스턴트

도구 호출 실패(권한 오류) 상황에서도 모델을 재호출하도록 구성해, 실패 원인을 해결하지 못한 채 토큰 비용만 반복 소모했다. 실패 유형을 분리하지 않은 설계가 원인이었다.

핵심 개념

LLM 신뢰성은 "모델 호출" 하나가 아니라 "확률적 추론 + 결정적 시스템"의 결합이다. 따라서 실패를 다음처럼 분류해야 한다.

실패 유형대표 원인재시도 가능성권장 대응
Transient Infra Failure네트워크 단절, 일시적 5xx높음짧은 백오프 재시도
Capacity Failure모델 공급자 포화, 레이트 제한중간큐잉 + 라우팅 다운그레이드
Deterministic Input Failure스키마 위반, 정책 차단낮음즉시 실패 + 사용자 가이드
Tool Permission Failure권한/정책 불일치낮음재시도 금지, 권한 경로 안내
Long Reasoning Timeout과도한 컨텍스트/복잡 질의중간분할 처리, 단계적 응답

핵심 원칙은 다음 네 가지다.

  1. 재시도는 실패 유형별로 다르게 적용한다.
  2. 타임아웃은 전체 요청 예산(deadline budget) 안에서 단계별로 분할한다.
  3. 폴백은 품질 저하를 수용하는 명시적 정책이어야 한다.
  4. 서킷브레이커는 공급자 보호뿐 아니라 우리 서비스 보호를 위한 장치다.
Mermaid diagram rendering...

실전 패턴

패턴 1: Deadline Budgeting + 단계별 타임아웃

전체 SLA를 먼저 정하고, 각 단계에 명시적으로 예산을 나눠야 한다. "모델이 느리면 기다린다"는 접근은 운영에서 거의 항상 실패한다.

type Budget = {
  totalMs: number;
  modelPrimaryMs: number;
  retryMs: number;
  toolMs: number;
  renderMs: number;
};

const defaultBudget: Budget = {
  totalMs: 2500,
  modelPrimaryMs: 1200,
  retryMs: 500,
  toolMs: 400,
  renderMs: 300,
};

export async function handleRequest(input: string, budget = defaultBudget) {
  const started = Date.now();

  const primary = await callModel(input, { timeoutMs: budget.modelPrimaryMs }).catch(() => null);
  if (primary) return finalize(primary, started);

  const elapsed = Date.now() - started;
  const remaining = budget.totalMs - elapsed;
  if (remaining <= budget.renderMs) {
    return degraded("응답 생성 시간이 초과되어 요약 결과를 제공합니다.");
  }

  const fallback = await callFallbackModel(input, {
    timeoutMs: Math.min(budget.retryMs, remaining - budget.renderMs),
  }).catch(() => null);

  if (!fallback) return degraded("현재 요청이 많아 간단 답변으로 대체합니다.");
  return finalize(fallback, started);
}

운영 포인트:

  • 요청마다 deadline_ms를 trace에 넣어 "시간 예산 소진 위치"를 분석한다.
  • 긴 문서 요약/분석은 synchronous 경로에서 분리해 비동기 작업 큐로 넘긴다.
  • degraded 응답은 UX에서 명시적으로 표시해 사용자 기대를 관리한다.

패턴 2: 실패 유형 기반 재시도 + 서킷브레이커

모든 실패를 재시도하면 장애 증폭기가 된다. 재시도는 "되돌릴 수 있는 실패"에만 제한해야 한다.

type FailureKind =
  | "TRANSIENT_INFRA"
  | "RATE_LIMIT"
  | "SCHEMA_INVALID"
  | "POLICY_DENIED"
  | "TOOL_PERMISSION"
  | "UNKNOWN";

function shouldRetry(kind: FailureKind, attempt: number) {
  if (attempt >= 2) return false;
  if (kind === "TRANSIENT_INFRA") return true;
  if (kind === "RATE_LIMIT") return true;
  return false;
}

function nextBackoffMs(attempt: number) {
  return Math.min(200 * 2 ** attempt + Math.floor(Math.random() * 100), 900);
}

export async function resilientInfer(input: string) {
  let attempt = 0;
  while (true) {
    try {
      return await callPrimaryModel(input);
    } catch (e) {
      const kind = classifyFailure(e);
      if (!shouldRetry(kind, attempt)) throw e;
      await sleep(nextBackoffMs(attempt));
      attempt += 1;
    }
  }
}
# 예시: 5분 창에서 실패율과 지연이 임계치 초과면 서킷 오픈
./ops/circuit/open-if-needed.sh \
  --target primary_model \
  --window 5m \
  --error-rate-threshold 0.08 \
  --latency-p95-threshold-ms 1800

운영 포인트:

  • 서킷 오픈 상태에서 저품질 폴백 경로를 허용할지, 완전 차단할지 서비스 도메인별로 다르게 둔다.
  • 서킷 상태 변경은 온콜 채널에 즉시 전파하고 자동 복구 기준(half-open probe)을 문서화한다.
  • 재시도 요청은 원 요청과 분리된 태그로 기록해 증폭률을 계산한다.

실패 사례/안티패턴

장애 시나리오: "재시도 폭주로 인한 2차 장애"

상황:

  • 새 모델 버전 배포 후 공급자 측 지연이 증가했다.
  • 애플리케이션은 timeout=4s, retry=3을 전역 설정으로 사용했다.
  • 평균 응답은 1.2초에서 6.7초로 증가했고, 워커 스레드가 포화되며 내부 API까지 지연이 전파됐다.

탐지 절차:

  1. retry_amplification_ratio 경보 (원 요청 대비 재시도 비율)
  2. queue_depth 급증 + worker_busy_ratio 90% 이상 지속
  3. trace에서 infer -> timeout -> retry 패턴 반복 확인

완화 절차:

  1. 전역 재시도 즉시 축소 (3 -> 1) 및 timeout 단축
  2. 서킷브레이커 강제 오픈, 폴백 모델로 60% 트래픽 우회
  3. 긴 요청 유형(문서 요약)을 비동기 큐로 강제 전환

회복 절차:

  1. 실패 분류기(classifier) 도입으로 deterministic failure 재시도 금지
  2. 단계별 deadline budget 표준화
  3. 재시도 증폭률과 degraded response 비율을 주간 SLO 리뷰 항목으로 추가

대표 안티패턴

  • "재시도는 많을수록 안전하다"는 가정
  • 공급자 SLA를 우리 SLA로 착각해 deadline budget을 비워두는 설계
  • 폴백 모델 품질 검증 없이 장애시에만 급히 활성화
  • 서킷브레이커 없이 클라이언트 타임아웃에만 의존

체크리스트

  • 실패 유형을 최소 4종 이상으로 분류하고, 유형별 재시도 정책을 코드화했는가?
  • 요청 전체 SLA 기준으로 단계별 타임아웃 예산을 분할했는가?
  • 폴백 경로의 품질/비용/정책 위반률을 평시에도 측정하는가?
  • 서킷브레이커 오픈/하프오픈/클로즈 전이 기준이 문서화되어 있는가?
  • 재시도 증폭률(retry_amplification_ratio)을 모니터링하는가?
  • 도구 호출 실패와 모델 실패를 별도 지표로 수집하는가?
  • 장애 시 동기 요청을 비동기 큐로 우회하는 runbook이 준비되어 있는가?

요약

LLM 신뢰성 설계의 핵심은 "성공 경로 최적화"가 아니라 "실패 경로 통제"다. 재시도, 타임아웃, 폴백, 서킷브레이커를 실패 유형 중심으로 설계하면 장애 반경을 제한할 수 있다. 프롬프트를 바꾸는 것보다 실패 분류와 시간 예산을 바꾸는 것이 운영 안정성에 더 큰 영향을 준다.

다음 편 예고

다음 편에서는 비용(Cost)을 다룬다. 캐시, 배칭, 모델 라우팅, 토큰 예산을 단순 절감 관점이 아니라 품질과 신뢰성을 함께 만족시키는 제어 루프로 설명한다. 특히 "비용을 줄였더니 품질/지연이 동시에 악화되는" 전형적 함정을 어떻게 피하는지 운영 지표 중심으로 정리한다.


이전 편: Part 2. 품질은 Prompt가 아니라 평가 루프에서 나온다

다음 편: Part 4. 비용 설계: 캐시, 배칭, 라우팅, 토큰 예산

댓글