본문 바로가기
프로그래밍/Spring Cloud

[MSA] Spring Cloud Feign - 커스터 마이징 설정편

by 사바라다 2020. 9. 30.

안녕하세요. 오늘은 Spring Cloud Feign의 2번째 시간입니다. 오늘 함께 알아볼 내용은 Spring Cloud Feign의 설정을 커스터마이징 할 수 있는 것은 어떤것들이 있으며 어떻게 커스터마이징 할 수 있는가에 대해서입니다. FeignClient에 커터마이징 설정을 적용하기 위해서는 아래와 같이 @FeignClient에서의 configuration 속성을 이용하면 됩니다. 아래처럼 했을 때 AzureClient는 커스터마이징 설정인 AzureHttpConfiguration과 오버라이딩 되지 않은 부분에 대해서는 기본설정인 FeignClientsConfiguration가 적용됩니다.

@FeignClient(name = "azureClient", url = "${external.bing.url}", configuration = AzureHttpConfiguration.class)
public interface AzureClient {
    ...
}

그렇다면 아래에서 차례로 어떤 설정을 커스터마이징 할 수 있는지 확인해보겠습니다.

기본 설정 커스터 마이징

Logger feignLogger

먼저 Logger입니다. Feign에서의 아무런 설정을 해주지 않은 로거는 로그를 찍지 않는다고 했습니다. 기본적으로 Feign에서 제공하는 로거에서도 일정부분 조정을 해주실 수 있습니다. 그렇게 하지 위해서는 Configuration에서 Logger.Level을 아래와같이 조정해주셔야합니다. 조정할 수 있는 레벨은 아래와 같습니다.

  • NONE : 별도의 추가 로깅을 하지 않음.
  • BASIC : 요청 메서드와 URL과 응답 상태코드, 그리고 실행시간을 로깅합니다.
  • HEADERS : BASIC 단계의 로깅과 request와 response의 headers를 로깅합니다.
  • FULL : request, response의 headers, body 그리고 metadata를 모두 로깅합니다.

아래의 코드는 커스텀 Configuration에 로깅 레벨을 변경한 것입니다. 모든 로그를 찍기위해서 저는 FULL로그 레벨 Bean을 생성하였습니다. 이렇게 하면 기본 설정에 Logger.Level의 bean이 오버로딩되는 효과가 있습니다. 그리고 주의하실점은 @FeginClient의 패키지가 로깅레벨 DEBUG로 되어있어야합니다. 아래의 application.yml 파일을 보시면 아래처럼 설정해 주시면되며, 저는 해당 FeignClient가 들어있는 package만 적용하였습니다.

public class AzureHttpConfiguration {
  @Bean
  Logger.Level feignLoggerLevel() {
  return Logger.Level.FULL;
  }

  ...
}
logging:
  level:
    personal.project.markl.core.infra: DEBUG // 각자 FeignClient가 있는 경로 설정

RequestInterceptor

RequestIntercepor는 공통으로 사용하는 header를 추가하기위해 사용할 수 있습니다. RequestInterceptor는 interface이며 apply 메서드를 정의할 수 있습니다.

public interface RequestInterceptor {

  /**
   * Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
   */
  void apply(RequestTemplate template);
}

아래 예제는 api-version: 3.0으로 header를 추가 등록합니다. 저번 포스팅에서는 @RequestHeader 어노테이션를 FeignClient의 메서드에 넣어주었습니다. 이렇게 하면 여러메서드에 대해서 공통 Header에 대해 중복이 많이 발생하게됩니다. 그런부분을 RequestInterceptor를 이용하면 공통화하여 사용할 수 있게되는 것입니다.

public class AzureHttpConfiguration {

  @Bean
  public RequestInterceptor requestInterceptor() {
    return requestTemplate -> {
      requestTemplate.query("api-version", "3.0");
    };
  }
}

그리고 Basic Auth는 Feign에서 구현하여 기본으로 제공하고 있습니다. 아래와 같이 이용할 수 있습니다.

@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
    return new BasicAuthRequestInterceptor("user", "password");
}

Encoder & Decoder

Feign은 통신 Framework입니다. 따라서 Encoder와 Decoder가 필수로 필요합니다. 데이터 포맷으로 요즘 가장 많이 사용하는 게 json, xml, String 등이 있습니다. 하지만 실제 기계적인 통신은 결국 byte Array로 통신됩니다. 따라서 특정 포맷 -> byte Array는 인코딩, byte Array -> 특정 포맷은 디코딩이라고 하는데요. 다양한 포맷 및 표현 방법(UTF-8 등등)이 있기 때문에 목적주소마다 충분히 다를 수 있습니다. 이에 대한 설정을 해줄 수 있는 부분이 해당 설정입니다. 아래를 보시면 JAXB를 이용해 xml형식을 사용하며 UTF-8의 문자열형식을 유지하겠다는 예제입니다.

@Bean
public Encoder encoder() {
  return new JAXBEncoder(new JAXBContextFactory.Builder()
      .withMarshallerJAXBEncoding("UTF-8")
      .build());
}

@Bean
public Decoder decoder() {
  return new JAXBDecoder(new JAXBContextFactory.Builder()
      .withMarshallerJAXBEncoding("UTF-8")
      .build());
}

ErrorDecoder

