4장. 신뢰성과 오류 처리

학습 목표

  • 오류를 상태 코드, 본문의 오류 코드, 헤더로 읽어 재시도할 것과 고칠 것, 사람이 판단할 것으로 구분한다.
  • 레이트 리밋의 구조와 429의 여러 원인을 설명하고 트래픽을 조절한다.
  • 지터와 예산을 갖춘 재시도 정책을 한 계층에 설계하고, 층별 타임아웃과 마감 시각을 정한다.
  • 멱등 키로 부작용 있는 작업을 안전하게 반복하고, 결과 불명 상태를 복구한다.
  • 중단된 응답과 부분 실패를 복구하고, 장애가 다른 기능으로 번지지 않게 막는다.

들어가며

새벽 두 시, 원고 교정 작업이 멈췄습니다. 담당자는 아침에 출근해 작업을 처음부터 다시 돌렸고, 점심 무렵 편집자들이 같은 장의 교정본이 두 개씩 올라와 있다고 알렸습니다. 로그를 보니 밤사이 529와 429가 섞여 있었고, 재시도 로직은 모든 실패를 1초 간격으로 무한 반복하고 있었습니다. 장애 자체보다 장애에 대한 대응이 더 큰 피해를 낸 셈입니다.

외부 API를 쓰는 시스템에서 실패는 예외가 아니라 일상입니다. 중요한 것은 실패의 종류를 구분하고, 종류마다 정해진 대응을 하고, 같은 일을 두 번 하지 않는 것입니다. 이 장은 오류 분류에서 시작해 레이트 리밋, 재시도와 백오프, 타임아웃, 멱등성, 중단된 응답과 부분 실패, 장애 격리 순서로 갑니다.

핵심 개념

성공, 실패, 그리고 결과 불명

요청의 결과는 둘이 아니라 셋입니다. 성공 응답을 받았거나, 실패 응답을 받았거나, 아무 응답도 받지 못했습니다. 세 번째를 결과 불명이라고 합니다. 연결이 끊기기 전에 서버가 이미 작업을 마쳤을 수도 있고, 시작도 안 했을 수도 있습니다.

모델 호출 자체는 결과 불명이어도 다시 보내면 그만입니다. 비용이 조금 더 들 뿐 외부 세계가 바뀌지 않습니다. 문제는 부작용이 있는 작업입니다. 환불, 예약 생성, 메일 발송이 결과 불명이라면 “실패”로 간주하고 다시 실행하는 순간 두 번 실행될 수 있습니다. 결과 불명인 변경 작업은 다음 순서로 다룹니다.

  1. 같은 업무 키로 실제 상태를 조회한다.
  2. 이미 반영됐으면 그 결과를 성공으로 쓴다.
  3. 반영되지 않았음이 확인되면 같은 멱등 키로 다시 실행한다.
  4. 조회할 수 없으면 자동 재실행하지 말고 보류 상태로 두고 사람에게 알린다.

오류 분류표

공급자마다 이름은 다르지만 상태 코드의 뜻은 비슷합니다.

상태 Anthropic OpenAI Gemini(Interactions API) 대응
400 invalid_request_error(직접 설정한 지출 한도 도달 포함) 잘못된 요청 invalid_request, failed_precondition 요청이나 설정을 고친다
401 authentication_error 인증 오류 — 키 확인·교체
402 billing_error — payment_required 결제 정보 처리. 재시도하지 않는다
403 permission_error 지원하지 않는 지역 등 permission_denied 권한·접근 설정 확인
409 conflict_error 충돌 — 충돌을 해소한 뒤 재시도
413 request_too_large(Messages API 32MB) — — 요청 크기를 줄인다
429 rate_limit_error 여러 원인(다음 절) rate_limit_exceeded, too_many_requests, quota_exceeded 원인별로 다르다
500 api_error 서버 오류 api_error 백오프 후 재시도
503 — server_is_overloaded service_unavailable 백오프 후 재시도
504 timeout_error — deadline_exceeded 스트리밍·배치 검토, 클라이언트 마감 확인
529 overloaded_error — — 백오프 후 재시도

