Retry, Backoff, Jitter

2026. 10. 4. 22:43·CS
 

Rate Limiter 알아보기

이전 글 — Consistent Hashing 알아보기 Consistent Hashing 알아보기사용자 0명에서 수백만 명까지, 서비스는 어떻게 확장될까? #2사용자 0명에서 수백만 명까지, 서비스는 어떻게 확장될까? #1최근에 꽤

constant1601.tistory.com

 

이전 글에서는 과도한 요청으로부터 시스템을 보호하기 위한 Rate Limiter에 대해 알아보았다.

사용자별로 일정 시간 동안 허용할 요청을 제한하고, 제한을 초과한 요청에는 429 Too Many Requests를 반환할 수 있었다.

 

그런데 Client 입장에서는 요청이 제한되었다고 해서 원하는 작업을 바로 포기하고 싶지는 않을 것이다.

잠시 기다렸다가 다시 요청하면 정상적으로 처리될 수도 있다. 일시적인 네트워크 오류나 Server의 장애가 발생한 경우에도 비슷하게 생각해볼 수 있다.

 

그렇다면 요청이 실패했을 때 단순히 다시 보내면 되는 것일까?

이번 글에서는 Retry가 어떻게 동작하는지 살펴보고, 재시도로 인해 발생할 수 있는 문제를 Exponential Backoff와 Jitter로 어떻게 줄일 수 있는지 알아보려고 한다.

Retry

Retry는 실패한 요청을 다시 시도하는 것이다.

예를 들어 Client가 Server에 데이터를 조회하는 요청을 보냈는데, 일시적인 오류로 정상 응답을 받지 못했다고 가정해보자.

오류의 원인이 사라진 뒤 같은 요청을 다시 보내면 이번에는 성공할 수도 있다.

일시적인 오류 뒤 같은 조회 요청을 재시도해 성공하는 Retry 흐름

분산 시스템에서는 모든 요청이 항상 성공한다고 보장하기 어렵다. 일부 요청만 실패하거나, 짧은 시간 동안 연결이 불안정해질

수도 있기 때문이다.

이처럼 일시적인 실패를 다시 시도하여 복구할 수 있도록 만드는 것이 Retry의 기본적인 목적이다.

모든 실패를 다시 시도해도 될까?

하지만 요청이 실패했다는 이유만으로 모두 재시도하는 것은 적절하지 않다.

잘못된 요청 데이터를 보냈다면 같은 요청을 다시 보내도 결과가 달라지지 않을 수 있다. 권한이 없는 사용자의 요청 역시 단순히 반복한다고 해결되지는 않는다.

상황 재시도를 생각할 때 확인할 점
429 Too Many Requests 제한이 풀릴 시간을 기다린다. Retry-After가 있다면 참고한다.
503 Service Unavailable 등 일시적인 Server 오류 API의 오류 설명과 재시도 정책을 확인한다.
Timeout / 연결 끊김 Server에서 이미 처리되었을 가능성도 함께 고려한다.
400 Bad Request 잘못된 요청이라면 데이터를 수정해야 한다.
401 Unauthorized / 403 Forbidden 인증이나 권한 문제를 해결한 뒤 요청해야 한다.

상태 코드만으로 모든 API의 동작을 결정할 수는 없다. 예를 들어 401 이후 인증 정보를 갱신해서 다시 보내는 것은 같은 요청을 무작정 반복하는 것과 다르다.

결국 다시 시도하면 해결될 가능성이 있는지와 같은 작업을 반복해도 안전한지를 함께 확인해야 한다.

실패하자마자 다시 요청한다면?

재시도할 수 있는 요청이라면 가능한 한 빨리 다시 보내는 것이 좋아 보일 수 있다.

하지만 요청이 실패한 원인이 Server의 과부하라면 어떻게 될까?

Server가 이미 많은 요청을 처리하느라 느려진 상태에서 Client들이 재시도 요청을 추가로 보내면 부하는 더 커질 수 있다.

Server 과부하와 재시도가 서로 부하를 키우는 Retry Storm

요청 증가 → Server 과부하 → 응답 지연과 실패 → Retry 증가 → 더 큰 과부하

 

예를 들어 원래 요청 1,000개가 모두 실패하고, 각 Client가 두 번씩 추가로 재시도한다고 가정해보자.

처음 요청 1000개에 재시도 2000개가 더해져 총 3000번의 시도가 발생할 수 있다. 

 

