백엔드/Spring

[Spring Cloud] Spring Cloud OpenFeign

밝은별 개발자 2023. 5. 22. 23:36
728x90

Spring Cloud OpenFeign이란?

Spring Cloud OpenFeign은 선언적 웹서비스 클라이언트이다.

RestTemplate과 같이 마이크로서비스 간의 통신에 사용하는 라이브러리이다.

 

Spring Cloud는 Feign에 Spring MVC 애노테이션과 HttpMessageConverter를 지원하고,

Eureka, Spring Cloud CircuitBreaker 및 Spring Cloud LoadBalancer를 통합하여 Load-Balanced HTTP 클라이언트를 지원한다.

 

기존의 RestTemplate은 반복적인 코드가 많고 유지 보수적으로 오래 유지하기 힘든 단점이 있었다.

RestTemplate은 다음과 같은 작업들을 반복적으로 수행해야 한다.

  • RestTemplate 인스턴스 또는 DI 주입
  • 타겟 URL 정의
  • Request 헤더 설정
  • Contents 타입 설정
  • URL 호출
String orderUrl = String.format(env.getProperty("order_service.url"), userId);
ResponseEntity<List<ResponseOrder>> orderListResponse =
        restTemplate.exchange(orderUrl, HttpMethod.GET, null,
                new ParameterizedTypeReference<List<ResponseOrder>>() {
});
List<ResponseOrder> orderList = orderListResponse.getBody();

 

반면에 Feign은 인터페이스와 그에 따른 애노테이션을 정의하는 것만으로 Spring이 실행될 때 구현체를 제공해 준다.


의존성

maven

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

gradle

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

Feign 적용

Application.java

@SpringBootApplication
@EnableFeignClients
public class Application {

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

}

StoreClient.java

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

    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    Page<Store> getStores(Pageable pageable);

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

    @RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\\d+}")
    void delete(@PathVariable Long storeId);
}

interface에 @FeignClient를 지정하고 값 "store"는 Spring Cloud LoadBalancer 클라이언트를 생성하는 데 사용되는 임의의 클라이언트 이름이다. url 속성을 사용하면 host이름을 지정할 수도 있다.

Application Context의 bean 이름은 인터페이스 이름으로 지정된다. 이름을 따로 지정하고 싶다면 @FeignClientqualifiers 속성을 사용하면 된다.

 

애플리케이션이 Eureka client인 경우에는"stores" 이름의 서비스를 Eureka service registry에서 확인한다.


Header에 값 설정하기

Request Header에 값을 넣는 방식은 몇 가지가 있다.

Configuration

public class HeaderConfiguration {
    
    @Bean
    public RequestInterceptor requestInterceptor() {
        return requestTemplate -> requestTemplate.header("header", "header1", "header2");
    }
}
@FeignClient(name = "order-service",configuration = HeaderConfiguration.class)
public interface OrderServiceClient {

    @GetMapping("/order-service/{userId}/orders")
    List<ResponseOrder> getOrders(@PathVariable String userId);
}

method 별로 별도의 header를 설정을 하는 것이 아닌, feign client 별로 설정이 필요할 때 사용한다.

  1. Header 값을 설정할 configuartion class를 생성을 하고, RequestInterceptor을 생성해 준다.
  2. 원하는 feign client에 만든 configuartion class를 설정해 주면 된다.

configuration class에 @Configuration을 적용하면 모든 client에 적용되니 주의하자.


@RequestHeader

@FeignClient(name = "order-service")
public interface OrderServiceClient {

    @GetMapping("/order-service/{userId}/orders")
    List<ResponseOrder> getOrders(@RequestHeader("header") String header1,  @PathVariable String userId);
}

@feign.Header

@FeignClient(name = "order-service")
public interface OrderServiceClient {

    // 이 호출은 Header 에 값이 설정되지 않습니다.
    // @GetMapping 은 SpringMVcContract 를 사용해야하고, @Headers 는 feign Contract 를 사용해야 합니다.
    @org.springframework.web.bind.annotation.GetMapping(value = "/order-service/{userId}/orders")
    @feign.Headers("header:header1")
    List<ResponseOrder> getOrders(@PathVariable String userId);
}

Contract를 feign에서 제공해 준 Default Contract를 사용해야 한다.