Gemini 문서는 400, 402, 403 같은 클라이언트 오류를 재시도하지 말라고 분명히 하고, OpenAI 문서도 잘못된 요청은 고치기 전까지 같은 결과가 나온다고 설명합니다.

재시도할 오류를 가려내는 결정표

상태 코드만으로는 부족합니다. 본문의 오류 코드와 헤더까지 읽고, 세 가지 질문에 답합니다.

질문 예 아니오
같은 요청이 시간이 지나면 성공할 수 있는가 레이트 리밋, 과부하, 일시적 서버 오류 형식 오류, 인증 오류, 지출 한도, 결제 문제
반복해도 안전한가 모델 호출, 조회, 멱등 키가 있는 변경 멱등 키 없는 변경 작업
기다릴 시간이 남았는가 마감 시각까지 여유가 있다 사용자가 이미 떠났거나 마감이 지났다

세 질문 모두 “예”일 때만 자동 재시도합니다. 첫 질문이 “아니오”이면 요청을 고치거나 사람에게 알리고, 둘째가 “아니오”이면 결과 불명 절차로, 셋째가 “아니오”이면 실패를 확정하고 대체 경로로 넘깁니다.

레이트 리밋의 구조

레이트 리밋은 일정 시간 동안 허용되는 요청과 토큰의 한도입니다. 공급자마다 재는 단위가 다릅니다.

공급자 단위 범위
Anthropic 모델 등급별 분당 요청(RPM), 분당 입력 토큰(ITPM), 분당 출력 토큰(OTPM) 조직. 워크스페이스별로 더 낮은 한도를 둘 수 있다
OpenAI RPM, RPD, TPM, TPD, 분당 이미지 수(IPM) 등 조직과 프로젝트. 일부 모델군은 한도를 공유한다
Gemini RPM, 분당 입력 토큰(TPM), 일일 요청(RPD) API 키가 아니라 프로젝트. RPD는 태평양 시간 자정에 초기화

Anthropic은 토큰 버킷 알고리즘을 씁니다. 물통에 물이 일정한 속도로 계속 채워지고 요청마다 물을 떠 가는 구조라서, 정해진 시각에 한꺼번에 초기화되지 않습니다. 그래서 분당 60회 한도가 초당 1회로 집행될 수 있고, 분당 총량이 한도 안이어도 짧은 순간에 몰리면 429가 납니다. 사용량이 갑자기 늘면 가속 한도(acceleration limit)에 걸릴 수 있으니 트래픽은 점진적으로 늘리라고 문서는 권합니다.

남은 한도는 응답 헤더로 알 수 있습니다. Anthropic은 anthropic-ratelimit-requests-remaining, anthropic-ratelimit-input-tokens-remaining처럼 항목별 한도·잔여·초기화 시각을, OpenAI는 x-ratelimit-remaining-requests, x-ratelimit-remaining-tokens 같은 헤더를 줍니다. 계산 방식의 차이도 기억해 둡니다. OpenAI는 max_tokens와 추정 토큰 중 큰 값으로 토큰 한도를 계산하고, 캐시된 입력도 TPM에 셉니다. Anthropic의 OTPM은 실제 생성량만 세고, 대부분의 모델에서 캐시 읽기는 ITPM에 세지 않습니다.

429를 읽는 법

같은 429라도 원인에 따라 대응이 정반대입니다.

원인 알아보는 법 대응
속도 한도 초과 대개 retry-after 헤더가 있다(없을 수도 있다) 최소 그만큼 기다린 뒤 재시도
급격한 증가 Anthropic 가속 한도, OpenAI slow_down 재시도하되 트래픽 증가 속도를 낮춘다
Anthropic 등급 지출 한도 retry-after 없음, error.details.error_code가 enforced_spend_limit_reached 재시도 중단. SDK 자동 재시도도 실패한다. 담당자에게 알린다
OpenAI 크레딧·지출·사용 한도 오류 코드가 크레딧 소진이나 한도 초과 재시도해도 소용없다. 크레딧 추가나 한도 조정
Gemini 일일 할당량 quota_exceeded 초기화까지 기다리거나 증액 요청

