Spring Cloud OpenFeign

이 글은 공식 문서를 토대로 작성한 글 입니다. 더 자세하고 정확한 설명은 공식문서를 참고하시길 바랍니다.

1. Spring Cloud OpenFeign이란?

Spring Cloud OpenFeign은 Netflix에서 개발된 Open Feign을 Spring Cloud와 통합한 버전으로 사용 시 다음과 같은 장점이 있다.

더 자세한 내용은 공식 문서를 참고하길 바랍니다.

Circuit Breaker 지원

Spring Cloud OpenFeignd은 Hystrix와 통합되어 동작한다.

  • 각 Method는 HystrixCommand로 감싸져서 호출이 된다.
  • 각 메소드들은 개별의 Circuit Breaker로 분리된다.
  • 각 메소드들은 지정된 ThreadPool에서 실행된다.

Eureka 및 Ribbon 지원

  • Spring Cloud OpenFeign의 경우 호출할 서버의 주소를 유레카로부터 가져온다.
  • 서버의 IP를 직접 명시할 필요가 없으며, 유레카에 등록된 이름만 명시하면 된다.
  • 서버 호출은 Ribbon을 통해 호출된다.

Declarative HTTP 호출 정의

HTTP 호출을 메소드 선언과 Annotation의 선언을 통해 정의한다.

  • 인터페이스 + Spring MVC 애노티이션 선언으로 HTTP 호출이 가능한 Spring 빈을 자동으로 생성

Spring Cloud Feign with Hystrix, Ribbon, Eureka

  • 호출하는 모든 메소드는 HystrixCommand로 실행된다.
    • Circuit Breaker, Timeout, Isolation, Fallack이 적용됌
  • 호출할 서버는 유레카를 통해 얻어서, Ribbon으로 Load Balancing되어 호출된다.

2. How to Include Feign

Spring cloud openfeign 디펜던시를 추가하고, @EnableFeignClients 애노테이션을 추가한다. 간단한 예제는 아래와 같다.

@SpringBootApplication
@EnableFeignClients
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

그리고 아래와 같이 @FeignClient 애노테이션을 사용해서 클라이언트를 정의한다.

@FeignClient("stores")
public interface StoreClient {
    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    List<Store> getStores();

    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    Store update(@PathVariable("storeId") Long storeId, Store store);
}

@FeignClient 애노티이션의 value 속성은 클라이언트의 이름이된다. 이 이름은 Ribbon의 Loadbalancer를 생성할 때 사용된다. @FeignClient의 url 속성을 사용해서 URL을 명시할 수도 있다. 자동으로 생성되는 빈의 이름은 인터페이스의 풀네임(패지키명 + 인터페이스명)이 된다. 만약 고유 alias 값을 지정하려면 @FeignClient의 qualifier 속성을 사용하면 된다.

Ribbon 클라이언트는 “stores” 서비스의 실제 주소를 검색한다. 만약 애플리케이션이 유레카 클라이언트라면 유레카 서버로부터 서버 목록을 가져온다. 만약 유레카를 사용하고 싶지 않다면 설정에서 서버 목록을 간단하게 설정할 수 있다.

3. Overrding Feign Defaults

Spring Cloud Feign의 핵심 개념은 named client 이다. @FeignClient 애노테이션을 통해 Feign 클라이언트에게 이름을 줄 수 있다. @FeignClient 애노테이션이 붙은 인터페이스를 선언하면 Spring Cloud가 자동으로 빈을 생성해서 등록한다. 이 때 사용하는 설정이 FeignClientsConfiguration 클래스이다. FeignClientsConfiguration 클래스에는 Decorder, Encorder, Contract 빈 설정이 포함되어 있다.

만약에 특정 FeignClient만 다른 설정을 사용하고 싶다면, 아래와 같이 하면 된다.

@FeignClient(name = "stores", configuration = FooConfiguration.class)
public interface StoreClient {
    //..
}

이 경우 FooConfiguration 클래스와 FeignClientsConfiguration을 함께 사용해서 Feign 클라이언트를 생성한다. 단 이때 FooConfiguration 클래스가 우선순위가 더 높다.

