공부한것들을 정리하는 블로그 입니다.
지수 백오프(Exponential Backoff) 본문
지수적 백오프: 재시도 전에 기다리는 시간을 직전 재시도 대비 두 배씩 늘려 나가는 방안. 예를 들어, 요청이 처음 실패하면 1초 후에 재시도하고, 두 번째로 실패하면 2초, 세 번째로 실패하면 4초를 기다린 후 재시도한다.
지수 백오프는 네트워크 상에서 일시적인 오류가 발생했을 때, 재시도 간격을 점진적으로 늘려가며 재시도를 수행하는 알고리즘이다. 이 방법은 주로 네트워크의 혼잡을 피하거나, 서버가 과부하 상태일 때 과도한 요청을 방지하기 위해 사용된다. 이 방법의 핵심은, "지수적으로" 대기 시간을 늘리는 것이다.
지수 백오프 구현 예시
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
public class ExponentialBackoffRetry {
private static final int MAX_RETRIES = 5; // 최대 재시도 횟수
private static final int INITIAL_DELAY_MS = 1000; // 초기 대기 시간 (1초)
private static final int MAX_DELAY_MS = 16000; // 최대 대기 시간
public static void main(String[] args) {
String targetUrl = "https://example.com/api";
boolean success = false;
int attempt = 0;
int delay = INITIAL_DELAY_MS;
while (attempt < MAX_RETRIES && !success) {
try {
attempt++;
System.out.println("시도 #" + attempt + ": " + targetUrl);
int responseCode = sendRequest(targetUrl);
if (responseCode == 200) {
System.out.println("✅ 성공적으로 응답 받음");
success = true;
} else {
throw new IOException("응답 코드: " + responseCode);
}
} catch (IOException e) {
System.err.println("❌ 실패: " + e.getMessage());
if (attempt < MAX_RETRIES) {
System.out.println("⏳ " + delay + "ms 후 재시도합니다...");
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
delay = Math.min(delay * 2, MAX_DELAY_MS); // 지수 증가
} else {
System.err.println("🚫 모든 재시도 실패");
}
}
}
}
private static int sendRequest(String urlStr) throws IOException {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000); // 연결 타임아웃
conn.setReadTimeout(3000); // 읽기 타임아웃
conn.setRequestMethod("GET");
return conn.getResponseCode();
}
}
* 5번째 재시도의 경우, 지수적 백오프의 영향은 받지 않음
Q1. 5번 모두 재시도한 후, 6번째 재시도는 어떻게 될까?
답변: 실행되지 않는다.
코드에서 while (attempt < MAX_RETRIES && !success) 조건에 따라 루프가 실행되므로, attempt가 5가 되면 루프 조건을 만족하지 않게 됨. 즉, 최대 5번까지만 시도하고, 6번째 재시도는 절대 실행되지 않음.
또한, attempt++는 루프 시작 직후에 수행되므로, 실제로는 attempt == 5일 때 마지막 시도를 하고, 그 이후에는 루프를 빠져나가게 됨.
Q2. 총 재시도에 소요된 시간은?
답변: 15초 ~ 30초
지수적 백오프 로직에서 Thread.sleep(delay)에 의해 대기하는 시간은 다음과 같이 증가:
| 시도 번호 | 대기 시간 (ms) |
| 1 | 1000 |
| 2 | 2000 |
| 3 | 4000 |
| 4 | 8000 |
| 5 | 없음 (마지막 시도 후 종료) |
총 대기 시간 = 1초 + 2초 + 4초 + 8초 = 15초
하지만 여기서 주의할 점은 요청 자체에 걸리는 시간도 있다는 것. sendRequest()에서 connectTimeout과 readTimeout이 각각 3초로 설정되어 있으므로, 각 시도마다 최대 3초까지 소요될 수 있음.
따라서 최악의 경우:
- 요청 시간: 5회 × 3초 = 15초
- 대기 시간: 15초
- 총합: 30초
실무에서
실무에서는 지수적 백오프의 총 대기 시간이 클라이언트 응답 지연(latency)과 비교해 너무 길어지지 않도록 조절하는 게 핵심
📌 예시: Google API 가이드라인
Google Cloud API에서는 다음과 같은 백오프 전략을 권장:
- 초기 지연: 1초
- 최대 재시도: 5회
- 최대 지연: 32초
- Jitter 적용 필수
✅ 실무에서의 효과
- 서버 부하 분산
- 장애 상황에서의 회복 안정성 향상
- API Rate Limit 회피 가능성 증가
왜 Jitter가 필수적인가?
지수적 백오프만 사용할 경우, 모든 클라이언트가 동일한 실패 시점에 동일한 간격으로 재시도하게 됨. 예를 들어, 수천 개의 클라이언트가 동시에 실패하고 1초 → 2초 → 4초...로 재시도 할 경우
서버는 특정 시점에 수천 개의 요청을 한꺼번에 받게 되고, 오히려 더 큰 장애를 유발할 수 있음.
이걸 "재시도 폭풍(retry storm)"이라고 부르기도 함
✅ Jitter의 역할
- 재시도 타이밍을 분산시켜 서버 부하를 완화
- Rate Limit 초과 방지
- 장애 복구 안정성 향상
- 서버의 처리 여유 확보
📌 실무 적용 예시
- AWS, Google Cloud, Netflix 등 대형 시스템에서는 지수적 백오프 + Jitter를 기본 전략으로 사용.
- 특히 Jitter를 적용하지 않은 백오프는 위험하다는 경고도 공식 문서에 명시돼 있음.
동기식 vs 비동기
지수적 백오프는 동기/비동기 모두 적용 가능하며, 환경과 목적에 따라 선택 가능
다만, 실무에서는 비동기 방식이 확장성과 안정성 면에서 훨씬 더 많이 사용.
✅ 비동기식 백오프가 선호되는 이유
- 대규모 트래픽 처리
- 수많은 클라이언트가 동시에 요청할 수 있는 환경 (예: 모바일 앱, IoT 디바이스, 웹 프론트엔드)
- 동기 방식은 스레드를 블로킹하므로 확장성이 떨어짐
- 고성능 서버 환경
- 서버는 수천 개의 요청을 동시에 처리해야 하므로, 비동기 방식으로 재시도 타이밍을 분산시키는 게 유리함
- 리액티브 프로그래밍 환경
- Spring WebFlux, Project Reactor, RxJava 같은 리액티브 프레임워크에서는 비동기 + 논블로킹이 기본
- 이런 환경에서는 Mono.delay, Observable.timer 등을 활용해 백오프 구현 가능
- 사용자 경험(UX) 개선
- 클라이언트 앱에서 UI가 멈추지 않도록 비동기 재시도 처리
- 예: API 실패 시 로딩 스피너 유지하면서 백그라운드에서 재시도
📌 실무 예시
- AWS SDK: 내부적으로 비동기 백오프 + Jitter 적용
- Google API Client: ExponentialBackOff 클래스에서 비동기 재시도 지원
- Netflix Hystrix / Resilience4j: 비동기 백오프 + Circuit Breaker 조합
예시) 결제 시스템에서 지수적 백오프를 사용할 수 있는 경우?
- 비동기 처리 영역
- 예: 결제 승인 요청이 실패했지만, 백엔드에서 일정 시간 내 재시도 가능할 때
- 사용자에게 즉시 실패를 알리고, 백엔드에서 보상 트랜잭션을 시도하는 구조 ( Saga 패턴(SAGA Pattern) )
- 외부 API 연동 시
- 카드사/은행 API가 일시적으로 장애일 경우, Rate Limit 회피나 서버 부하 완화를 위해 백오프 적용
- 단, 이 경우에도 최대 지연 시간은 수 초 이내로 제한하는 게 일반적
- 비핵심 로직 (예: 포인트 적립, 알림 전송 등)
- 결제 자체와 직접 관련 없는 부가 기능은 백오프를 자유롭게 적용 가능
⚠️ 지수적 백오프가 부적절한 경우
- 사용자 응답이 실시간으로 필요한 경우
- 예: 결제 승인 결과를 즉시 받아야 하는 POS 시스템, 온라인 쇼핑몰 결제창
- 이 경우 10초 이상 지연되면 사용자 이탈이나 중복 결제 등의 문제가 발생할 수 있음
- 트랜잭션 일관성이 중요한 경우
- 재시도 중에 중복 요청이 발생하면 이중 결제나 정산 오류로 이어질 수 있음
- 따라서 재시도 로직은 idempotency와 트랜잭션 관리가 철저히 되어야 함
Q. 그러면 지수적 백오프에는 어떤 행위가 보장되어야 하는걸까?
지수적 백오프(Exponential Backoff)는 단순히 "재시도 간격을 늘리는 전략"이지만, 이 전략이 실무에서 안전하고 효과적으로 작동하려면 몇 가지 핵심 행위가 반드시 보장되어야 함.
✅ 지수적 백오프에서 반드시 보장되어야 하는 행위
| 멱등성 (Idempotency) | 같은 요청을 여러 번 보내도 결과가 한 번만 처리된 것과 동일해야 함. 중복 결제, 중복 생성 방지. |
| 상태 추적 (State Awareness) | 이전 요청이 성공했는지 실패했는지, 서버가 어떤 상태인지 파악할 수 있어야 함. |
| 재시도 조건 명확화 | 어떤 오류에 대해 재시도할지, 어떤 경우에는 즉시 실패 처리할지 기준이 명확해야 함. (예: 500은 재시도, 400은 실패) |
| 최대 재시도 횟수 제한 | 무한 재시도 방지. 시스템 자원 낭비와 장애 확산을 막기 위해 제한 필요. |
| Jitter 적용 | 재시도 타이밍을 분산시켜 서버 부하를 완화. 동시 재시도 폭풍 방지. |
| 로깅 및 모니터링 | 재시도 이력, 실패 원인, 응답 시간 등을 기록하여 장애 분석과 보상 처리에 활용 가능해야 함. |
| Fallback 전략 | 재시도 실패 시 대체 경로, 사용자 메시지, 보상 트랜잭션 등 후속 조치가 준비되어 있어야 함. |
지수적 백오프는 단순한 재시도 전략이 아니라, 시스템의 안정성과 신뢰성을 높이기 위한 복합적인 설계 요소야. 이 전략이 제대로 작동하려면 멱등성, 상태 추적, 재시도 조건, Jitter, Fallback 같은 행위들이 함께 보장되어야 함.
실무에서 사용하려는 개발자라면, 백오프를 단순한 "재시도 간격 조절"로 보지 않고, 시스템 전체의 복원력(resilience)을 설계하는 관점으로 접근하는 게 정말 중요.
'경력 실무경험 > 생각해볼만한 주제' 카테고리의 다른 글
| 트레이드 오프(Trade-off) (0) | 2025.09.29 |
|---|---|
| 최종적 일관성(Eventual Consistency) (0) | 2025.09.29 |
| CQRS(Command Query Responsibility Segregation) 패턴 (0) | 2025.09.04 |
| 결제 시스템의 분석과 설계, 그리고 멱등성 (3) | 2025.08.31 |
| Kafka와 ZooKeeper ( 분산 메시징 시스템 :Kafka / 분산 시스템 코디네이터 : ZooKeeper ) (3) | 2025.08.25 |
| 알림 시스템의 분석과 설계, 그리고 분산 시스템 (7) | 2025.08.25 |
| 트위터 시스템 디자인 생각해보기 (1) | 2023.05.10 |
| 결제 도메인의 네트워크 예외처리(망취소) (0) | 2023.04.30 |