Anthropic에서 조직이나 워크스페이스에 직접 설정한 지출 한도에 닿으면 429가 아니라 400 invalid_request_error가 온다는 점도 함께 기억하세요(Claude Code 워크스페이스 한도는 예외로 retry-after가 있는 429). 표의 Gemini 코드는 Interactions API 기준이며, generateContent API는 RESOURCE_EXHAUSTED 같은 다른 형식을 씁니다. 상태 코드가 아니라 원인으로 분기해야 합니다.

지수 백오프와 지터

재시도할 오류라면 얼마나 기다릴지를 정해야 합니다. 지수 백오프(exponential backoff)는 시도할 때마다 대기 시간을 두 배씩 늘립니다. 지터(jitter)는 여기에 무작위 흔들림을 더해, 같은 장애를 겪은 수많은 클라이언트가 같은 순간에 다시 몰려드는 것을 막습니다. 엘리베이터 앞에 줄이 무너졌을 때 모두가 정확히 10초 뒤에 다시 달려가면 또 엉키는 것과 같습니다.

function backoffMs(attempt: number, retryAfterSec?: number): number | null {
  const base = 500, cap = 30_000;
  if (retryAfterSec !== undefined) {
    const wait = retryAfterSec * 1000 + Math.random() * 1000; // 최소 대기 + 작은 무작위 지연
    return wait > cap ? null : wait; // 상한보다 길면 null: 재시도하지 않고 실패를 확정한다
  }
  const ceiling = Math.min(cap, base * 2 ** attempt);
  return Math.random() * ceiling; // 전체 지터: 0~상한 사이에서 고른다
}

OpenAI 문서는 Retry-After 값을 최소 대기로 보고 작은 무작위 지연을 더하라고 권하며, 시도 횟수와 총 재시도 시간에 모두 상한을 두라고 안내합니다. 실패한 요청도 분당 한도에 포함되므로 같은 요청을 계속 다시 보내면 상황이 나빠질 뿐이라는 경고도 덧붙입니다.

재시도는 한 곳에서, 예산 안에서

재시도는 여러 계층에 숨어 있습니다. Anthropic 공식 SDK는 연결 오류, 429, 5xx를 기본 2회 지수 백오프로 재시도하고 retry-after를 따르며 max_retries로 바꿀 수 있습니다. Gemini Python SDK는 일시적 오류를 최대 4회까지, OpenAI 공식 SDK는 연결 오류와 408·409·429·5xx를 기본 2회 재시도합니다. 여기에 애플리케이션 래퍼, 작업 큐, API 게이트웨이가 각자 재시도를 더하면 횟수가 곱해집니다.

AWS Builders' Library는 재시도를 “이기적”이라고 표현합니다. 내 요청의 성공 확률을 높이려고 서버 용량을 더 쓰기 때문입니다. 그래서 스택의 한 지점에서만 재시도하고, 의존 서비스가 건강해 보일 때만 재시도하라고 권합니다. 실천 방법은 다음과 같습니다.

  1. 재시도를 맡을 계층을 하나 정한다. SDK에 맡긴다면 래퍼의 재시도를 끄고, 래퍼에서 한다면 SDK 재시도를 0으로 둔다. 설정 이름은 SDK와 언어마다 다르다(예: Python max_retries, TypeScript maxRetries).
  2. 재시도 예산을 둔다. 시도 횟수, 총 대기 시간, 그리고 전체 요청 대비 재시도 비율(예: 10%)이다. 정상 응답마다 토큰을 조금씩 채우고 재시도할 때 토큰을 쓰는 클라이언트 쪽 토큰 버킷을 두면 장애 때 재시도가 폭증하지 않는다.
  3. 로그에 원요청 ID와 시도 번호를 남긴다. 요청 하나가 실제로 몇 번 전달됐는지 알아야 예산을 지킬 수 있다.

타임아웃을 층으로 설계한다

타임아웃 하나로 모든 대기를 다루면 너무 일찍 끊거나 너무 오래 붙잡습니다. 세 층으로 나눕니다.