Feign은 가장 기본적으로 4xx, 5xx에 대해 ErrorDecoder.default를 이용합니다. ErrorDecoder.default의 처리를 보면 어떤 에러든 FeignException로 반환하고 있습니다. 따라서 에러에 대해서 로깅을 추가한다던지 에러코드에 대해 다른 Exception을 발생시키는 등 좀 더 정밀한 에러 핸들링이 필요할 경우에는 ErrorDecoder의 커스터마이징이 필요합니다. 첫번째로 ErrorDecoder를 implements 받아 에러에 대해서 어떻게 처리를 할 것인지를 정합니다. 아래의 예제는 로그를 남기고 좀 더 정밀하게 Exception을 분할해서 처리하도록 하였습니다. 그리고 이렇게 만든 커스텀 ErrorDecoder를 커스텀 Configuration에 붙이면 사용할 수 있게 됩니다.

@Slf4j
public class CustomErrorDecoder implements ErrorDecoder {

  @Override
  public Exception decode(String methodKey, Response response) {
    log.info("%s 요청이 성공하지 못했습니다. status : %s, body : %s", methodKey, response.status(), response.body());

    switch (response.status()){
            case 400:
                return new BadRequestException();
            case 404:
                return new NotFoundException();
            default:
                return new Exception("Generic error");
        }
  }
}

/* In CustomFeignConfiguration */
@Bean
public ErrorDecoder errorDecoder() {
  return new CustomErrorDecoder();
}

FeignFormatterRegistrar

URL 요청 파라미터로 날짜를 요청하는 경우가 종종있습니다. 그런 API는 아래와 같이 작성할 수 있을것입니다. FeignClient로 아래처럼 작성하며 날짜가 yyyy-MM-dd의 형식으로 나가길 원합니다. 하지만 실제 결과를 보면 hello?startDate=20.%209.%2030.&endDate=20.%2010.%201.&api-version=3.0로 나가느것을 확인할 수 있습니다. LocalDate Type을 FeignClient의 기본 url 인코딩을 통해 나가 이렇게 표현되는 것으로 보여집니다. 따라서 2가지의 선택지가 있습니다. @DateTimeFormat를 이용하는 방법과 FeignFormatterRegistrar를 이용하는 방법입니다.

@FeignClient(name = "적당한 이름", url = "적당한 URL")
public interface AzureClient {

  @GetMapping("hello")
  String localDate(@RequestParam("startDate") LocalDate from, @RequestParam("endDate") LocalDate to);
}

아래 처럼 @DateTimeFormat 어노테이션을 이용하면 yyyy-MM-dd 형식으로 나가게됩니다. hello?startDate=2020-09-30&endDate=2020-10-01&api-version=3.0

@FeignClient(name = "적당한 이름", url = "적당한 URL")
public interface AzureClient {

  @GetMapping("hello")
  String localDate(
    @RequestParam("startDate") @DateTimeFormat("yyyy-MM-dd") LocalDate from,
    @RequestParam("endDate") @DateTimeFormat("yyyy-MM-dd") LocalDate to);
}

이렇게 하면 단점이 각 파라미터마다 명시를 해줘야한다는 것입니다. 아래처럼 Configuration을 이용하면 모든 LocalDate에 적용됩니다.

@Bean
  public FeignFormatterRegistrar localDateFeignFormatterRegister() {
    return registry -> {
      DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
      registrar.setUseIsoFormat(true);
      registrar.registerFormatters(registry);
    };
  }

Retryer

오늘 마지막으로 알아 볼 커스텀 Configuration은 Retryer입니다. Retryer는 실패에 대해서 재시도를 해주는 옵션입니다. Feign에서의 Retryer는 Ribbon에서의 Retryer과는 상관이 없으며 독자적으로 Retryer를 시도합니다. Retyer는 기본적으로 꺼져있습니다. 사용하기 위해서는 아래처럼 Configuration에 Bean을 등록해주시면 됩니다.

@Bean
public Retryer retryer() {
  return new Retryer.Default();
}

Feign에서 기본적으로 제공하는 Retryer는 아래와 같은 스펙을 가지고 있습니다. 초기화 되는 변수로 maxAttempts, period, maxPeriod가 있습니다. 각각, maxAttempts는 최대 시도 수, period는 각 시도간의 차이, maxPeriod는 모든 재시도 사이의 시간입니다. 재밌는 부분은 nextMaxInterval() 메서드를 보시면 각 시도의 interval에 1.5 또는 (시도 수 - 1)를 곱해서 시도수가 늘어날수록 간격이 늘어나게 되어있습니다.

/* IN Retryer.Default */
class Default implements Retryer {

  private final int maxAttempts;
  private final long period;
  private final long maxPeriod;
  int attempt;
  long sleptForMillis;

  public Default() {
    this(100, SECONDS.toMillis(1), 5);
  }

  public Default(long period, long maxPeriod, int maxAttempts) {
    this.period = period;
    this.maxPeriod = maxPeriod;
    this.maxAttempts = maxAttempts;
    this.attempt = 1;
  }
  ...

  long nextMaxInterval() {
      long interval = (long) (period * Math.pow(1.5, attempt - 1));
      return interval > maxPeriod ? maxPeriod : interval;
    }
  }
}

마무리

오늘은 이렇게 Spring Cloud Feign을 커스터마이징을 하는 방법에 대해서 알아보았습니다.

다음 시간에는 FeignClient와 Hystrix를 함께 사용하는 법에 대해서 알아보는 시간을 가지도록 하겠습니다.

감사합니다.

참조

medium_feign-client-configure-date-time-format-for-request-parameter

우아한_형제들_Feign

우아한_형제들_Feign_2

spring_cloud_openfeign_reference_doc

댓글