openFeign Retry 처리하기

openFeign Retry 처리하기

생성일
Nov 7, 2023 06:32 AM
상위 항목
최종 편집 일시
Last updated October 11, 2024
태그
DEV
JAVA
SpringBoot
하위 항목
openFeign은 기본적으로 retry 처리를 하지 않는다.
따라서 retry 처리를 하고 싶다면 몇 가지 방법이 있다.
  1. openFeign의 Retryer를 설정한다.
  1. spring의 retry를 사용한다.
  1. resilience4j의 retry를 사용한다.
 
이 중 나는 3번을 선택해서 구현했다. 무엇을 선택하든 지금 구현하려는 retry 기능은 다 똑같이 동작하도록 할 수 있다. 다만 spring-cloud 진영의 것들을 앞으로 더 사용할 것을 대비해서 cloud 진영에서 resilience4j와 openFeign을 세트로 처리할 수 있기 때문에 3번을 선택하게 되었다.
resilience4j는 여러가지 기능을 제공한다.
  • resilience4j-circuitbreaker: Circuit breaking
  • resilience4j-ratelimiter: Rate limiting
  • resilience4j-bulkhead: Bulkheading
  • resilience4j-retry: Automatic retrying (sync and async)
  • resilience4j-cache: Result caching
  • resilience4j-timelimiter: Timeout handling
이런 코어 모듈을 가지고 있다.
보통 retry와 circuitbreaker를 같이 사용하는데 내가 구현한 feign 클라이언트는 보통의 MSA 구조에서 사용하는 우리 내부 서비스 간의 통신을 목적으로 구현한 것이 아니라 외부 API 서비스를 사용하는데 restTemplate을 openFeign 방식으로 변경한 것이기 때문에 굳이 circuitbreaker는 사용하지 않았다.
외부 서비스가 공적 서비스라 딱히 장애가 날 일도 없고 retry를 1초 간격으로 3번까지만 시도하기 때문에 저쪽 서버를 죽일 일도 없다고 생각했다.
 
그래서 의존성 적용을
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
위에 것이 아니라 boot3용 resilience4j만 추가했다.
implementation("io.github.resilience4j:resilience4j-spring-boot3")
나중에 서킷브레이커가 필요하면 위의 것으로 바꾸면 될 것 같다.
 
 
아무튼 구현 내용은 간단하다.
Feign은 Exception을 만나면 FeignException을 발생시킨다.
따라서 이 FeignException의 status코드를 판단해서 재시도를 시킬 것이다.
 
