ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Springboot] 외부 HTTP 호출 Retry와 재처리 전략
    Server/Spring Boot 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
    반응형

    댓글

Designed by Tistory.