Logger

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class UserServiceApplication {

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

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

Logger.Level의 객체는 다음과 같다.

  • NONE : 로깅하지 않는다. (default)
  • BASIC : Request Method와 URL 그리고 Response 상태 코드와 실행 시간을 로깅한다.
  • HEADERS : Request, Response Header 정보와 함께 BASIC 정보를 로깅한다.
  • FULL : Request와 Response의 Header, Body 그리고 메타데이터를 로깅한다.

하기 쉬운 실수

  • feign.Contract.Default를 사용하는 실수
    • org.springframework.cloud:spring-cloud-starter-openfeign을 사용하게 되면 SpringMvcContract를 사용하게 되어 @GetMapping, @PostMapping, @RequestMapping을 사용할 수 있다.
    • Feign에서 제공하는 Default를 사용하게 되면, 위에 있는 것이 아닌 feign.RequestLine을 사용해야 한다.
    • Feign에서 제공하는 Default Contract를 사용하는데 @GetMapping과 같은 것을 사용한다면 아래와 같은 오류를 보게 된다.
Caused by: java.lang.IllegalStateException: Method status not annotated with HTTP method type (ex. GET, POST)
at feign.Util.checkState(Util.java:128)
at feign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:99)
at feign.Contract$BaseContract.parseAndValidatateMetadata(Contract.java:66)
at feign.ReflectiveFeign$ParseHandlersByName.apply(ReflectiveFeign.java:146)
at feign.ReflectiveFeign.newInstance(ReflectiveFeign.java:53)
at feign.Feign$Builder.target(Feign.java:218)
at org.springframework.cloud.openfeign.HystrixTargeter.target(HystrixTargeter.java:39)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.getObject(FeignClientFactoryBean.java:261)
at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:171)
... 28 more
  • feign logger level을 full로 설정했는데 로그 출력이 되지 않음
    • request와 response의 header, body 등의 상세한 로그가 나와야 한다.
    • FULL로 설정을 했음에도 나오지가 않는다면, @FeignClient으로 client를 만든 package log level을 DEBUG로 설정했는지 확인해봐야 한다.
    • FULL로 설정하면 볼 수 있는 로그
[Class#method] ---> GET https://httpbin.org/status/200 HTTP/1.1
[Class#method] ---> END HTTP (0-byte body)
[Class#method] <--- HTTP/1.1 200 OK (1437ms)
[Class#method] access-control-allow-credentials: true
[Class#method] access-control-allow-origin: *
[Class#method] connection: keep-alive
[Class#method] content-length: 0
[Class#method] content-type: text/html; charset=utf-8
[Class#method] date: Mon, 13 May 2019 12:15:41 GMT
[Class#method] referrer-policy: no-referrer-when-downgrade
[Class#method] server: nginx
[Class#method] x-content-type-options: nosniff
[Class#method] x-frame-options: DENY
[Class#method] x-xss-protection: 1; mode=block
[Class#method] 
[Class#method] <--- END HTTP (0-byte body)
  • PathVariable에 값이 들어가지 않음
    • Spring MVC Controller에서는 @PathVariable, @QueryParam에 value(name)을 넣지 않아도, field의 이름이 default로 설정되어 유입된 값이 세팅이 되지만, feign에서는 value(name) 값을 넣어줘야 @GetMapping @PostMapping 등에 있는 url에 자동으로 넣어준다.
    • default로 넣어줄 거라 생각하고 안 넣어주면 에러가 발생된다.
  • Feign client method parameter에 @PathVariable, @QueryParam, @RequestBody와 같은 annotation을 설정하지 않음
    • 아무것도 설정하지 않으면 @RequestBody로 생각해서 Http Body 로 데이터를 보내려고 한다.
    • 이때, parameter 가 2개 이상이 되면 Method has too many Body parameters이라는 에러가 발생된다.
@PostMapping(value = "/anything")
void anything(ExampleRequest request, ExampleRequest request2);

예외 처리

ErrorDecoder를 사용하면 예외처리를 할 수 있다.

@RequiredArgsConstructor
@Component
public class FeignErrorDecoder implements ErrorDecoder {

    private final Environment env;

    @Override
    public Exception decode(String methodKey, Response response) {
        switch (response.status()) {
            case 400:
                break;
            case 404:
                if(methodKey.contains("getOrders")) {
                    return new ResponseStatusException(HttpStatus.valueOf(response.status()), env.getProperty("order_service.exception.orders_is_empty"));
                }
                break;
            default:
                return new Exception(response.reason());
        }

        return null;
    }
}

 

ErrorDecoder를 상속한 뒤 decode() 함수를 Override 하여 구현한다.


참고 자료 : 

https://techblog.woowahan.com/2630/

 

우아한 feign 적용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요. 저는 비즈인프라개발팀에서 개발하고 있는 고정섭입니다. 이 글에서는 배달의민족 광고시스템 백엔드에서 feign 을 적용하면서 겪었던 것들에 대해서 공유 하고자 합니다

techblog.woowahan.com

https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/

 

Spring Cloud OpenFeign

Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable

docs.spring.io