프로그래밍/디자인패턴

재시도 패턴(Retry Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 20:45

"API 호출이 실패했습니다. 0.1초 뒤에 다시 해보고, 또 실패하면 0.1초 뒤에 다시 해봤는데... 결국 서버가 트래픽 폭주로 완전히 뻗어버렸습니다."

일시적인 네트워크 오류(Transient Fault)는 클라우드 환경에서 필연적입니다. 그래서 우리는 **재시도(Retry)**를 합니다. 하지만 실패한 요청 수천 개가 **똑같은 시간에 동시에 재시도를 감행(Thundering Herd)**하면, 간신히 살아나려던 서버를 다시 짓밟는 꼴이 됩니다.

이를 막기 위한 두 가지 재시도 전략을 비교해 봅니다.

Shutterstock
 

1. 방식 1: 고정 간격 재시도 (Fixed Backoff) - "꾸준함의 미학"

가장 단순한 방식입니다. 실패하면 **항상 정해진 시간(예: 1초)**만큼 기다렸다가 다시 시도합니다.

특징

  • 일정함: 1번째 재시도도 1초 대기, 5번째 재시도도 1초 대기.
  • 구현 용이성: 단순한 루프나 타이머로 구현 가능합니다.

코드 예시 (Spring Retry)

Java
 
@Service
public class PaymentService {

    // 최대 3번 시도, 매번 1초(1000ms) 간격으로 대기
    @Retryable(
        maxAttempts = 3, 
        backoff = @Backoff(delay = 1000)
    )
    public void pay() {
        // 네트워크 불안정으로 가끔 실패하는 로직
        externalBankApi.call();
    }
}

치명적 문제점

서버가 과부하로 인해 응답을 못 하는 상황이라고 가정해 봅시다.

  1. 사용자 A, B, C가 동시에 요청 실패.
  2. 셋 다 정확히 1초 뒤에 동시에 재요청.
  3. 서버는 또다시 순간적인 부하를 견디지 못하고 실패.
  4. (무한 반복)

2. 방식 2: 지수 백오프 (Exponential Backoff) - "배려의 기술"

실패할 때마다 대기 시간을 지수적으로 늘리는($2^n$) 방식입니다. 여기에 **무작위성(Jitter)**을 더하는 것이 표준입니다.

특징

  • 점진적 증가: 처음엔 1초, 그다음엔 2초, 4초, 8초... 뒤에 시도합니다.
  • Jitter(지터): 정확히 2초가 아니라 2초 + 랜덤(0~100ms) 시간에 시도하여 요청을 분산시킵니다.
  • 서버 보호: 서버가 복구될 시간을 충분히 벌어줍니다.

코드 예시 (Resilience4j / Java)

Java
 
// 순수 자바 로직 예시 (개념 설명용)
public void callWithExponentialBackoff() {
    int maxRetries = 5;
    long waitTime = 1000; // 초기 대기 1초

    for (int i = 0; i < maxRetries; i++) {
        try {
            externalApi.call();
            return; // 성공 시 탈출
        } catch (Exception e) {
            // 지수 증가 (Multiplier: 2)
            waitTime *= 2; 
            
            // Jitter 추가 (요청 분산을 위해 랜덤 시간 더함)
            long jitter = (long) (Math.random() * 100); 
            
            System.out.println((waitTime + jitter) + "ms 뒤에 다시 시도합니다.");
            Thread.sleep(waitTime + jitter);
        }
    }
    throw new RuntimeException("결국 실패");
}

Spring Retry / Resilience4j 설정

YAML
 
resilience4j:
  retry:
    instances:
      backendA:
        maxRetryAttempts: 3
        waitDuration: 500ms
        enableExponentialBackoff: true # 지수 백오프 활성화
        exponentialBackoffMultiplier: 2 # 2배씩 증가

장단점

  • 장점: 요청이 분산되므로 서버 부하를 줄이고 시스템 안정성을 높입니다. 클라우드 네이티브 앱의 표준입니다.
  • 단점: 사용자가 느끼는 응답 지연 시간(Latency)이 길어질 수 있습니다. (마지막 재시도는 10초 뒤일 수도 있음)

3. 실무 비교: 언제 무엇을 쓰는가?

구분 Fixed Backoff (고정 간격) Exponential Backoff (지수 간격)
대기 시간 1초 -> 1초 -> 1초 1초 -> 2초 -> 4초 -> 8초
구현 난이도 낮음 중간 (알고리즘 또는 라이브러리 필요)
서버 부하 높음 (동시 재시도 충돌 위험) 낮음 (충분한 회복 시간 제공)
사용자 경험 실패 여부를 빨리 알 수 있음 실패 확정까지 오래 걸림
추천 상황 사내 내부망, 충돌 가능성이 낮은 DB 조회 외부 API 호출, 클라우드 환경, 모바일 앱

결론

"단순히 DB 커넥션이 잠깐 튀었다" 정도라면 **고정 간격(Fixed)**으로 2~3번 빠르게 재시도하는 것이 낫습니다. 사용자를 오래 기다리게 할 필요가 없으니까요.

하지만 **"외부 결제 API를 호출하거나, 마이크로서비스 간 통신"**을 한다면 반드시 **지수 백오프(Exponential Backoff) + 지터(Jitter)**를 사용해야 합니다. 이것이 대규모 장애 상황에서 내 서비스와 상대방 서비스를 모두 살리는 **"우아한 재시도"**의 정석입니다.

주의: 재시도를 무한정 하면 안 됩니다. 일정 횟수 이상 실패하면 **서킷 브레이커(Circuit Breaker)**를 열어서 아예 요청을 차단해야 합니다. (재시도 패턴과 서킷 브레이커는 단짝 친구입니다.)