장애를 복구하기 위해 추가한 Retry가 오히려 장애를 키우는 것이다. 많은 재시도 요청이 몰리는 이런 상황을 Retry Storm이라고 부른다.

앞에서 Rate Limiter로 요청을 제한했더라도, Client들이 거부된 요청을 즉시 반복하면 제한을 검사하는 계층에도 불필요한 부하가 쌓일 수 있다.

 

그렇다면 요청을 바로 다시 보내는 대신, 잠시 기다린 뒤 다시 시도할 수는 없을까?

Backoff

Backoff는 재시도하기 전에 일정 시간 기다리는 것이다.

가장 단순하게는 실패할 때마다 1초씩 기다리는 방법을 생각해볼 수 있다.

요청 → 실패 → 1초 대기 → 재시도 → 실패 → 1초 대기 → 재시도

 

이처럼 고정된 간격으로 기다리면 즉시 재시도하는 것보다 요청의 빈도를 줄일 수 있다.

하지만 장애가 계속되는 동안에도 모든 Client가 같은 빈도로 요청을 반복하게 된다.

 

실패가 반복될수록 조금 더 오래 기다린다면 Server가 회복할 시간을 더 줄 수 있지 않을까?

Exponential Backoff

Exponential Backoff는 실패가 반복될 때 재시도 대기 시간을 지수적으로 늘리는 방식이다.

예를 들어 첫 재시도까지 1초를 기다리고, 이후에는 대기 시간을 두 배씩 늘릴 수 있다.

1초 → 2초 → 4초 → 8초 → 16초 → …

고정 간격과 지수적으로 증가하는 대기 간격 비교

위 그림은 비교를 쉽게 하기 위해 각 요청이 실패하기까지 걸리는 시간을 생략했다. 대기 간격이 1초, 2초, 4초, 8초라면 시도 시각은 누적되어 0초, 1초, 3초, 7초, 15초가 된다.

대기 시간을 계속 늘려도 될까?

하지만 계속 두 배씩 늘리면 대기 시간이 너무 길어질 수 있다. 따라서 보통 한 번에 기다릴 수 있는 최대 시간을 설정한다.

backoff(n) = min(maxDelay, baseDelay × 2n)
n = 0: 첫 번째 재시도 전 대기

 

baseDelay가 1초이고 maxDelay가 8초라면 대기 간격은 다음과 같다.

1초 → 2초 → 4초 → 8초 → 8초 → …

 

이처럼 상한을 둔 방식을 Capped Exponential Backoff라고 한다.

다만 대기 간격의 상한을 정했다고 해서 전체 재시도가 끝나는 것은 아니다. 최대 재시도 횟수나 전체 제한 시간도 별도로 정해야 한다.

그런데 모두 같은 시간에 다시 요청한다면?

Exponential Backoff를 적용하면 요청 간격은 점점 길어진다.

하지만 많은 Client가 비슷한 시각에 실패하고 같은 규칙을 사용한다면 또 다른 문제가 남는다.

Client A → 1초 대기 → 2초 대기 → 4초 대기
Client B → 1초 대기 → 2초 대기 → 4초 대기
Client C → 1초 대기 → 2초 대기 → 4초 대기

 

모두 비슷한 시간에 다시 요청하므로, 재시도 사이에 조용한 구간이 생겼다가 특정 시점에 요청이 몰릴 수 있다.

즉, 재시도 빈도를 줄이는 것과 여러 Client의 재시도 시점을 분산하는 것은 서로 다른 문제이다.

 

이를 줄이기 위해 사용할 수 있는 방법이 Jitter이다.

Jitter

Jitter는 대기 시간에 무작위성을 넣어 여러 Client의 재시도 시점을 분산하는 방법이다.

모든 Client가 정확히 1초를 기다리는 대신, 각자 조금씩 다른 시간을 기다리게 만드는 것이다.

동기화된 재시도와 Full Jitter로 분산된 재시도 시각 비교

위 그림에서는 Client A, B, C가 서로 다른 시각에 재시도한다. 대기 시간은 매번 선택한 난수에 따라 달라진다.

Full Jitter

Jitter를 적용하는 방법은 여러 가지가 있다. 여기서는 Full Jitter를 살펴보자.

먼저 Exponential Backoff로 대기 시간의 상한을 구한 뒤, 0부터 그 상한 사이에서 대기 시간을 무작위로 선택한다.

upperBound(n) = min(maxDelay, baseDelay × 2n)
delay(n) = random(0, upperBound(n))

 