FooConfiguration는 @Configuration 애노테이션을 붙일 필요가 없다. 하지만 @Configuration 애노테이션이 붙어 있다면, @ComponentScan에서 제외시켜야 한다. 만약 @ComponentScan에 포함된다면, FooConfiguration에 있는 Decorder, Encorder, Contract 설정이 기본 설정이 된다.

FeignClientsConfiguration의 코드를 보면 아래와 같이 빈이 등록되어있지 않은 경우에만, Decorder, Encorder, Contract 빈을 생성한다. 만약 FooConfiguration에서 Decorder, Encorder, Contract빈을 등록하고 있다면 FeignClientsConfiguration에서는 빈을 따로 등록하지 않는다.

@FeignClient 애노테이션의 serviceId 속성은 deprecated 되었디.

이전에는 url 속성이 있으면 name 속성은 따로 지정하지 않아도 되었다. 하지만 지금은 url 속성이 있더라도 name 속성을 지정해야한다.

name과 url 속성에 placeholder를 사용할 수 있다.

@FeignClient(name = "${feign.name}", url = "${feign.url}")
public interface StoreClient {
    //..
}

아래는 FeignClientsConfiguration 코드이다.

@Configuration
public class FeignClientsConfiguration {

	@Autowired
	private ObjectFactory<HttpMessageConverters> messageConverters;

	@Autowired(required = false)
	private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();

	@Autowired(required = false)
	private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();

	@Autowired(required = false)
	private Logger logger;

	@Bean
	@ConditionalOnMissingBean
	public Decoder feignDecoder() {
		return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
	}

	@Bean
	@ConditionalOnMissingBean
	public Encoder feignEncoder() {
		return new SpringEncoder(this.messageConverters);
	}

	@Bean
	@ConditionalOnMissingBean
	public Contract feignContract(ConversionService feignConversionService) {
		return new SpringMvcContract(this.parameterProcessors, feignConversionService);
	}

	@Bean
	public FormattingConversionService feignConversionService() {
		FormattingConversionService conversionService = new DefaultFormattingConversionService();
		for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) {
			feignFormatterRegistrar.registerFormatters(conversionService);
		}
		return conversionService;
	}

	@Configuration
	@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
	protected static class HystrixFeignConfiguration {
		@Bean
		@Scope("prototype")
		@ConditionalOnMissingBean
		@ConditionalOnProperty(name = "feign.hystrix.enabled")
		public Feign.Builder feignHystrixBuilder() {
			return HystrixFeign.builder();
		}
	}

	@Bean
	@ConditionalOnMissingBean
	public Retryer feignRetryer() {
		return Retryer.NEVER_RETRY;
	}

	@Bean
	@Scope("prototype")
	@ConditionalOnMissingBean
	public Feign.Builder feignBuilder(Retryer retryer) {
		return Feign.builder().retryer(retryer);
	}

	@Bean
	@ConditionalOnMissingBean(FeignLoggerFactory.class)
	public FeignLoggerFactory feignLoggerFactory() {
		return new DefaultFeignLoggerFactory(logger);
	}

}

Spring Cloud Netflix는 다음과 같은 타입의 빈을 자동으로 등록해준다.

  • Decorder : OptionalDecoder를 사용함 (ResponseEntityDecoder 감싼)
  • Encorder : SpringEncoder
  • FeignLoggerFactory : DefaultFeignLoggerFactory
  • Contract : SpringMvcContract
  • Feign.Builder
    • feign.hystrix.enabled를 true로 설정 : HystrixFeign.Builder
    • feign.hystrix.enabled를 true로 설정하지 않음 : Feign.builder()
  • Retryer : NEVER_RETRY
  • Client
    • Ribbon이 enabled된 경우 : LoadBalancerFeignClient
    • Ribbon이 enabled 되지 않은 경우
      • Apache HttpClient가 클래스패스에 존재하는 경우 : ApacheHttpClient
      • OkHttpClient가 클래스패스에 존재하는 경우 : OkHttpClient
      • 모든 경우에 해당되지 않는 경우 : Default

