Server/Spring Boot

[Springboot] 외부 HTTP 호출 Retry와 재처리 전략

hoonylab 2025. 4. 15. 14:28
728x90
반응형

외부 API를 호출할 때는 항상 실패 가능성을 염두에 두고 안정적인 Retry 정책을 세워야 합니다. 이 글에서는 Spring Boot 환경에서 Resilience4j를 활용한 Retry재시도 큐(DB + 스케줄러) 전략을 함께 설명합니다.


✅ 1. 어떤 요청에 Retry를 적용해야 할까?

  • Retry 가능한 요청
    • GET, HEAD, OPTIONS 같은 멱등한 요청
    • HTTP 5xx 또는 네트워크 예외 (timeout 등)
  • Retry 피해야 할 요청
    • POST, PUT, DELETE 같은 멱등하지 않은 요청
    • 비즈니스 오류나 중복 처리 위험이 있는 경우

✅ 2. 어떤 예외/상태코드에 Retry를 걸어야 할까?

  • Retry 권장: SocketTimeoutException, ConnectTimeoutException, HTTP 502/503/504
  • Retry 피함: HTTP 400/401/403/404 또는 사용자 요청 오류

✅ 3. Retry 간격(backoff) 전략

  • Fixed delay: 일정 간격으로 재시도 (ex. 3초마다)
  • Exponential backoff: 1초 → 2초 → 4초 등 점점 늘리기
  • Jitter: 랜덤 요소 추가해 트래픽 폭주 방지
resilience4j.retry:
  instances:
    myRetry:
      max-attempts: 3
      wait-duration: 2s
      retry-exceptions:
        - java.io.IOException
        - java.net.SocketTimeoutException
      ignore-exceptions:
        - com.example.exception.BusinessException

✅ 4. Resilience4j란?

Resilience4j는 마이크로서비스 환경에서 흔히 사용하는 장애 대응(Fault Tolerance) 라이브러리입니다.

  • Retry - 실패 시 자동 재시도
  • Circuit Breaker - 계속 실패 시 요청 차단
  • Rate Limiter - 트래픽 제한
  • Time Limiter - 시간 초과 제어
  • Bulkhead - 리소스 격리

장점: 가볍고, Spring Boot와 궁합 좋으며, Micrometer 기반 모니터링 가능

@Retry(name = "myRetry", fallbackMethod = "fallback")
@CircuitBreaker(name = "myCircuit", fallbackMethod = "fallback")
public String callExternalApi() {
    return restTemplate.getForObject("https://external-api.com", String.class);
}

public String fallback(Exception ex) {
    return "기본 응답값";
}

✅ 5. Retry 후에도 실패하면? → DB 저장 + 스케줄러 재시도

Retry로도 실패할 경우, 요청을 DB에 저장하고 스케줄링으로 재시도하는 방식이 실무에서 많이 사용됩니다.

📌 흐름

  1. HTTP 요청 시도 → 실패
  2. Retry 시도 → 계속 실패
  3. 요청 정보를 DB에 저장
  4. 스케줄러가 주기적으로 조회해서 다시 시도

📋 테이블 예시

CREATE TABLE failed_requests (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  request_url VARCHAR(255),
  payload TEXT,
  headers TEXT,
  retry_count INT DEFAULT 0,
  status VARCHAR(20) DEFAULT 'PENDING',
  last_attempt_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

⚙️ 실패 저장 코드 예시

try {
    restTemplate.postForObject(url, payload, String.class);
} catch (Exception ex) {
    failedRequestRepository.save(new FailedRequest(url, payload, headers));
}

⏱ 스케줄러 재시도 코드

@Scheduled(fixedDelay = 60000)
public void retryFailedRequests() {
    List<FailedRequest> list = repository.findPending();
    for (FailedRequest req : list) {
        try {
            restTemplate.postForObject(req.getUrl(), req.getPayload(), String.class);
            req.markSuccess();
        } catch (Exception ex) {
            req.incrementRetry();
            if (req.getRetryCount() > 5) {
                req.markFailed();
            }
        }
        repository.save(req);
    }
}

✅ 마무리 정리

  • 1차 방어: Resilience4j로 Retry 설정
  • 2차 보완: DB에 실패 요청 저장
  • 3차 복구: 스케줄러로 재시도 및 알림/모니터링

이 방식은 결제 시스템, 외부 정산 API, 메시지 연동 등에서 데이터 유실 없이 안정적인 운영을 가능하게 합니다.


🎁 더 나아가고 싶다면 Kafka나 Redis Stream을 활용한 비동기 재처리 구조도 고려해볼 수 있습니다.

728x90
반응형