API 재시도 로직과 지수 백오프: 실제로 작동하는 패턴

전체 지터가 적용된 지수 백오프, Retry-After 헤더, 멱등성 키, 서킷 브레이커를 숙달한 후, Apidog 목으로 API 재시도 로직을 테스트하세요.

INEZA Felin-Michel

INEZA Felin-Michel

31 August 2026

API 재시도 로직과 지수 백오프: 실제로 작동하는 패턴

Apidog 엔터프라이즈

온프레미스 배포

SSO & RBAC

SOC 2 준수

Apidog Enterprise 살펴보기

새벽 2시에 결제 API 호출이 실패했습니다. 일시적인 네트워크 문제였을까요, 요청 속도 제한이었을까요, 아니면 서버가 다운된 걸까요? 이 질문의 답에 따라 재시도가 트랜잭션을 살릴지, 아니면 고객에게 이중으로 청구할지가 결정됩니다.

재시도는 분산 시스템에서 가장 흔한 복원력 패턴이자, 가장 흔하게 잘못 다루어지는 패턴입니다. HTTP 호출을 감싸는 루프는 방어적인 프로그래밍처럼 보이지만, 잘못하면 수천 개의 클라이언트가 동시에 고통받는 서버를 공격하여 30초의 장애를 30분으로 늘릴 수 있습니다. 올바르게 구현하면 재시도는 일시적인 오류를 사용자조차 알아채지 못하게 깔끔하게 흡수합니다.

이 가이드는 프로덕션 시스템이 의존하는 재시도 로직을 다룹니다: 어떤 상태 코드를 재시도해야 하는지, 풀 지터가 적용된 지수 백오프 공식, Retry-After 헤더, 멱등성 키, 재시도 예산, 그리고 서킷 브레이커. 또한 Apidog 목 서버를 사용하여 429 및 503 오류를 시뮬레이션함으로써 클라이언트가 올바르게 작동하는지 증명하는 방법도 배우게 될 것입니다. 왜냐하면 실패하는 서버에 대해 테스트하지 않은 재시도 패턴은 설계가 아니라 추측에 불과하기 때문입니다. 핀테크 API 재시도 로직을 구축하는 팀들은 이를 값비싼 방식으로 배우지만, 여러분은 그럴 필요가 없습니다.

어설픈 재시도가 장애를 악화시키는 이유

초당 1,000개의 요청을 처리하는 서비스를 상상해 보세요. 5초 동안 문제가 발생합니다. 모든 클라이언트가 즉시 각각 3번씩 재시도합니다. 초당 1,000개의 요청 수요는 이미 힘든 서버에 초당 4,000개의 요청을 퍼붓게 됩니다. 서버는 완전히 다운됩니다. 이제 모든 클라이언트가 다시 재시도합니다.

이러한 피드백 루프에는 '재시도 폭풍(retry storm)'이라는 이름이 있습니다. 서버가 다시 온라인될 때 동시에 몰려드는 현상은 '때 지어 몰려드는 현상(thundering herd)'입니다. Google SRE 책은 연쇄 실패 처리 장에서 이 패턴을 지적합니다: 백오프 없는 재시도는 시스템이 가장 감당할 수 없을 때 정확히 부하를 증폭시키며, 원래 오류가 수정된 후에도 서비스를 오랫동안 다운 상태로 유지할 수 있습니다.

두 가지 설계 결함이 대부분의 재시도 폭풍을 일으킵니다:

해결책은 "절대 재시도하지 마라"가 아닙니다. 해결책은 선택적으로, 점차 늘어나는 무작위 지연으로, 그리고 재시도가 추가하는 부하에 대한 엄격한 상한선을 두어 재시도하는 것입니다.

이러한 실패는 재시도하고, 저러한 실패는 절대 재시도하지 마세요

어떤 백오프 계산을 하기 전에, 클라이언트는 의사결정 테이블이 필요합니다. 서버가 이미 유효하지 않다고 거부한 요청을 재시도하는 것은 용량을 낭비하고 로그를 오염시킵니다. 일시적인 오류를 재시도하는 것이 핵심입니다.

재시도할 경우:

신호 의미
429 Too Many Requests 요청 속도 제한에 도달했습니다. 잠시 기다린 후 더 느리게 다시 시도하세요.
502 Bad Gateway 업스트림 홉에서 잘못된 응답을 반환했습니다. 종종 일시적입니다.
503 Service Unavailable 서버가 과부하 상태이거나 재시작 중입니다.
504 Gateway Timeout 업스트림 종속성이 너무 느렸습니다.
연결 재설정, DNS 실패, 소켓 타임아웃 요청이 도착하지 않았을 수 있습니다.