층 뜻 정하는 기준
연결 타임아웃 TCP·TLS 연결 수립까지 짧게. 수 초
유휴 타임아웃 스트림에서 다음 이벤트가 오기까지 정상 응답에서 관측한 이벤트 간격의 높은 백분위수(p99 등)에 여유를 더한 값
전체 마감 시각 사용자에게 답해야 하는 절대 시각 응답 계약의 지연 목표

AWS는 하위 서비스의 지연 분포에서 높은 백분위수(예: p99.9)를 골라, 오탐 타임아웃을 작은 비율로 감수하는 방식을 소개합니다. 마감 시각은 아래로 전달합니다. 도구를 실행할 때 남은 시간을 넘겨, 마감이 지난 뒤에 끝날 작업은 시작하지 않습니다.

긴 생성 요청은 스트리밍이나 배치로 보냅니다. Anthropic 문서는 10분을 넘을 수 있는 요청에 이를 권하는데, 일부 네트워크가 오래 유휴 상태인 연결을 끊기 때문입니다. SDK는 비스트리밍 요청이 10분을 넘기지 않을지 검사하고 TCP keep-alive를 설정하며, OpenAI SDK의 기본 타임아웃도 10분입니다. OpenAI 문서에 따르면 동기 응답은 연결을 끊어 취소하지만, 백그라운드 모드처럼 서버에서 도는 작업은 명시적으로 취소해야 멈춥니다. 중간 계층만 연결을 끊고 앱 서버의 상류 요청이 살아 있으면 생성과 과금은 계속됩니다.

멱등성: 두 번 눌러도 한 번만

엘리베이터 호출 버튼은 여러 번 눌러도 엘리베이터가 한 대만 옵니다. 같은 요청을 여러 번 보내도 한 번 보낸 것과 결과가 같은 성질을 멱등성이라고 합니다. HTTP 표준(RFC 9110)은 PUT, DELETE와 안전한 메서드를 멱등 메서드로 정하고, 클라이언트가 비멱등 요청을 자동으로 재시도하려면 그 요청이 실제로 멱등하다는 것을 알거나 원래 요청이 적용되지 않았음을 확인할 수단이 있어야 한다고 말합니다. AWS도 부작용이 있는 API는 멱등성을 제공하지 않는 한 재시도하기에 안전하지 않다고 정리합니다.

애플리케이션에서 멱등성을 만드는 방법은 이렇습니다.

  1. 작업 하나에 키 하나를 정하고, 그 작업의 모든 재시도에 같은 키를 쓴다. 예: 도구 호출 ID로 만든 키, 또는 refund:{주문번호}:{환불 회차} 같은 업무 키.
  2. 받는 쪽은 키에 고유 제약을 걸고 상태(처리 중, 완료, 실패), 요청 내용의 지문(해시), 결과를 저장한다. 동시에 같은 키가 들어와도 하나만 처리되도록 키 선점을 원자적으로 한다(예: 고유 제약을 이용한 삽입).
  3. 같은 키가 다시 오면 저장된 결과를 돌려준다. 같은 키인데 내용이 다르면 충돌로 거부한다.
  4. 키의 보관 기간을 재시도가 일어날 수 있는 기간보다 길게 잡는다.

모델이 주는 도구 호출 ID는 호출마다 고유하므로 키를 만드는 재료로 쓰기 좋습니다. 같은 도구 호출을 네트워크 오류로 다시 보낼 때 같은 키가 따라가므로 받는 쪽이 중복을 알아봅니다. 재시도할 때마다 키를 새로 만들면 이 장치가 무너집니다. 또 모델 요청 자체를 다시 보내 모델이 같은 작업을 새 호출로 다시 제안할 수 있는 흐름이라면, 주문번호 같은 업무 식별자를 키에 함께 넣어 받는 쪽이 같은 작업임을 알아보게 합니다. 들어오는 쪽도 마찬가지입니다. OpenAI 웹훅은 드물게 같은 이벤트를 두 번 보낼 수 있으므로 webhook-id 헤더로 중복을 걸러 내라고 문서는 안내합니다.

중단된 응답을 복구한다

