Spring/Spring

Spring Retry로 안전한 Feign 호출 처리하기

ghyeong 2024. 11. 20. 12:44

비즈니스 로직을 처리하다 보면 비동기적으로 데이터가 생성되거나, 외부 API 호출 시 타이밍 이슈로 데이터가 준비되지 않아 실패하는 상황을 자주 겪게 됩니다. 특히, Feign 클라이언트를 사용할 때 외부 API가 아직 데이터 준비를 마치지 않은 상태라면 null 값이나 예외가 발생할 수 있습니다.

최근 저 역시 이런 문제를 경험했는데, 이를 해결하기 위해 Spring Retry를 활용해 재시도 로직을 도입했고, 최종적으로 실패한 경우에만 알림을 전송하는 구조로 개선했습니다. 이 과정을 공유하고자 합니다.


문제 상황

  1. Feign 호출 실패
    외부 API 호출 시 데이터가 아직 생성되지 않아 예외(null)가 발생.
  2. 재시도 필요
    외부 시스템이 데이터를 생성하는 데 약간의 지연 시간이 필요하므로, 요청을 일정 시간 간격으로 재시도할 필요가 있었음.
  3. 최종 실패 처리
    모든 재시도가 실패한 경우에만 텔레그램으로 알림을 보내야 했음. 기존 로직에서는 예외 발생 시 즉시 알림이 전송되었기 때문에 불필요한 경고가 너무 많이 발생.

Spring Retry 도입

Spring Retry를 활용하여 아래와 같은 동작을 구현했습니다.

  • Feign 호출 실패 시 최대 3번까지 재시도.
  • 재시도 간격은 1초.
  • 모든 재시도가 실패한 경우, 텔레그램 알림 전송.

구현 코드

1. Feign Client 정의

먼저 Feign 클라이언트를 정의합니다.

@FeignClient(name = "example-service", url = "http://example.com")
public interface ExampleFeignClient {
    @PostMapping("/api/data")
    ResponseDto fetchData(@RequestBody RequestDto requestDto);
}
 

2. 서비스 클래스에 @Retryable 적용

@Retryable 애노테이션을 사용해 재시도 로직을 추가합니다. 모든 재시도가 실패하면 @Recover 메서드가 호출됩니다.

@Service
public class ExampleService {

    private final ExampleFeignClient feignClient;
    private final TelegramService telegramService;

    public ExampleService(ExampleFeignClient feignClient, TelegramService telegramService) {
        this.feignClient = feignClient;
        this.telegramService = telegramService;
    }

    @Retryable(
        value = { FeignException.class }, // 재시도 대상 예외
        maxAttempts = 3,                  // 최대 재시도 횟수
        backoff = @Backoff(delay = 1000)  // 재시도 간격 (1초)
    )
    public void fetchDataAndProcess(RequestDto requestDto) {
        ResponseDto response = feignClient.fetchData(requestDto);
        if (response == null || !response.isSuccessful()) {
            throw new FeignException.BadRequest("API 호출 실패");
        }
        // 비즈니스 로직 처리
        processResponse(response);
    }

    private void processResponse(ResponseDto response) {
        // 응답 데이터 처리 로직
        System.out.println("응답 처리 완료: " + response);
    }

    @Recover
    public void handleFailure(FeignException e, RequestDto requestDto) {
        // 최종 실패 시 텔레그램 알림 전송
        telegramService.sendMessage(
            "데이터 처리 실패\n요청 데이터: " + requestDto + "\n에러 메시지: " + e.getMessage()
        );
        System.err.println("최종 실패 처리: " + e.getMessage());
    }
}

동작 설명

  1. Feign 호출
    fetchDataAndProcess 메서드는 ExampleFeignClient를 호출해 데이터를 가져옵니다.
  2. Retryable
    호출 중 FeignException이 발생하면 최대 3번까지 재시도하며, 각 재시도는 1초 간격으로 이루어집니다.
  3. Recover
    재시도가 모두 실패하면 handleFailure 메서드가 호출되어 텔레그램으로 실패 알림을 전송합니다.

원래 로직의 경우 실패시에는 무조건 텔레그램을 발송하는 로직이 있었습니다.

저는 retry를 할 경우 3번의 시도중에 실패하는 경우마다 텔레그램이 발송이 되기 때문에 모든 retry가 실패했을 경우에만 텔레그램을 받고 싶어 recover를 통해 retry가 모두 실패했을 경우에만 텔레그램을 발송할 수 있도록 하여 이렇게 로직을 수정 하였습니다.

반응형

장점

  • 자동 재시도: 실패 상황에서 수동으로 재시도를 구현하지 않아도 되므로 코드가 깔끔해집니다.
  • 최종 실패 처리: @Recover를 사용해 모든 시도가 실패했을 때만 별도로 처리할 수 있습니다.
  • 재시도 정책 설정 가능: 재시도 간격, 최대 횟수 등 정책을 유연하게 설정할 수 있습니다.

적용 시 주의사항

  1. 비동기 작업과 병렬 처리
    재시도가 필요한 작업이 많을 경우, 병렬 처리 시 스레드 충돌이 발생하지 않도록 주의해야 합니다.
  2. 재시도 정책 과도 설정 방지
    재시도 횟수나 간격을 과도하게 설정하면 시스템 부하가 증가할 수 있으니 적절히 조율해야 합니다.
  3. 장애 감지
    @Recover에서는 알림을 보내는 외에도, 실패시에 발생 할 수 있는 장애 모니터링 시스템과 연동하면 더욱 효과적입니다.

결론

Spring Retry는 재시도가 필요한 로직을 깔끔하고 유연하게 처리할 수 있는 방법입니다. 특히, Feign 클라이언트와 같이 외부 시스템과의 통신에서 발생하는 타이밍 이슈를 완화하는 데 유용합니다.