504 게이트웨이 타임아웃은 특별한 주의가 필요합니다. 게이트웨이가 기다리기를 포기했더라도 원본 서버가 요청을 처리했을 수 있습니다. 이 차이점은 멱등성(idempotency)에 도달하면 중요해집니다.

절대 재시도하지 않을 경우:

신호 의미
400 Bad Request 페이로드가 잘못되었습니다. 다음에도 잘못될 것입니다.
401 Unauthorized 자격 증명이 잘못되었거나 만료되었습니다. 토큰을 새로 고치세요, 루프를 돌지 마세요.
403 Forbidden 권한이 없습니다. 재시도해도 권한이 부여되지 않습니다.
422 Unprocessable Entity 유효성 검사가 실패했습니다. 타이밍이 아닌 데이터를 수정하세요.

규칙: 실패가 서버의 상태나 네트워크에 관한 것이라면 재시도하세요. 실패가 클라이언트 요청에 관한 것이라면 빠르게 실패하세요. 429는 그 중간에 있습니다. 재시도 가능하지만, 전체 요청 속도에 문제가 있다는 신호이기도 합니다. 이는 재시도 루프 상위에서 해결해야 할 요청 속도 제한 문제입니다.

지수 백오프 공식과 지터가 중요한 이유

지수 백오프는 각 재시도가 이전 재시도보다 더 오래 기다리며, 기본적으로 두 배씩 증가한다는 의미입니다:

delay = base * 2^retry_count

기본값이 500ms라면 0.5초, 1초, 2초, 4초, 8초가 됩니다. 지연 시간이 분 단위로 늘어나지 않도록 상한선(예: 30초)을 추가합니다:

delay = min(cap, base * 2^retry_count)

이것은 "공격(hammering)" 문제는 해결하지만 동기화 문제는 해결하지 못합니다. 만약 5,000개의 클라이언트가 동시에 실패한다면, 일반적인 지수 백오프는 이 5,000개가 t=0.5초, t=1초, t=2초에 모두 동시에 돌아오게 합니다. 여전히 파도처럼 몰려듭니다. 여전히 떼 지어 몰려들지만, 좀 더 공손한 무리일 뿐입니다.

지터(Jitter)는 지연 시간을 무작위화하여 동기화를 깨뜨립니다. AWS Architecture Blog는 지수 백오프 및 지터 분석에서 경쟁하는 클라이언트와 경합 리소스를 시뮬레이션하여 수치를 분석했습니다. 지터 없는 백오프는 여전히 클러스터링된 호출 급증을 발생시켰습니다. 0에서 지수 상한선 사이에서 무작위 지연을 선택하는 풀 지터는 총 호출 수를 최소화하고 완료 시간을 가장 짧게 만들었습니다:

delay = random_between(0, min(cap, base * 2^retry_count))

이 결과는 사람들을 놀라게 합니다. 지연 시간을 0까지 무작위화하는 것은 깔끔한 두 배 일정과 비교할 때 엉성하게 느껴질 수 있습니다. 그러나 클라이언트를 창 전체에 균일하게 분산시키는 것이 서버 부하를 평평하게 유지하는 비결입니다. AWS 분석은 "균등 지터(equal jitter)" (절반 고정, 절반 무작위)와 "비상관 지터(decorrelated jitter)"도 테스트했습니다. 풀 지터와 비상관 지터가 우위에 있었고, 풀 지터가 올바르게 작성하기 가장 간단합니다. 다른 측정 결과가 없다면 이를 기본 재시도 패턴으로 사용하세요.

서버가 지시할 때 Retry-After를 따르세요

백오프는 클라이언트가 얼마나 기다려야 할지 추측하는 것입니다. 때로는 서버가 이러한 추측을 없애줍니다. 429 및 503 응답에 대해 정의된 Retry-After 헤더는 초 단위 숫자 또는 HTTP 날짜를 포함합니다:

HTTP/1.1 429 Too Many Requests
Retry-After: 12

이 헤더가 존재하면 계산된 백오프를 무시합니다. 서버는 요청 속도 제한 기간이 언제 재설정되거나 유지 보수가 언제 끝나는지 알고 있습니다. 클라이언트의 지수 스케줄은 그렇지 않습니다. 클라이언트가 Retry-After를 무시하는 것은 공급업체가 스로틀링에서 전면 금지로 확대하는 한 가지 이유입니다. 이를 파싱하고, 존중하며, 상한선과 최대 재시도 횟수를 계속 적용하여 악의적이거나 버그 있는 `Retry-After: 86400`이 워커를 하루 동안 멈추게 할 수 없도록 하세요.

멱등성: POST 재시도의 전제 조건