스트리밍은 200 응답을 받은 뒤에도 실패할 수 있습니다. 이벤트 단위로 오류를 처리하고, 끊겼을 때의 복구 전략을 미리 정합니다.

  • Anthropic: 받은 부분을 저장해 두고 이어 받기를 요청할 수 있습니다. Claude 4.5 이하 모델은 받은 부분을 새 assistant 메시지의 시작으로 넣고, Claude 4.6 이후 모델은 받은 부분과 함께 “끊긴 지점부터 이어 쓰라”는 user 메시지를 보냅니다. 도구 사용 블록과 사고 블록은 부분 복구가 되지 않으므로, 마지막 텍스트 블록까지만 이어 받고 나머지는 새로 요청합니다. 이어 쓰기는 중복이나 어긋남이 생길 수 있으므로, 구조화 출력처럼 형식이 중요한 결과는 처음부터 다시 요청하는 편이 안전합니다.
  • OpenAI: 오래 걸리는 작업은 background: true로 서버에서 돌리고 상태를 폴링합니다. 스트리밍과 함께 쓰면 이벤트마다 오는 sequence_number를 커서로 저장해 두었다가, 연결이 끊긴 뒤 starting_after로 그 지점부터 다시 받을 수 있습니다. 데이터 무보존(ZDR, zero data retention) 프로젝트에서는 store=false로 동작하며 응답 데이터가 폴링을 위해 약 10분간 임시 저장됩니다.

미완성 결과를 보여 주는 것과 업무 완료를 확정하는 것은 별개입니다. 끊긴 응답으로 만든 미리보기는 “생성 중” 상태로 두고, 완성되고 검증을 통과한 결과만 저장합니다.

부분 실패를 복구한다

여러 단위로 이뤄진 작업은 일부만 실패하는 일이 흔합니다. 원칙은 “성공한 것은 보존하고, 실패한 것만 원인별로 다시”입니다.

  • 병렬 도구 호출: 일부 호출이 실패하면 실패 결과를 돌려주고 모델이 다음 행동을 정하게 합니다. 실행하지 않은 호출에도 이유를 담은 결과를 보냅니다(2장).
  • 배치: 결과를 성공, 오류, 취소, 만료로 나눕니다. 만료·취소·서버 오류는 다시 제출하고, 요청 형식 오류는 고치기 전에는 다시 넣지 않습니다. 이미 성공한 항목은 다시 생성하지 않습니다.
  • 다단계 파이프라인: 항목별 상태(대기, 진행, 완료, 실패, 격리)를 저장하고 단계가 끝날 때마다 체크포인트를 남깁니다. 재시작하면 완료된 항목은 건너뜁니다.
  • 되돌릴 수 없는 작업: 메일 발송처럼 취소할 수 없는 작업은 보정 절차(정정 메일, 수동 처리)를 따로 정해 둡니다.

반복해서 같은 오류를 일으키는 입력은 격리 큐(dead letter queue)로 옮겨 다른 작업을 막지 않게 하고, 사람이 원인을 확인합니다.

장애가 번지지 않게 막는다

외부 API가 오래 느리거나 실패하면, 그 호출을 기다리는 스레드와 연결이 쌓여 무관한 기능까지 느려집니다. 배의 격벽처럼 피해를 칸 안에 가두는 장치가 필요합니다.

장치 동작 설정할 것
대기열 입장 제한 대기 중인 요청이 일정 수를 넘으면 새 요청을 바로 거절 최대 대기 수, 거절 시 사용자 안내
자원 격리(벌크헤드) 기능별로 동시 실행 수와 연결 풀을 나눈다 기능별 동시 실행 한도
회로 차단기 실패율이 임계값을 넘으면 일정 시간 호출 자체를 멈추고, 이후 일부만 시험해 회복을 확인 실패율 임계값, 차단 시간, 시험 요청 수
대체 경로 미리 평가한 다른 모델·공급자, 축소된 기능, 정적 안내 품질 기준, 전환·복귀 조건

회로 차단기의 상태는 셋입니다. 전기 회로처럼 닫혀 있을 때 전류가 흐릅니다. 정상(닫힘)에서는 호출을 통과시키고, 차단(열림)에서는 즉시 대체 경로로 보내며, 시험(반열림)에서 일부 요청이 성공하면 다시 닫힙니다. 대체 모델은 장애가 난 뒤에 고르면 늦습니다. 평소에 같은 평가 세트로 품질을 확인해 두고, 전환 사실을 로그와 지표에 남깁니다.