baseDelay가 1초이고 maxDelay가 8초라면, 첫 번째 재시도는 0~1초, 두 번째는 0~2초, 세 번째는 0~4초 범위에서 기다리게 된다.

따라서 실제로 선택된 대기 시간이 매번 길어지는 것은 아니다. 점점 늘어나는 것은 무작위로 선택할 수 있는 범위의 상한이다.

상한에 도달한 뒤에도 0~8초 범위에서 매번 다시 선택하므로, 모두 정확히 8초 간격으로 반복하는 상황을 줄일 수 있다.

다만 무작위로 분산한다고 해서 요청이 절대 겹치지 않는 것은 아니다. 순간적으로 몰릴 가능성을 낮추는 것이며, Server의 처리 용량 자체를 늘리는 방법은 아니다.

Retry-After는 어떻게 사용할까?

이전 글에서 Rate Limiter가 다음과 같이 응답할 수 있다고 설명했다.

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

 

Retry-After: 10은 응답을 받은 뒤 10초 기다리라는 의미이다. HTTP 날짜 형식으로 다음 시도를 기다릴 시점을 표현할 수도 있다.

Client가 이 안내를 무시하고 0~1초의 Jitter만 적용하면 Server가 요청한 대기 시간보다 너무 빨리 다시 보내게 된다.

 

따라서 유효한 Retry-After가 있고 이를 따르는 정책이라면, 안내된 시간보다 이른 시각에 재시도하지 않도록 대기 시간을 계산해야 한다.

예를 들어 Server가 안내한 시간을 최소 대기 시간으로 삼고, 그 뒤에 작은 양수의 Jitter를 더하는 정책을 사용할 수 있다. 이는 하나의 설계 예시이며 API나 Client 라이브러리의 정책에 따라 계산 방법은 달라진다.

 

안내된 대기 시간이 전체 제한 시간보다 길다면 기다린 뒤 재시도하기보다 이번 작업을 종료하는 것이 맞을 수도 있다. Retry-After를 기다렸다고 해서 다음 요청의 성공이 보장되는 것도 아니다.

언제까지 다시 시도할까?

Backoff와 Jitter를 적용했더라도 무한히 재시도하면 Client와 Server의 자원을 계속 사용하게 된다.

따라서 재시도 정책에는 대기 시간 외에도 몇 가지 제한이 필요하다. 아래는 대표적인 지표를 정리한 예시이다.

설정 의미 예시
Max Attempts 첫 요청을 포함한 최대 시도 횟수 4번 = 최초 1번 + 재시도 최대 3번
Per-attempt Timeout 한 번의 시도에서 기다릴 최대 시간 2초
Total Timeout / Deadline 시도와 대기를 모두 포함한 전체 제한 시간 10초
Max Delay Backoff로 계산한 대기 간격의 상한 8초

 

여러 계층에서 각각 재시도한다면?

서비스 호출이 여러 단계로 이어져 있을 때도 주의해야 한다.

예를 들어 Service A가 B를 호출하고, B가 C를 호출하고, C가 Database를 호출한다고 가정해보자.

세 호출 계층의 재시도가 Database 시도를 최대 27번으로 증폭시키는 구조

각 호출 계층이 최초 요청을 포함해 최대 3번 시도한다면, 모든 하위 요청이 계속 실패하는 경우 Database에는 최대 3 × 3 × 3 = 27번의 시도가 전달될 수 있다.

 

각 계층에서는 적은 횟수만 재시도했더라도 전체적으로 보면 호출이 크게 증폭되는 것이다.

특히 SDK가 이미 자동 재시도를 제공하는데 애플리케이션에서 다시 반복문을 추가하면, 의도하지 않게 재시도가 중첩될 수 있다.

 

따라서 어느 계층이 재시도를 책임질지 정하고, 전체 시도 횟수와 시간 예산을 함께 관리해야 한다. 필요하다면 일정 시간 동안 허용할 재시도 수 자체를 제한하는 Retry Budget도 사용할 수 있다.

응답을 못 받았다면 정말 실패한 것일까?

여기까지는 요청을 언제 다시 보낼지 살펴보았다. 하지만 더 먼저 확인해야 할 문제가 하나 남아 있다.

Client가 주문 생성 요청을 보냈고, 응답을 기다리다가 Timeout이 발생했다고 가정해보자.

Client 입장에서는 요청이 실패한 것처럼 보인다. 그런데 Server에서는 주문을 이미 생성했고, 응답만 전달되지 않았을 수도 있다.

응답 Timeout 이후 중복 주문을 방지하는 Idempotency Key