이전에 언급된 504 오류의 함정이 여기 있습니다. GET, PUT, DELETE는 계약상 멱등적입니다. 즉, 두 번 보내도 시스템이 동일한 상태를 유지합니다. POST는 그렇지 않습니다. 만약 서버가 처리한 후 `POST /v1/payments`가 타임아웃되면, 재시도는 두 번째 결제를 생성합니다. 축하합니다, 당신은 뛰어난 가동 시간을 가진 이중 청구 기계를 만들었습니다.

해결책은 멱등성 키입니다: 각 논리적 작업에 대한 헤더로 전송되는 고유한 클라이언트 생성 ID(일반적으로 UUID). 서버는 첫 번째 응답과 함께 키를 저장하고, 중복 요청에 대해 저장된 응답을 재생합니다. Stripe의 멱등성 요청은 정확히 이런 방식으로 작동하며, 대부분의 결제 및 프로비저닝 API가 이를 따르고 있습니다.

두 가지 규칙이 키를 작동시킵니다:

호출하는 API가 멱등성 키를 지원하지 않는다면, 멱등성이 아닌 쓰기 작업을 자동으로 재시도하지 마세요. 실패를 알리고 사람이나 조정 작업이 결정하도록 하세요.

재시도 예산과 서킷 브레이커: 비상 탈출구

백오프는 재시도가 언제 발생하는지 결정합니다. 얼마나 많이 발생하는지는 제한하지 않습니다. 오랜 장애가 발생하면, 잘 지터링된 클라이언트조차 재시도 부하를 쌓고, 계층화된 재시도는 증폭됩니다. API 게이트웨이가 3번 재시도하고 서비스 클라이언트가 3번 재시도하면, 한 번의 사용자 클릭이 9개의 요청이 될 수 있습니다.

두 가지 메커니즘이 피해를 제한합니다:

재시도 예산(Retry budgets). "요청당 3번 재시도" 대신, 슬라이딩 윈도우를 통해 측정된 "재시도는 최대 10%의 추가 부하만 추가할 수 있다"는 규칙을 적용합니다. 예산이 소진되면, 실패는 즉시 반환됩니다. 이는 한 번에 얼마나 많은 요청이 실패하든 재시도 증폭을 제한합니다. Linkerd와 Envoy는 모두 이를 최우선 구성으로 제공합니다.

서킷 브레이커(Circuit breakers). 다운스트림별 실패율을 추적합니다. 임계값을 초과하면 브레이커가 열립니다: 호출은 네트워크를 건드리지 않고 즉시 실패합니다. 쿨다운 후, 몇 번의 프로브 요청이 종속성이 복구되었는지 테스트한 후 브레이커가 다시 닫힙니다. 백오프가 정중하게 쇄도를 늦추는 동안, 브레이커는 이를 취소합니다. 모든 진지한 재시도 설계는 이 두 가지를 함께 사용합니다. 왜냐하면 백오프만으로는 결국 모든 요청을 보내기 때문입니다.

Python으로 구현된 프로덕션 준비 예제

여기 모든 패턴이 한 곳에 있습니다: 재시도 가능한 상태 필터링, 풀 지터, Retry-After 지원, 멱등성 키, 그리고 엄격한 재시도 상한선.

import random
import time
import uuid
import requests

RETRYABLE = {429, 502, 503, 504}
BASE = 0.5     # seconds
CAP = 30.0     # ceiling on any single delay
MAX_RETRIES = 5

def create_payment(payload):
    idempotency_key = str(uuid.uuid4())  # one key per logical payment
    headers = {"Idempotency-Key": idempotency_key}

    for retry_count in range(MAX_RETRIES + 1):
        try:
            resp = requests.post(
                "https://api.acmepay.com/v1/payments",
                json=payload, headers=headers, timeout=10,
            )
            if resp.status_code < 400:
                return resp.json()
            if resp.status_code not in RETRYABLE:
                resp.raise_for_status()  # 400/401/403/422: fail fast
            retry_after = resp.headers.get("Retry-After")
        except (requests.ConnectionError, requests.Timeout):
            retry_after = None  # network fault: fall through to backoff

        if retry_count == MAX_RETRIES:
            raise RuntimeError("payment failed after all retries")

        if retry_after and retry_after.isdigit():
            delay = min(CAP, float(retry_after))
        else:
            delay = random.uniform(0, min(CAP, BASE * 2 ** retry_count))
        time.sleep(delay)

주목할 만한 점: 키는 루프 외부에서 한 번만 생성됩니다. Retry-After는 계산된 백오프보다 우선하지만 상한선을 존중합니다. 재시도 불가능한 상태는 즉시 예외를 발생시킵니다. JavaScript 측이라면 axios-retry 라이브러리가 `retryCondition` 및 `retryDelay` 훅을 통해 동일한 형태를 제공합니다. 의사결정 테이블은 동일하게 유지됩니다.