깊이 읽기

재시도 폭풍의 해부

재시도는 계층마다 곱해집니다. API 게이트웨이가 1회, 애플리케이션 래퍼가 3회, SDK가 기본 2회 재시도한다면, 실패가 계속될 때 사용자 요청 하나는 최대 (1+1)×(1+3)×(1+2) = 24번 API에 전달됩니다. 장애 중에 정상 트래픽의 수십 배가 몰리고, 실패한 요청도 한도를 소모하므로 회복은 더 늦어집니다. 백오프가 있어도 곱셈 구조는 그대로입니다.

폭풍을 막는 순서는 이렇습니다. 첫째, 재시도 계층을 하나로 줄입니다. 둘째, 재시도 예산을 비율로 둡니다. 정상 요청 대비 재시도가 일정 비율을 넘으면 재시도를 멈추고 실패를 빨리 확정합니다. 셋째, 회로 차단기로 의존 서비스가 건강하지 않을 때는 재시도 대신 대체 경로를 씁니다. 넷째, 장애가 끝난 뒤 쌓인 작업을 한꺼번에 풀지 않습니다. 가속 한도에 다시 걸리지 않도록 처리량을 단계적으로 올립니다.

실패를 주입해 복구 정책을 검증한다

정상 응답만으로는 재시도와 중복 방지의 결함을 찾을 수 없습니다. 모델 호출과 도구 호출 앞에 가짜 응답을 끼워 넣는 장애 주입 계층을 만들고, 다음 상황을 재현합니다.

주입할 장애 확인할 것
retry-after가 있는 429, 헤더 없는 일시적 429, 지출 한도 오류 코드가 붙은 429 앞의 둘은 백오프로 재시도, 지출 한도 코드는 즉시 중단·알림
529·500 연속 예산 안에서 회복하거나 대체 경로로
스트림 중간 끊김 미완성 결과가 저장되지 않는가
변경 작업 성공 직후 응답 유실 결과 불명 절차로 조회하고 중복 실행이 없는가
배치 일부 실패, 같은 웹훅 두 번 성공 항목을 다시 만들지 않고, 같은 이벤트를 한 번만 처리하는가

검사 기준은 세 가지입니다. 같은 업무가 두 번 실행되지 않았는가, 요청 하나의 실제 전달 횟수가 예산 안인가, 최종 상태 표시가 실제와 같은가. 이 시험을 배포 파이프라인에 넣어 두면 재시도 로직을 고칠 때마다 회귀를 잡을 수 있습니다.

실무 장면

맑은샘정수기: 60초에 잘리는 리포트

정수기 렌털 회사 ‘맑은샘정수기’의 방문 기사 앱은 점검 전에 고객별 수질 이력 리포트를 생성한다. 리포트가 길어진 뒤로 약 15%의 요청이 실패했고, 기사들이 재시도 버튼을 눌러 같은 리포트를 두세 번 생성하는 일이 잦았다. 비용은 생성된 리포트 수보다 40% 많이 나갔다.

백엔드 담당 봉 선임이 확인해 보니, 앱 서버는 비스트리밍으로 모델을 호출했고 사내 API 게이트웨이의 요청 타임아웃은 60초였다. 긴 리포트는 생성에 70~90초가 걸려 게이트웨이에서 끊겼지만, 앱 서버의 모델 요청은 살아 있어 끝까지 생성되고 과금됐다. 게이트웨이와 앱 래퍼, SDK가 각자 재시도도 하고 있었다.

봉 선임의 수정은 다음과 같았다.

  1. 리포트 경로를 스트리밍으로 바꾸고, 게이트웨이에서 이 경로의 버퍼링을 끈다. 타임아웃은 “이벤트 사이 30초 유휴”와 “전체 4분 마감”으로 바꾼다.
  2. 재시도는 SDK 한 곳에서만 하고, 래퍼와 게이트웨이의 재시도는 끈다.
  3. 기사가 화면을 닫으면 상류 스트림을 끊는다.
  4. 다음 날 방문 예정 고객의 리포트는 전날 밤 미리 생성해 둔다. 당일 급히 추가된 방문만 실시간으로 생성한다.