@Configuration @RequiredArgsConstructor public class Resilience4jConfig { private final RetryRegistry retryRegistry; @Bean public RetryConfig customRetryConfig() { Predicate<Throwable> retryOn500SeriesStatus = throwable -> throwable instanceof FeignException && ((FeignException) throwable).status() / 100 == 5; return RetryConfig.custom() .maxAttempts(3) .waitDuration(Duration.ofSeconds(1)) .retryOnException(retryOn500SeriesStatus) // 5xx 상태 코드에서만 재시도 .build(); } @Bean public Retry customRetry(RetryConfig customRetryConfig) { return Retry.of("customRetry", customRetryConfig); } }
 
Resilience4jConfig라는 클래스를 만들어서 @Configuration을 붙여서 설정을 적용한다.
 
500대가 아닌 다른 400대 에러들은 외부 API에서 세부 에러 응답 코드와 에러 내용을 보내주기 때문에 해당 처리는 ErrorDecoder를 구현한 FeignErrorDecoder라는 클래스를 만들어서 처리했다.
@Slf4j @RequiredArgsConstructor @Component public class FeignErrorDecoder implements ErrorDecoder { private final ObjectMapper objectMapper; @Override public Exception decode(String methodKey, Response response) { String body = null; KftcBaseResponse kftcBaseResponse = null; try { body = Util.toString(response.body().asReader(StandardCharsets.UTF_8)); log.info("feign decode response: {}", body); kftcBaseResponse = objectMapper.readValue(body, KftcBaseResponse.class); } catch (IOException e) { throw new ErrorException(ErrorStatus.KFTC_SERVER_EXCEPTION); } return new KftcErrorException(kftcBaseResponse); } }
 
그리고 retry 3회가 실패한 경우에는 fallbackMethod를 실행하도록 했다.
@Component @FeignClient( name = "retry-test", url = "http://localhost:8080/", configuration = {DefaultFeignConfig.class}) public interface RetryTestClient { @Retry(name = "retryTest", fallbackMethod = "retryFallback") @GetMapping("test/retry") String exception(); default String retryFallback(Exception e) { ObjectMapper om = new ObjectMapper(); Map<String, String> fallbackResponse = new HashMap<>(); fallbackResponse.put("rsp_code", "9999"); fallbackResponse.put("rsp_message", "3회 재시도에도 응답이 없습니다."); try { return om.writeValueAsString(fallbackResponse); } catch (JsonProcessingException jsonEx) { return "{\"rsp_code\": \"9999\", \"rsp_message\": \"JSON 처리 오류\"}"; } } }
메서드 별로도 @Retry를 적용할 수 있고 인터페이스 위에 붙여서 클라이언트 전체에 적용되게 할 수도 있다.
그래서 DefaultFeignConfig에 아예 Retry를 넣어서 전역으로 적용되게 하려고 했는데 실패했다.
(이건 그냥 써야겠다…)
 
FallbackMethod를 테스트한다고 저렇게 작성해 놨는데 위의 전역 적용에 실패한 것 때문에 이제 내가 쓰는 모든 FeignClient에 retryFallback 메서드를 공통으로 작성해야 된다. (복붙…?)
 
이 아니고 Factory 패턴을 사용한다.
나는 Feign의 Fallback을 사용하는 게 아니고 resilience4j의 fallback을 사용하기 때문에 feign의 FallbackFactory를 구현하는 것으로는 해결이 안된다.
 
결국은 @Retry어노테이션에 들어갈 메서드가 각 feignClient 인터페이스 안에 구현이 되어 있어야 한다.
처음엔 추상 클래스를 만들어서 상속시키면 되는 거 아닌가라고 생각했지만 feign은 클래스가 아니고 인터페이스라 스태틱 메서드를 만들어서 해결했다.
 
public class FeignClientDefaultFallback { private FeignClientDefaultFallback() {} public static String retryFallback(Exception e) { ObjectMapper om = new ObjectMapper(); Map<String, String> fallbackResponse = new HashMap<>(); fallbackResponse.put("rsp_code", "9999"); fallbackResponse.put("rsp_message", "3회 재시도에도 응답이 없습니다."); try { return om.writeValueAsString(fallbackResponse); } catch (JsonProcessingException jsonEx) { return "{\"rsp_code\": \"9999\", \"rsp_message\": \"JSON 처리 오류\"}"; } } }
 
공통으로 가져다 쓸 default fallback 클래스를 만들고 그 안에 retryFallback이라는 static 메서드를 만들었다.
 
@FeignClient( name = "kftc-goods", url = "${kftc.api.url}", configuration = {DefaultFeignConfig.class, FeignRequestInterceptor.class}) @Retry(name = "customRetry", fallbackMethod = "retryFallback") public interface GoodsClient { @PostMapping(value = "/goods") String insertGoods( @RequestBody InsertGoodsRequest request, @RequestHeader(value = "requester") String header); ... default String retryFallback(Exception e) { return FeignClientDefaultFallback.retryFallback(e); }
이런 식으로 모든 FeignClient에 retryFallback을 스태틱 메서드를 리턴해서 만들어 주고 어노테이션에 fallbackMethod 이름을 달아줘서 해결했다.
이것도 맨 아래 retryFallback 메서드 부분이 중복이긴 하지만 현재로써는 어쩔 수 없는 것 같다. circuitbreaker까지 적용을 하면 아예 거기서 fallback이 떨어져서 뭔가 다른 방법이 있을 것도 같지만 지금은 필요 없는 기능인 것 같다.