티스토리 뷰
최근에 있었던 사내 엔지니어링 데이에서 발표를 하였는데, 상당히 편리하지만 많은 분들이 모르시는 OpenFeign을 준비하였습니다. 그리고 발표하면서 준비했던 내용을 블로그 포스팅에서도 적어두었습니다. 아래의 내용은 Spring Cloud OpenFeign 공식 문서를 기반으로 개인적인 경험을 더해 작성한 내용입니다.
1. OpenFeign 타임아웃(Timeout), 재시도(Retry), 로깅(Logging) 등 설정하기
OpenFeign의 설정은 yml과 Java config 모두로 할 수 있다. 만약 yml과 Java config 모두 존재한다면 YML의 정보가 Java config를 덮어씌우며 우선순위를 가진다. 하지만 이러한 우선순위 설정은 바꿔줄 수 있으며, 심지어 Feign Client 별로 다르게 가져갈 수도 있다. 클라이언트별로 설정을 다르게 가져가려면, @Configuration이 붙은 설정 클래스를 FeignClient에 붙여주면 된다.
이번에는 이러한 기본적인 지식을 바탕으로 타임아웃(Timeout), 재시도(Retry), 로깅(Logging) 등을 설정해보자.
[ 타임아웃(Timeout) 설정하기 ]
OpenFeign이 Defaut로 갖는 tiemout 설정은 다음과 같다.
- connectTimeout: 1000
- readTimeout: 60000
만약 connectionTimeout과 readTimeout을 모두 5초로 변경해서 사용하려면 다음과 같이 설정할 수 있다. 아래와 같이 설정하면 모든 Feign Client들의 타임아웃이 5초로 적용된다.
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
[ 재시도(Retry) 설정하기 ]
Feign은 기본적으로 Retryer.NEVER_RETRY를 등록하여 Retry를 시도하지 않으므로 Retry를 시키려면 추가적인 설정이 필요하다. 예를 들어 0.1초의 간격으로 시작해 최대 3초의 간격으로 간격이 점점 증가하며, 최대 5번 재시도하는 Retryer는 다음과 같이 설정할 수 있다.
다만 Feign이 제공하는 Retryer는 IOException이 발생한 경우에만 처리되므로, 이외의 경우에도 재시도가 필요하다면 Spring-Retry를 이용하거나 에러디코더 혹은 인터셉터로 직접 구현하는 등의 방법을 사용해야 한다.
@Configuration
@EnableFeignClients("com.mangkyu.openfeign")
class OpenFeignConfig {
@Bean
Retryer.Default retryer() {
// 0.1초의 간격으로 시작해 최대 3초의 간격으로 점점 증가하며, 최대5번 재시도한다.
return new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(3L), 5);
}
}
재시도는 조심해서 설정되어야 한다. 만약 어떤 서버에서 장애가 발생해서 이제 막 복구가 되었는데, 우리의 서버가 재시도(Retry)를 적용하여 수 많은 동시 요청을 보내게 된다면 다시 장애를 유발하는 Retry Storm을 일으킬 수 있기 때문이다.
[ 요청/응답 로깅(Logging) 설정하기 ]
Logger의 이름은 전체 인터페이스 이름이며, Feign Client들마다 만들어진다. Feign은 남길 로그에 따라 4가지 수준을 제공한다.
- NONE: 로깅하지 않음(기본값)
- BASIC: 요청 메소드와 URI와 응답 상태와 실행시간만 로깅함
- HEADERS: 요청과 응답 헤더와 함께 기ㄹ본 정보들을 남김
- FULL: 요청과 응답에 대한 헤더와 바디, 메타 데이터를 남김
만약 로그 수준을 FULL로 하고 싶다면 다음과 같이 Logger.Level을 빈으로 등록 해주면 된다.
@Configuration
@EnableFeignClients("com.mangkyu.openfeign")
class OpenFeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
여기서 주의 사항이 있는데, Feign은 DEBUG 레벨로만 로그를 남길 수 있다. 그러므로 반드시 로그 레벨이 DEBUG로 설정이 되어 있어야 한다. 간단하게 설정 파일에서 다음과 같이 수정해줄 수 있으며, 이것 역시 클라이언트 별로도 적용 가능하다.
logging:
level:
com.mangkyu.openfeign.app.openfeign: DEBUG
만약 이를 INFO 레벨로 남기고 싶다면 별도의 로깅 설정이 필요한데, Feign이 제공하는 Logger를 확장하거나 인터셉터를 사용하는 방법이 있다. 이 링크를 따라 들어가면 Logger를 확장한 예시 코드가 나와 있다. 참고로 예시 코드는 클라이언트 별로 로그가 남지 않는다는 단점이 있다. 대신 인터셉터 등을 활용할 수도 있으므로, 충분히 커스터마이징이 가능하다.
[ LocalDate, LocalDateTime, LocalTime 등을 위한 설정하기 ]
주고 받는 데이터 타입으로 LocalDate, LocalDateTime, LocalTime 등이 사용된다면 제대로 처리가 되지 않는다. 그러므로 DateTimer과 관련된 포매터 추가 설정이 필요하다. Java8 이상에서는 해당 타입들이 자주 사용되므로 기본적으로 등록해두면 좋다.
@Bean
public FeignFormatterRegistrar dateTimeFormatterRegistrar() {
return registry -> {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
};
}
또한 스프링 부트 버전이나 의존성에 따라 ObjectMapper가 해당 타입들을 Serialize, Deserialize 하기 위한 라이브러리가 존재하지 않을 수도 있다. 만약 실패한다면 아래의 2가지 의존성을 추가해주면 된다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
[ OpenFeign 테스트 설정 ]
OpenFeign은 인터페이스와 어노테이션 기반으로 코드를 구현하는 만큼 테스트 없이는 작성한 코드에 문제가 없는지 확인하기가 어렵다. 또한 외부 API 호출의 경우 단독으로 사용되기 보다는 다른 로직들과 결합되어 호출이 진행되어 테스트 하기도 까다롭다. 그러므로 손쉽게 OpenFeign 부분만을 테스트할 수 있다면 매우 유용하다. Spring Data Jpa는 @DataJpaTest를 이용해 손쉽게 테스트를 작성할 수 있는데, OpenFeign은 테스팅 도구를 지원하지 않으므로 @FeignTest를 직접 만들어주면 된다.
먼저 다음과 같이 FeignTest를 위해 필요한 클래스들을 포함하는 FeignTextContext라는 클래스를 만들어준다.
@ImportAutoConfiguration({
OpenFeignConfig.class,
CustomPropertiesConfig.class,
FeignAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class,
})
class FeignTestContext {
}
위에서 FeignAutoConfiguration.class와 HttpMessageConvertersAutoConfiguration.class는 반드시 포함시켜야 하는 것들이다. 이것들에 더해 개발 환경에 맞는 클래스들(OpenFeignConfig.class, CustomPropertiesConfig.class)를 추가해주면 된다. 위에서는 테스트 코드에 Properties가 사용되어 CustomPropertiesConfig라는 직접 구현한 프로퍼티 설정 클래스까지 추가하였다.
그리고 이러한 설정들을 메타 정보로 사용하는 @FeignTest 어노테이션을 만들어주면 된다.
@SpringBootTest(
classes = {FeignTestContext.class},
properties = {
"spring.profiles.active=local"
})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeignTest {
}
이렇게 만든 어노테이션을 사용하면 다음과 같이 편리하게 테스트 코드를 작성할 수 있다.
@Slf4j
@FeignTest
class ExchangeRateOpenFeignTestWithFeignTest {
@Autowired
private ExchangeRateOpenFeign openFeign;
@Autowired
private ExchangeRateProperties properties;
@Test
void 환율조회() {
ExchangeRateResponse result = openFeign.call(properties.getKey(), Currency.USD, Currency.KRW);
assertThat(result).isNotNull();
log.info("result: {}", result);
}
}
[ OpenFeign 빈 Body 설정 ]
이번에 설명한 부분은 OpenFeign 12.0 버전에 수정되었다. 그러므로 사용중인 OpenFeign의 버전을 확인한 후에 적용하도록 하자.
OpenFeign으로 POST나 PUT 등의 요청을 보낼때, Body가 없을 수 있다. 그런데 OpenFeign은 Body가 없어도 Content-Length을 자동으로 추가해주지 않는다. 그리고 나의 경우에는 Content-Length를 추가해도 다른 서버에서 제대로 처리가 되지 않고, 411 Length Required 에러를 응답받았다. 관련된 이슈는 다른 사람들에게도 재현되는 것으로 보이고, 대응이 필요하다. 그래서 Body가 비어있고, GET이나 DELETE가 아닌 경우에는 반드시 빈 Body를 넣어 요청하도록 인터셉터를 추가하였다. 만약 Content-Length를 0으로 추가해도 동일하게 411 에러가 발생한다면, 빈 Body를 추가해주도록 하자.
@Configuration
@EnableFeignClients(basePackages = "com.worksmobile.eco.ecoapi")
class FeignConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
if(ArrayUtils.isEmpty(requestTemplate.body()) && !isGetOrDelete(requestTemplate)) {
// body가 비어있는 경우에 요청을 보내면 411 에러가 생김 https://github.com/OpenFeign/feign/issues/1251
// content-length로 처리가 안되어서 빈 값을 항상 보내주도록 함
requestTemplate.body("{}");
}
};
}
private boolean isGetOrDelete(RequestTemplate requestTemplate) {
return Request.HttpMethod.GET.name().equals(requestTemplate.method())
|| Request.HttpMethod.DELETE.name().equals(requestTemplate.method());
}
...
}
[ 기타 등등 ]
위에서 설명한 것들 외에도 에러 응답에 따른 후처리를 위한 ErrorDecoder와 같은 것들도 제공하고 있다. 그러므로 필요에 따라서 적합한 모델을 사용해주면 된다.
public class CustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
switch (response.status()){
case 400:
return new BadRequestException();
case 404:
return new NotFoundException();
default:
return new Exception("Generic error");
}
}
}
2. OpenFeign 사용 시 주의 사항
[ 마지막 슬래시(/) 제거 ]
API 요청을 보내야 하는 고객의 URI가 슬래시(/)로 끝났다. 그래서 해당 URI로 요청을 보내 보니 마지막 슬래시가 없는 URI로 요청을 전송하고 있었다.
요청 URI: http://www.naver.com/test/
ASIS: http://www.naver.com/test
BOBE: http://www.naver.com/test/
OpenFeign 소스 코드를 확인해보니 요청 URI가 슬래시(/)로 끝나는 경우 해당 부분을 제거해주는 로직이 RequestTemplate 클래스에 명시적으로 구현되어 있다.
if (target.endsWith("/")) {
target = target.substring(0, target.length() - 1);
}
그래서 찾아보니 해당 건과 관련된 이슈가 이미 등록된 적이 있었고, 다음과 같은 답변이 달렸다. URI 템플릿 명세에 따라 대상을 분할해야 한다고 하는데, 자세히는 이해하지 못했고 여기서 할 책임이 아니라는 것 같다.
어쨌든 마지막 슬래시가 제거되고 있기 때문에, 인터셉터 등을 사용해서 별도의 설정을 해주어야 할 듯 하다. 참고로 Spring은 이거를 어떻게 처리하는지 궁금해서 찾아보니 역시 예상한대로 "/users"와 "/users/"는 별도의 URI로 구분되고 있었다.
추가적으로 가능한 설정 부분은 OpenFeign을 계속 사용해나가면서 채워나가도록 하겠습니다.
최근에 있었던 사내 엔지니어링 데이에서 발표를 하였는데, 상당히 편리하지만 많은 분들이 모르시는 OpenFeign을 준비하였습니다. 그리고 발표하면서 준비했던 내용을 블로그 포스팅에서도 적어두었습니다. 아래의 내용은 Spring Cloud OpenFeign 공식 문서를 기반으로 개인적인 경험을 더해 작성한 내용입니다. 혹시 추가적인 피드백있으면 편하게 남겨주세요! 감사합니다:)
관련 포스팅
- OpenFeign이란? OpenFeign 소개 및 사용법
- OpenFeign 타임아웃(Timeout), 재시도(Retry), 로깅(Logging) 등 설정하기
- OpenFeign에 Resilence4J 서킷 브레이커 적용하는 방법과 예시 및 주의사항
'Spring' 카테고리의 다른 글
[Spring] ObjectMapper의 동작 방식과 SpringBoot가 제공하는 추가 기능들 (16) | 2023.01.10 |
---|---|
[Spring] POJO 프로그래밍과 스프링 프레임워크의 탄생 (8) | 2022.11.29 |
[Spring] OpenFeign이란? OpenFeign 소개 및 사용법 (8) | 2022.11.15 |
[Spring] 스프링의 트랜잭션 전파 속성(Transaction propagation) 완벽하게 이해하기 (23) | 2022.08.22 |
[Spring] 멀티쓰레드 환경에서 MDC를 사용해 요청 별로 식별가능한 로그 남기기 (14) | 2022.07.25 |