게이트웨이 타임아웃을 15분으로 늘리는 안은 연결을 오래 붙잡고, 유휴 연결이 중간 네트워크에서 끊길 위험이 남아 채택하지 않았다. 같은 요청을 두 번 동시에 보내 먼저 오는 답을 쓰는 안은 비용이 두 배라 기각했다. 수정 뒤 실패율은 1% 아래로 내려갔고, 중복 생성은 사라졌다.

솔내음출판: 처음부터 다시 돌리지 않는 교정 파이프라인

출판사 ‘솔내음출판’은 단행본 원고를 장 단위로 나눠 교정 제안을 만들고 원고 관리 시스템에 저장한다. 원고 12권, 380개 장을 처리하던 밤에 529가 이어지자 작업이 멈췄고, 담당자는 아침에 작업을 처음부터 다시 돌렸다. 이미 저장된 장의 교정본이 한 번 더 저장됐고, 이틀 치 비용이 나갔다. 한 장은 고해상도 스캔 이미지가 많아 요청이 매번 크기 한도를 넘는 413으로 실패하면서 재시도 대기열을 계속 막고 있었다.

플랫폼 엔지니어 진 책임은 파이프라인을 다시 짰다.

  1. 장마다 상태(대기, 진행, 완료, 실패, 격리)를 저장한다. 재시작하면 완료된 장은 건너뛴다.
  2. 저장은 (원고ID, 장번호, 교정 회차)를 멱등 키로 한다. 같은 키가 다시 오면 기존 결과를 돌려준다.
  3. 오류는 4장의 결정표로 나눈다. 529·500·속도 한도 429는 예산 안에서 재시도하고, 413·400은 재시도 없이 곧바로 격리 큐로 보낸다. 격리된 장은 담당자가 나눠서 다시 넣는다.
  4. 출시 전에 장애 주입 시험을 돌린다. 작업 도중 프로세스를 강제로 끝낸 뒤 재시작해 중복 저장이 없는지 확인한다.

전체를 배치 API로 옮기는 안도 검토했다. 비용은 줄지만 편집자들이 아침부터 장 순서대로 검토하는데 배치는 처리에 최대 24시간이 걸릴 수 있고 기한 안에 처리되지 않은 항목은 만료될 수 있으므로, 급하지 않은 재교정 회차에만 쓰기로 했다. 이후 장애가 난 날에도 재시작은 남은 장만 처리했고, 중복 저장은 한 건도 없었다.

자주 틀리는 지점

  • 429는 기다리면 모두 풀린다. 지출 한도나 일일 할당량 429는 한도가 풀리거나 조정될 때까지 계속 실패합니다. 본문의 오류 코드로 구분합니다.
  • 재시도는 빠르고 많을수록 좋다. 실패한 요청도 한도를 소모합니다. 백오프, 지터, 횟수·시간 상한, 재시도 예산이 필요합니다.
  • 400도 몇 번 다시 보내면 된다. 요청 형식 문제는 고치기 전까지 같은 결과가 나옵니다.
  • 타임아웃이 났으면 서버에서 실행되지 않았다. 결과 불명입니다. 변경 작업은 상태를 조회한 뒤에 다시 실행합니다.
  • 타임아웃을 늘리면 중복 실행이 해결된다. 빈도만 줄 뿐입니다. 근본 해결은 멱등 키입니다.
  • 재시도할 때마다 새 멱등 키를 만든다. 키가 바뀌면 받는 쪽은 같은 작업인지 알 수 없습니다. 같은 작업의 재시도에는 같은 키를 씁니다.
  • SDK 재시도에 애플리케이션 재시도를 더하면 더 안전하다. 횟수가 곱해져 장애를 키웁니다. 재시도는 한 계층에서만 합니다.

한눈에 정리

