OpenFeign은 Netflix에 의해 처음 만들어진 Declarative(선언적인) HTTP Client 도구로써, 외부 API 호출을 쉽게 할 수 있도록 도와준다. 여기서 선언적인 이란 어노테이션 사용을 의미하는데, Open Feign은 인터페이스에 어노테이션들만 붙여주면 구현이 된다.
@Component
@RequiredArgsConstructor
class ExchangeRateRestTemplate {
private final RestTemplate restTemplate;
private final ExchangeRateProperties properties;
private static final String API_KEY = "apikey";
public ExchangeRateResponse call(final Currency source, final Currency target) {
return restTemplate.exchange(
createApiUri(source, target),
HttpMethod.GET,
new HttpEntity<>(createHttpHeaders()),
ExchangeRateResponse.class)
.getBody();
}
private String createApiUri(final Currency source, final Currency target) {
return UriComponentsBuilder.fromHttpUrl(properties.getUri())
.queryParam("source", source.name())
.queryParam("currencies", target.name())
.encode()
.toUriString();
}
private HttpHeaders createHttpHeaders() {
final HttpHeaders headers = new HttpHeaders();
headers.add(API_KEY, properties.getKey());
return headers;
}
}@FeignClient(name = "ExchangeRateOpenFeign", url = "${exchange.currency.api.uri}")
public interface ExchangeRateOpenFeign {
@GetMapping
ExchangeRateResponse call(
@RequestHeader String apiKey,
@RequestParam Currency source,
@RequestParam Currency currencies);
}
Open Feign은 Spring Cloud 기반의 기술이므로 Spring Cloud에 대한 의존성이 필요하다.
[아래는 스프링 3.2.x 기준]
ext {
set('springCloudVersion', "2023.0.4")
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'@EnableFeignClients 어노테이션을 붙여주면 된다.
Main에 @Enable 어노테이션을 붙여주는 것은 SpringBoot가 제공하는 테스트에 영향을 줄 수 있으므로 별도의 config 파일로 만들어주자. 별도의 파일로 설정할 경우에는 feign 인터페이스들의 위치를 반드시 지정해주어야한다.
@Configuration
@EnableFeignClients(basePackages = "Journey.Together.global.external")
class OpenFeignConfig {
}
API 호출을 수행할 클라이언트는 인터페이스에 @FeignClient 어노테이션을 붙여주면 된다. 그리고 value와 url설정이 필요한데 url에는 호출할 주소를, value에는 임의의 client의 이름을 적어주면 된다.
value는 다른 스프링 클라우드와의 통합 시에 SpringCloud LoadBalancer Client를 만드는 데 사용된다는데 중요하지 않으므로 넘어가도 되지만, 필수값이므로 반드시 입력해주어야 한다. value와 url에는 placeholder가 사용가능하므로 프로퍼티에 작성해둔 값을 사용하면 된다.
@FeignClient(
name = "discord-client",
url = "${discord.webhook-url}",
configuration = DiscordFeignConfiguration.class)
public interface DiscordClient {
@PostMapping()
void sendAlarm(@RequestBody DiscordMessage message);
}
@FeignClient(name = "ExchangeRateOpenFeign", url = "USE_DYNAMIC_URI")
public interface DynamicUrlOpenFeign {
@GetMapping
ExchangeRateResponse call(
URI uri
@RequestHeader String apiKey,
@RequestParam Currency source,
@RequestParam Currency currencies);
}OpenFeign이 Defaut로 갖는 timeout 설정은 다음과 같다.
connectTimeout : 1000readTimeout: 60000원하는 시간으로 변경하려면 다음과 같이 설정할 수 있다 (ex. 5초)
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
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을 일으킬 수 있기 때문이다.
Logger의 이름은 전체 인터페이스 이름이며, Feign Client들마다 만들어진다. Feign은 남길 로그에 따라 4가지 수준을 제공한다.
NONE: 로깅하지 않음(기본값)BASIC: 요청 메소드와 URI와 응답 상태와 실행시간만 로깅함HEADERS: 요청과 응답 헤더와 함께 기ㄹ본 정보들을 남김FULL: 요청과 응답에 대한 헤더와 바디, 메타 데이터를 남김@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
주고 받는 데이터 타입으로 LocalDate, LocalDateTime, LocalTime 등이 사용된다면 제대로 처리가 되지 않는다. 그러므로 관련 포매터 추가 설정이 필요하다.
@Bean
public FeignFormatterRegistrar dateTimeFormatterRegistrar() {
return registry -> {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
};
}
라이브러리가 존재하지 않는다면 아래의 2가지 의존성을 추가해주면 된다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'