Apache를 사용하는 경우 CloseableHttpClient 또는 OK Http를 사용하는 경우 OkHttpClient 타입의 빈을 등록해서 HttpClient를 커스터마이징 할 수 있다.

Spring Cloud Netflix는 추가적으로 다음과 같은 타입의 빈들을 찾아서 Feign 클라이언트를 생성한다.

  • Logger.Level
  • ErrorDecorder
  • Request.Options
  • Collection<RequestInterceptor>
  • SetterFactory

만약에 특정 클라이언트의 Contract 빈 설정을 변경하고, RequestInterceptor를 추가하고 싶다면 아래와 같이 FooConfiguraton 클래스를 생성하고 @FeignClient 애노테이션에 configuration 속성으로 지정하면 된다.

@Configuration
public class FooConfiguration {
    @Bean
    public Contract feignContract() {
        return new feign.Contract.Default();
    }

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

SpringMvcContract를 Default로 대체하고, BasicAuthRequestInterceptor를 Collection<RequestInterceptor>에 추가한다.

Feign 클라이언트는 아래와 같이 프로퍼티를 통해 설정할 수도 있다.

feign:
  client:
    config:
      {feignName}:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        errorDecoder: com.example.SimpleErrorDecoder
        retryer: com.example.SimpleRetryer
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

@EnableFeignClients 애노테이션의 defaultConfiguration에 디폴트 설정 클래스를 지정할 수도 있다. 이 경우에는 모든 Feign 클라이언트에 적용된다. 지정하지 않는다면 FeignClientsConfiguration를 사용한다.

만약에 설정 파일을 통해 모든 Feign 클라이언트에 적용되는 기본 설정을 변경하고 싶으면, 아래와 같이 feignName을 default로 하면 된다.

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic

설정 클래스와, 설정 파일을 모두 사용해서 기본 설정을 변경할 수도 있다. 이 경우 설정 파일이 우선순위가 더 높다. 만약에 설정 클래스의 우선 순위가 더 높게 하고 싶다면, feign.client.default-to-properties 속성을 false로 하면 된다.

만약에 RequestInterceptor에서 ThreadLocal에 바운드되는 변수를 사용하고 싶다면, Hystrix의 IsolacatoinStarategy를 SEMAPHORE로 사용하거나, Feign에서 Hystrix 기능을 disable 시켜야 한다.

# To disable Hystrix in Feign
feign:
  hystrix:
    enabled: false

# To set thread isolation to SEMAPHORE
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: SEMAPHORE

만약에 같은 name과 url을 가졌지만(같은 서버를 호출), 다른 커스텀 설정을 사용하는 Feign 클라이언트를 생성하고 싶은 경우에는 contextId 속성을 사용하면 된다.

@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
public interface FooClient {
    //..
}

@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
public interface BarClient {
    //..
}

4. Creating Feign Clients Manually

위와 같은 방법을 사용해서 커스터마이징이 불가능 한 경우에는 Feign Builder API를 사용해서 Feign 클라이언트를 직접 만들어야한다. 아래는 동일한 인터페이스를 사용하지만 서로 다른 RequestInteceptor를 사용하는 2개의 Feign 클라이언트를 생성하는 코드이다.

@Import(FeignClientsConfiguration.class)
class FooController {

	private FooClient fooClient;

	private FooClient adminClient;

    	@Autowired
	public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
		this.fooClient = Feign.builder().client(client)
				.encoder(encoder)
				.decoder(decoder)
				.contract(contract)
				.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
				.target(FooClient.class, "http://PROD-SVC");

		this.adminClient = Feign.builder().client(client)
				.encoder(encoder)
				.decoder(decoder)
				.contract(contract)
				.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
				.target(FooClient.class, "http://PROD-SVC");
    }
}
  • 위의 예제의 경우 FeignClientsConfiguration 클래스를 기본 설정으로 사용한다.
  • PROD-SVC는 클라이언트가 요청을 보낼 서비스의 이름이다.
  • Contract 객체는 인터페이스에서 어떤 애노테이션과 값이 유효한지를 정의한다. 만약에 SpringMvcContract를 사용하는 경우 Spring MVC 관련 애노테이션을 사용할 수 있다.