주제 기억할 것
결과 상태 성공·실패·결과 불명. 결과 불명인 변경은 조회 먼저
재시도 판단 나중에 성공할 수 있나, 반복해도 안전한가, 시간이 남았나
429 속도·급증은 기다려 재시도, 지출·할당량은 중단·알림. Anthropic 직접 설정 한도는 400
레이트 리밋 토큰 버킷이라 짧은 폭주도 걸린다. 점진적으로 늘린다
백오프 지수 증가 + 지터 + 상한. Retry-After는 최소 대기
재시도 계층 한 곳에서만. SDK 기본값(Anthropic 2회)을 확인
타임아웃 연결·유휴·전체 마감. 긴 요청은 스트리밍·배치·백그라운드
멱등성 재시도 내내 같은 키(도구 호출 ID·업무 식별자) + 고유 제약 + 결과 저장. 들어오는 이벤트도 중복 제거
부분 실패 성공은 보존, 실패는 원인별로. 반복 실패는 격리
장애 격리 입장 제한, 벌크헤드, 회로 차단기, 사전 평가된 대체 경로

공식 문서로 확인하기

셀프 체크

4-1. 응답을 받지 못한 변경 요청을 “실패”가 아니라 “결과 불명”으로 다뤄야 하는 이유를 쓰고, 결과 불명 상태에서 할 일을 순서대로 적으라.

4-2. 자동 재시도 여부를 정할 때 HTTP 상태 코드 외에 확인해야 할 정보 두 가지를 들고, 각각이 판단을 바꾸는 예를 하나씩 쓰라.

4-3. 토큰 버킷 방식의 레이트 리밋이 고정 시각 초기화 방식과 어떻게 다른지 설명하고, 분당 총량이 한도 안인데도 거절이 생기는 이유를 쓰라.

4-4. 연결 타임아웃, 유휴 타임아웃, 전체 마감 시각을 각각 정의하고, 긴 생성 요청에서 유휴 타임아웃이 단일 요청 타임아웃보다 나은 이유를 쓰라.

4-5. 멱등 키를 받는 쪽(결제·예약 서버)이 저장해야 할 정보와, 같은 키로 내용이 다른 요청이 왔을 때의 처리를 쓰라.

4-6. 하나의 LLM API를 실시간 상담, 문서 요약, 야간 분석 세 기능이 함께 쓴다. 기능별 재시도 예산, 동시 실행 한도, 타임아웃, 장애 시 동작을 정하는 표를 설계하라.

실습 과제

실습

장애 주입 재시도 래퍼 (60분)

  1. 모델 호출 앞에 가짜 응답을 끼워 넣는 계층을 만든다. retry-after가 있는 429, 헤더 없는 일시적 429, 지출 한도 오류 코드가 붙은 429, 529, 500, 400을 무작위로 섞는다.
  2. 이 장의 결정표와 백오프 함수를 구현하고, SDK 자동 재시도는 끈다. 시도 횟수, 총 대기 시간, 재시도 비율에 예산을 둔다.
  3. 요청 100개를 보내고 오류 유형별 재시도 횟수, 최종 결과, 요청당 실제 전달 횟수를 집계한다.

완료 기준: 400과 지출 한도 오류 코드가 붙은 429는 한 번도 재시도되지 않고, 나머지는 예산 안에서 회복되거나 대체 경로로 넘어갔으며, 요청당 최대 전달 횟수가 설계값을 넘지 않으면 완료다.

실습

체크포인트와 멱등 저장이 있는 파이프라인 (60분)

  1. 문서 30개를 하나씩 모델로 처리해 저장하는 작업을 만든다. 문서마다 상태(대기, 진행, 완료, 실패, 격리)를 파일이나 DB에 기록한다.
  2. 저장은 (문서ID, 처리 회차)를 멱등 키로 해서 같은 키가 다시 오면 기존 결과를 돌려주게 한다.
  3. 문서 두 개는 항상 재시도 불가 오류가 나도록 만들어 격리 큐로 보내고, 15번째 문서 처리 중 프로세스를 강제로 끝낸 뒤 다시 실행한다.

완료 기준: 재실행 후 완료된 문서는 다시 처리되지 않고, 저장소에 중복 결과가 없으며, 격리된 두 문서가 이유와 함께 목록으로 나오면 완료다.