프로덕션 환경에서 발생하기 전에 재시도 동작을 테스트하는 방법

대부분의 팀은 실패 분기(failure branch)를 한 번도 실행해 보지 않은 재시도 코드를 배포합니다. 성공 경로는 테스트했지만, 503 경로는 실제 장애 중에 처음으로 실행됩니다. Apidog의 두 가지 기능으로 더 잘할 수 있습니다.

목 서버로 실패 시뮬레이션. Apidog의 스마트 목은 `/v1/payments`와 같은 엔드포인트를 정의하고 응답을 스크립팅할 수 있게 해줍니다. 첫 두 호출에는 503을 반환하고 세 번째 호출에는 200을 반환하도록 만들거나, `Retry-After: 5`와 함께 429를 반환하거나, 15초 지연을 추가하여 클라이언트 타임아웃을 트리거할 수 있습니다. 클라이언트를 목 URL로 연결하고 재시도 루프가 각 시나리오를 처리하는 것을 관찰하세요. 프로덕션 사고는 필요 없습니다.

테스트 시나리오로 클라이언트 동작 검증. Apidog 테스트 시나리오는 요청을 검증(assertion) 및 타이밍 확인과 연결합니다. 불안정한 목 서버를 대상으로 실행되는 시나리오를 구축하고, 호출이 결국 성공하는지, 총 경과 시간이 예상되는 백오프 범위 내에 있는지, 그리고 정확히 하나의 리소스가 생성되었는지(멱등성 키가 제 역할을 했음을 증명) 검증하세요. 이 시나리오를 CI에 연결하면, 재시도 로직은 매번 장애가 발생할 때가 아니라 매번 커밋할 때마다 연습됩니다.

이것이 "재시도를 추가했습니다"와 "클라이언트가 요청 속도 제한이 걸리고 부분적으로 다운된 종속성에서도 살아남는지 확인했습니다"의 차이입니다. Apidog를 무료로 다운로드하면 약 10분 만에 클라이언트와 연동되는 실패하는 목 서버를 실행할 수 있습니다.

FAQ

429를 재시도해야 할까요?

네, 그리고 서버가 보통 어떻게 해야 할지 알려주는 유일한 상태 코드입니다. Retry-After 헤더를 읽고 최소한 그만큼 기다리세요. 헤더가 없으면 지터가 있는 지수 백오프로 폴백하세요. 또한 반복되는 429는 정상적인 작동으로 간주하지 말고, 클라이언트 측 스로틀링이나 캐싱으로 요청 속도를 수정해야 한다는 신호로 받아들이세요.

풀 지터(full jitter)란 무엇인가요?

풀 지터는 각 재시도 지연 시간을 0과 지수 상한선 사이에서 균일하게 무작위로 선택하는 것입니다: `random(0, min(cap, base * 2^n))`. 이는 많은 클라이언트로부터 발생하는 동기화된 재시도 파동을 방지합니다. AWS의 시뮬레이션에서 풀 지터는 총 호출 수와 완료 시간 모두에서 일반 백오프 및 균등 지터를 능가했으며, 이것이 AWS SDK의 기본값인 이유입니다.

POST 요청을 재시도해도 안전한가요?

실제로 요청이 멱등적일 때만 안전합니다. POST의 경우, 서버가 중복을 제거할 멱등성 키를 전송하는 것을 의미합니다. 멱등성 키가 없으면, 타임아웃 후 재시도는 결제, 주문 또는 기록을 중복시킬 수 있습니다. 서버가 실패했다고 생각한 요청을 이미 처리했을 수 있기 때문입니다. 쓰기 API를 호출하는 AI 에이전트는 이런 문제에 계속 직면합니다. 에이전트 오류 복구 패턴은 여기서 다루는 것과 동일합니다: 키가 있는 쓰기, 상한선이 있는 재시도, 그리고 서킷 브레이커입니다.

몇 번 재시도해야 할까요?

세 번에서 다섯 번의 재시도는 거의 모든 일시적인 오류를 처리합니다. 그 이상은 성공률이 정체되는 반면 부하와 지연 시간은 계속 증가합니다. 요청당 상한선을 전역 재시도 예산(예: 재시도가 최대 10%의 추가 트래픽을 유발할 수 있음)과 짝지어 전체 장애가 부하를 증폭시키지 않도록 하세요. 종속성이 마지막 재시도 이후에도 다운 상태로 유지된다면, 그것은 재시도 영역이 아닌 서킷 브레이커 영역입니다.

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요