이 상태에서 동일한 주문 생성 요청을 다시 보내면, API의 동작에 따라 주문이 두 개 만들어질 수 있다.

즉, 응답을 받지 못했다는 사실과 Server에서 작업이 수행되지 않았다는 사실은 같지 않다.

 

이를 안전하게 처리하기 위해 중요한 개념이 Idempotency, 즉 멱등성이다.

멱등성은 같은 작업을 여러 번 수행해도 한 번 수행했을 때와 같은 의도된 효과를 갖는 성질이다. 매번 똑같은 HTTP 상태 코드나 응답 본문을 반환해야 한다는 뜻은 아니다.

 

주문 생성처럼 중복 실행이 문제가 되는 API에서는 하나의 논리적인 작업을 식별하는 Idempotency Key를 사용할 수 있다.

첫 요청: Idempotency-Key = order-request-123
같은 작업의 재시도: Idempotency-Key = order-request-123

 

Server는 이 Key를 기준으로 이미 처리한 작업인지 확인하고 중복 실행을 막을 수 있다. 재시도할 때마다 새로운 Key를 만들면 같은 작업임을 식별하기 어렵다.

 

다만 Client가 헤더를 붙이는 것만으로 해결되지는 않는다. Server에서도 중복 판단과 실제 작업을 안전하게 연결하고, 동시 요청과 Key의 보관 기간 등을 처리해야 한다.

 

이 부분은 별도의 주제로 더 살펴볼 수 있겠지만, Retry를 설계할 때는 실패를 분류하는 것뿐 아니라 같은 작업을 반복할 때의 영향도 확인해야 한다.

정리

처음에는 요청이 실패하면 다시 보내면 되는 문제처럼 보였다.

하지만 실패의 원인이 과부하라면 Retry가 부하를 더 키울 수 있고, 모든 Client가 같은 간격으로 재시도하면 특정 시점에 요청이 다시 몰릴 수 있었다.

 

Exponential Backoff는 실패가 반복될수록 대기 간격의 상한을 늘리고, Jitter는 그 대기 시간에 무작위성을 넣어 재시도 시점을 분산한다.

여기에 재시도할 오류의 종류, 최대 시도 횟수, Timeout과 전체 제한 시간을 정해야 한다. 여러 호출 계층에 재시도가 중첩되어 있지는 않은지도 함께 확인해야 한다.

 

그리고 응답을 받지 못했더라도 Server의 처리는 완료되었을 수 있으므로, 중복 실행의 영향과 멱등성도 고려해야 한다.

결국 Retry는 실패한 요청을 무조건 반복하는 것이 아니라, 일시적인 실패에서 복구할 기회를 만들면서 시스템에 추가되는 부하와 중복 처리의 영향을 제어하는 것이라고 볼 수 있다.

 

 

참고

  • Amazon Builders' Library — Timeouts, retries, and backoff with jitter
  • AWS Architecture Blog — Exponential Backoff And Jitter
  • Azure Architecture Center — Retry pattern
  • Google Cloud — Retry strategy
  • RFC 9110 — Retry-After
  • Amazon Builders' Library — Making retries safe with idempotent APIs
  • Stripe — Idempotent requests

'CS' 카테고리의 다른 글

Rate Limiter  (0) 2026.10.03
Consistent Hashing  (0) 2026.09.30
[네트워크] ICMP 프로토콜  (1) 2026.01.10
[네트워크] ARP 프로토콜  (0) 2026.01.08
[네트워크] 라우팅, 클래스풀 IP, 클래스리스 IP  (0) 2025.12.20
'CS' 카테고리의 다른 글
  • Rate Limiter
  • Consistent Hashing
  • [네트워크] ICMP 프로토콜
  • [네트워크] ARP 프로토콜
chnn_1601
chnn_1601
  • chnn_1601
    개발 일기장
    chnn_1601
  • 글쓰기 관리
    • 분류 전체보기 (77)
      • Back-end (9)
        • Java (2)
        • Spring (4)
      • Infra (8)
      • PS (34)
      • Project (5)
      • Practice (8)
      • CS (8)
      • AI (2)
      • 후기 (3)
      • 잡담 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    백준
    redis
    플로이드 워셜
    자바 코테
    자바코테
    AWS
    DFS
    김영한
    EC2
    자바
    네트워크
    BOJ
    nginx
    유니온 파인드
    데브코스
    BFS
    코테
    MST
    CS
    스프링
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.0
chnn_1601
Retry, Backoff, Jitter
상단으로

티스토리툴바