재시도, 백오프, 멱등성의 안전한 조합
네트워크 실패를 재시도로 넘길 때는 Backoff와 Jitter로 부하를 분산하고, Idempotency Key로 중복 부작용을 막아야 한다. 실패율, 호출 비용, 부작용 여부, 요청 deadline, 체인 깊이에 따라 Retry, Circuit Breaker, Fallback을 나눠 선택한다.
Script Companion
오디오와 함께 스크립트 보기
- 01
네트워크 요청은 실패할 수 있고, 그 실패가 항상 같은 의미는 아니다. 서버가 일시적으로 과부하일 수도 있고, 네트워크가 불안정할 수도 있으며, 서버는 이미 처리했지만 응답만 유실됐을 수도 있다. 그래서 단순히 실패하면 다시 보낸다는 규칙만으로는 부족하다. Retry, Backoff, Jitter, Idempotency는 일시 장애를 넘기면서도 서버 부하와 중복 부작용을 함께 다루기 위한 기본 방어 기제다.
- 02
프론트엔드에서도 fetch 실패 후 재시도하거나, navigator.onLine을 보고 오프라인이 풀린 뒤 요청을 다시 보내는 패턴을 쓴다. 개념은 서버 사이드와 같다. 다만 프론트엔드 재시도는 보통 단일 사용자의 요청 흐름에서 일어나지만, 서버 사이드에서는 초당 수천 개의 서비스 간 호출이 동시에 실패할 수 있다. 여러 탭이 동시에 온라인으로 돌아오는 경우처럼 작은 규모에서도 같은 문제가 나타날 수 있다.
- 03
전체 흐름은 세 겹으로 볼 수 있다. Retry는 실패한 요청을 다시 시도하는 것이고, Backoff는 실패 직후 바로 몰아치지 않도록 재시도 간격을 점점 늘리는 것이다. Jitter는 여러 클라이언트가 같은 초에 다시 몰리는 일을 막기 위해 대기 시간에 랜덤성을 섞는다. Idempotency는 같은 요청을 여러 번 보내도 서버가 이미 처리한 요청으로 인식해 결과가 중복되지 않게 만드는 장치다.
- 04
Jitter가 중요한 이유는 Thundering Herd 문제 때문이다. 서버 장애가 풀리는 순간 모든 클라이언트가 같은 간격으로 재시도하면, 회복 중인 서버가 다시 과부하에 빠질 수 있다. AWS Architecture Blog의 100개 동시 클라이언트 시뮬레이션에서는 jitter를 넣었을 때 호출 수가 절반 이하로 줄었다. AWS SDK 문서도 대부분의 SDK가 jitter가 포함된 truncated binary exponential backoff를 쓴다고 설명한다.
- 05
재시도 설계의 첫 번째 기준은 어떤 에러를 다시 시도할지 고르는 것이다. 네트워크 오류, 429, 503, 500, 502처럼 일시적 장애나 과부하 가능성이 있는 경우에는 재시도가 의미가 있다. 반대로 400, 422, 401, 403, 404는 요청 자체나 권한, 리소스 존재 여부의 문제라서 같은 요청을 반복해도 해결되지 않는다. 4xx까지 재시도 조건에 넣으면 실패를 회복하는 것이 아니라 불필요한 부하만 만든다.
- 06
Backoff에는 반드시 경계가 필요하다. cap 없이 지수 증가만 적용하면 10회 시도 후 대기시간이 17분, 20회면 12일이 될 수 있다. 반대로 cap만 있고 재시도 횟수 제한이 없으면 모든 클라이언트가 cap 간격으로 영구 재시도할 수 있다. 그래서 maxRetries, 전체 요청 deadline, Circuit Breaker를 함께 봐야 하며, 사용자 결제 API처럼 전체 deadline이 8초인 흐름에서는 서버가 사용자가 포기한 뒤에도 계속 재시도하지 않게 설계해야 한다.
- 07
Idempotency는 부작용이 있는 요청에서 핵심이 된다. GET, PUT, DELETE는 같은 요청을 여러 번 보내도 최종 결과가 같을 수 있지만, POST로 주문이나 결제를 만들면 요청 횟수만큼 새 리소스가 생길 수 있다. Idempotency Key는 각 요청에 고유 키를 붙여 서버가 같은 키의 요청을 중복 처리하지 않게 한다. Stripe 공식 문서는 첫 요청의 status code와 body를 저장하고 같은 키의 후속 요청에 같은 결과를 반환하며, 키는 최소 24시간 이후 pruning될 수 있다고 설명한다.
- 08
Idempotency Key를 설계할 때는 키의 종류와 TTL 만료를 함께 봐야 한다. UUID v4는 범용 API에 쓰기 쉽지만 DB 인덱스 단편화가 생길 수 있고, UUID v7은 타임스탬프와 랜덤 비트 조합으로 B-Tree 인덱스에 순차 삽입되어 고쓰기 환경에 적합하다. 비즈니스 키는 orderId:userId:action처럼 재처리 가능 여부를 비즈니스 로직으로 판단할 때 유용하지만, 충돌 가능성을 미리 설계해야 한다. TTL이 지난 키가 다시 들어오면 새 요청으로 처리될 수 있다는 점도 운영상 중요한 경계다.
- 09
마이크로서비스 체인에서는 Retry Amplification을 조심해야 한다. AWS Builders Library는 5단계 호출 체인에서 각 계층이 3회씩 독립 재시도하면 최하위 데이터베이스 부하가 243배까지 늘 수 있다고 설명한다. A에서 B, B에서 C로 이어지는 3단계에서도 각 단계가 3회 재시도하면 C에는 27개의 트래픽이 몰릴 수 있다. 그래서 AWS 공식 권장 대응은 엔드 클라이언트만 재시도하고, 중간 서비스는 에러를 상위로 넘기며, Retry Budget과 엔드포인트별 정책, Circuit Breaker를 함께 쓰는 방향이다.
- 10
패턴 선택은 실패율만 보고 정하지 않는다. 최근 5분 실패율, 호출 비용, 부작용 여부, 상위 요청 deadline, 체인 깊이를 같이 봐야 한다. 읽기 전용 프로필 조회에서 503이 1에서 2퍼센트라면 짧은 Retry와 Backoff가 합리적일 수 있지만, POST /payments는 504가 1건뿐이어도 응답 유실 뒤 중복 청구가 생길 수 있어 Idempotency Key 없이는 재시도하지 않는다. IETF Idempotency-Key draft도 같은 키가 다른 payload에 재사용되면 422, 원 요청이 아직 처리 중이면 409로 응답하는 시나리오를 제시한다.
- 11
대표적인 장애 징후도 같은 원리로 읽을 수 있다. 서버가 복구되는 듯하다가 다시 죽는다면 Jitter 없는 고정 간격 재시도로 Thundering Herd가 생겼을 수 있고, 결제가 두 번 청구된다면 처음 요청은 처리됐지만 응답만 유실된 상태에서 새 Idempotency Key로 다시 보냈을 수 있다. Redis에 Idempotency Key를 저장했는데도 중복 처리된다면 GET, 처리, SET 사이의 Race Condition을 의심해야 한다. 이때는 externalOrderId나 merchantOrderId 같은 비즈니스 키로 요청을 묶어 확인하고, 필요하면 중복 청구 취소, idempotency cache 복구, external_order_id unique constraint 추가 순서로 복구한다.
- 12
정리하면 Retry는 일시적 실패를 다시 시도하는 장치이고, Backoff와 Jitter는 그 재시도가 하위 시스템을 다시 밀어붙이지 않게 만드는 장치다. Idempotency Key는 결제와 주문처럼 부작용이 있는 요청에서 응답 유실과 중복 실행을 분리해 다루게 해준다. 실패가 오래 지속되면 Retry만으로는 부족하므로 Circuit Breaker로 시도 자체를 끊고, Queue Worker나 SQS, Redis 기반 저장소를 쓸 때도 메시지 ID만 믿지 말고 비즈니스 키 기준의 중복 처리 여부를 확인해야 한다.
같은 레이어