5. Feign Hystrix Support

만약에 Hystrix가 클래스패스에 존재하고 feign.hystrix.enabled=true이면 Feign의 모든 메소드는 Circuit Breaker로 감싸진다. 또한 HystrixCommand 타입을 리턴하는것도 가능하다. 만약에 특정 클라이언트는 Hystrix 기능을 사용하고 싶지 않다면 다음과 같이 Feign.Builder 타입의 빈을 prototype 스코프로 등록하면 된다.

@Configuration
public class FooConfiguration {
    	@Bean
	@Scope("prototype")
	public Feign.Builder feignBuilder() {
		return Feign.builder();
	}
}

Spring Cloud Dalston 버전전에는 Hystrix가 클래스패스에 존재하면 Feign의 모든 메소드는 Circuit Breaker로 감싸져서 호출이 됐다. Spring Cloud Dalston 버전 이후부터는 “feign.hystrix.enabled” 속성을 명시적으로 true로 설정해야만 메소드를 Circuit Breaker로 감싼다.

6. Feign Hystrix Fallback

Hytrix는 fallback 개념을 지원한다. fallback을 circuit이 open된 경우나 실행 도중 에러가 발생한 경우에 실행이 된다. fallback 기능을 사용하려면 @FeignClient의 fallback 속성에 fallback을 구현한 클래스를 지정하면 된다. fallback을 구현한 클래스는 빈으로 등록 되어야 한다.

@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
protected interface HystrixClient {
    @RequestMapping(method = RequestMethod.GET, value = "/hello")
    Hello iFailSometimes();
}

static class HystrixClientFallback implements HystrixClient {
    @Override
    public Hello iFailSometimes() {
        return new Hello("fallback");
    }
}

만약에 fallback을 트리거한 cause에 접근하고 싶다면, fallbackFactory 속성을 사용하면 된다.

@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
protected interface HystrixClient {
	@RequestMapping(method = RequestMethod.GET, value = "/hello")
	Hello iFailSometimes();
}

@Component
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
	@Override
	public HystrixClient create(Throwable cause) {
		return new HystrixClient() {
			@Override
			public Hello iFailSometimes() {
				return new Hello("fallback; reason was: " + cause.getMessage());
			}
		};
	}
}

Fallback은 HystrixCommand 혹은 Observable을 리턴하는 메소드에서는 지원되지 않는다.

7. Feign and @Primary

Feign과 Hystrix fallback을 사용할 때 같은 타입의 빈이 여러개 등록된다. 이 경우 @Autowired에서 예외가 발생할 수 있다. 이런 문제를 해결 하기 위해 Spring Cloud Netflix는 Feign 클라이언트 빈을 @Primary로 표시한다. 만약에 Feign 클라이언트를 @Primary로 표시하고 싶지 않다면 아래와 같이 primary 속성을 false로 하면 된다.

@FeignClient(name = "hello", primary = false)
public interface HelloClient {
	// methods here
}

8. Feign Inheritance Support

Feign의 경우 인터페이스 상속을 지원한다. 이를 통해 공통 API를 base 인터페이스에 정의할 수 있다.

// UserService
public interface UserService {

    @RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
    User getUser(@PathVariable("id") long id);
}

// UserResource
@RestController
public class UserResource implements UserService {

}

// UserClient
@FeignClient("users")
public interface UserClient extends UserService {

}

9. Feign Request/Response Compression

Feign 요청과 응답을 GZIP 압축을 하고 싶은 경우, 아래와 같은 속성을 추가하면 된다.

feign:
  compression:
    request:
      enabled: true
    response:
      enabled: true

아래와 같이 압축할 특정 media type이나 요청의 최소 크기를 설정할 수 있다.

feign:
  compression:
    request:
      enabled: true
      mime-types: text/xml,application/xml,application/json
      min-request-size: 2048

10. Feign Logging

각각의 Feign 클라이언트별로 Logger가 생성된다. Logger의 디폴트 이름은 Feign 클라이언트를 생성하기 위해 사용된 인터페이스의 full name이다.

아래와 같이 클라이언트별로 logging 레벨을 설정할 수 있다.

logging.level.project.user.UserClient: DEBUG

Logger.Level 객체는 클라이언트별로 설정할 수 있다. 이는 Feign이 로그를 얼마나 남길지를 결정한다.

  • NONE : 로깅을 하지 않는다. (default)
  • BASIC : 요청 메소드와 URL만 로그를 남긴다. 그리고 응답 status와 실행 시간을 로깅한다.
  • HEADERS : BASIC 로그 + 요청과 응답의 헤더를 로그를 남긴다.
  • FULL : 요청과 응답의 헤더와 바디 그리고 메타데이터를 로그로 남긴다.

아래는 Logger.Level을 FULL로 설정하는 예제이다.

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

11. Feign @QueryMap support

OpenFeign에서 @QueryMap 애노테이션을 사용하면 POJO 객체를 GET 요청의 쿼리 파라미터로 사용할 수 있다. 불행히도 @QueryMap 애노테이션은 스프링과 호환되지 않는다.

대신에 Spring Cloud OpenFeign은 @SpringQueryMap 애노테이션을 제공한다. 이는 @QueryMap과 동일한 역할을 한다. @SpringQueryMap 애노테이션은 POJO 또는 Map 파라미터에 추가할 수 있으며, 애노테이션이 추가된 경우 요청의 쿼리 파라미터로 사용된다.

아래는 간단한 예제이다.

// Params.java
public class Params {
    private String param1;
    private String param2;

    // [Getters and setters omitted for brevity]
}

@FeignClient("demo")
public class DemoTemplate {

    @GetMapping(path = "/demo")
    String demoEndpoint(@SpringQueryMap Params params);
}

필드명인 param1, param2는 쿼리 파라미터의 키가 된다. 필드의 값은 쿼리 파라미터의 값이 된다.

12. How Feign Works?

만약에 아래와 같이 설정한 경우 Feign의 메소드가 어떤 순서로 실행되는지 확인해봤다.

  • hystrix 사용 안함
  • ribbon 사용함 -> Client 구현체로 LoadBalancerFeignClient를 사용
  • Apache HttpClient 사용함 -> LoadBalancerFeignClient가 내부적으로 Apache HttpClient를 사용해서 실제 요청을 수행

그럼 다음과 같은 순서로 메소드가 실행된다.

  • FeignInvocationHandler#invoke가 호출됨
    • SynchronousMethodHandler#invoke 메소드 호출
  • SynchronousMethodHandler#invoke
    • LoadBalancerFeignClient#execute 메소드 호출 -> ApacheHttpClient#execute 메소드를 호출
      • LoadBalancerFeignClient는 Service Discovery를 통해 서버 목록을 가져오고, 실제 요청은 다른 Client로 위임한다. 만약에 Apache HttpClient를 사용하는 경우 ApacheHttpClient#execute 메소드를 호출해서 실제 요청 작업을 위임한다.
    • Client가 요청을 보내고 응답을 받으면 응답을 원하는 형태로 디코딩함
      • status 코드가 2XX인 경우 : Decorder를 사용해서 응답을 디코딩함
      • decode404 속성이 true이고, status 코드가 404인 경우 : Decorder를 사용해서 응답을 디코딩함
      • 그 외 : ErrorDecorder를 사용함
    • 디코딩하는 과정에서 IOException이 발생하는 경우 FeignException이 발생함.
    • Client#execute 메소드를 호출하는 과정에서 IOException이 발생하는 경우 RetryableException이 발생함
    • Response를 디코딩한 객체를 리턴함 / 혹은 ErrorDecorder에서 리턴한 예외를 throw함

만약에 hystrix을 enabled한 경우, SynchronousMethodHandler가 HystrixCommand로 감싸져서 호출됨.

  • HystrixInvocationHandler#invoke가 호출됨
    • SynchronousMethodHandler#invoke 메소드를 HystrixCommand로 감싸서 호출함
  • SynchronousMethodHandler#invoke 실행됨