FeignClient는 선언적 웹 서비스 클라이언트 (RestTemplate 비슷한 것..)입니다. 손쉽게 웹 서비스 클라이언트를 만들 수 있으며 Spring Cloud 와 함께 사용할 경우 Eureka, Ribbon 이 함께 사용 되어 각 microservice 들에 손쉽게 요청 및 인스턴스가 여러개일 경우 자동으로 로드 밸런싱을 지원합니다.
"선언적" 이라는게 무슨 표현인가요?
Declarative (선언적)은 작업을 어떻게 수행하는지를 명시하는 것 대신 무엇을 수행하는지 명시하는 방식을 의미합니다. FeignClient는 interface 와 annotation 을 사용해서 무엇을 할지 명시하는 방법으로 웹 서비스 클라이언트를 생성합니다.
즉, 개발자는 구현체를 직접 작성하는 게 아니고 interface, annotation만 사용하고 구현체는 알아서 만들어진다.
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
@SpringBootApplication
@EnableFeignClients // <-- 여기에 추가
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
@FeignClient(name = "product")
public interface ProductFeignClient extends ProductService {
@GetMapping("/products/{productId}")
ProductResponseDto getProduct(@PathVariable("productId") Long productId);
}
@FeignClient(name = “product”) - FeignClient로 선언합니다. Eureka 서버에 연결되어 있다면 클라이언트의 url을 name을 기반으로 탐색합니다. 이를 통해 별도로 url을 지정하지 않아도 Eureka 서버에 등록된 클라이언트로 요청을 보낼 수 있습니다.Eureka 서버에 연결되지 않은 경우 (RestTemplate 사용) url을 지정하여 요청을 보낼 수 있습니다.
@GetMapping(”/products/{productId}”) - controller에서 사용하는 것과 같이 Http Method와 path를 지정합니다.
ProductResponseDto getProduct(@PathVariable("productId") Long productId); - controller와 같이 함수 시그니처를 정의합니다. 후에 이 함수를 사용할 때 productId는 path variable로 들어가며, 반환 값은 ProductResponseDto 로 받게 될 것입니다.
Service, FeignClient를 인터페이스로 구현하여 결합도를 낮춘다.
bean으로 등록이 되었기 때문에 다른 Service에서 가져다 사용할 수 있다.
ProductResponseDto product = productService.getProduct(productId);
함수를 사용하는 것과 같이 편리하게 사용할 수 있습니다.
Request header 를 다양한 방법으로 넣을 수 있습니다. 방법들을 살펴보며 장단점 들을 생각해보겠습니다.
// @Configuration 달지 않음
public class ProductFeignClientConfig {
@Value("${server.port}")
private String serverPort;
@Bean // 헤더가 잘 삽입되었는지 확인 용도의 로깅
public Logger.Level feignLoggerLevel() {
return Logger.Level.HEADERS;
}
@Bean // bean으로 등록시키고
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
requestTemplate.header("API-KEY", "123");
requestTemplate.header("USER-ROLE", "CUSTOMER");
requestTemplate.header("SERVER-PORT", serverPort);
};
}
}
@FeignClient(
name = "product",
configuration = ProductFeignClientConfig.class
// configuration 부착, Client랑 Config 파일 연결
)
public interface ProductFeignClient extends ProductService {
@GetMapping("/products/{productId}")
ProductResponseDto getProduct(@PathVariable("productId") Long productId);
}
이 방법을 사용하게 되면 config 가 적용된 feign client의 모든 함수에서 헤더가 설정되게 됩니다. 또한 @Value 어노테이션으로 yaml에서 값을 읽어와 헤더로 설정해줄 수도 있습니다.
@Configuration : 지정된 클래스가 스프링의 설정정보를 포함한 클래스임을 표시
: Spring 컨테이너에 해당 클래스를 빈 설정 파일로 등록, 내부 정의 메소드들이 bean으로 등록된다.
전역적으로 적용되어야 하는 설정이나 빈을 정의할 때 사용한다.
@FeignClient의 configuration 옵션
: FeignClient에만 특정한 설정을 적용할 수 있다.
: Config 파일을 전역 빈으로 등록하지 않고, 해당 클라이언트에만 사용하는 설정으로 인식한다.
: 특정 API 호출에만 적용해야 하는 헤더나 로깅 레벨 같은 설정을 격리하기 위해 유용하다.
참고 블로그
request를 낚아채서 controller로 가기 전 request 객체를 가져올 수 있다.
@GetMapping(value = "/products/{productId}")
ProductResponseDto getProduct(
@PathVariable("productId") Long productId,
**@RequestHeader("key3") String headers**
);
파라미터로 헤더에 사용할 값을 받아서 넣을 수도 있습니다.
@GetMapping(value = "/products/{productId}", headers = "key2=value2")
ProductResponseDto getProduct(@PathVariable("productId") Long productId);
고정된 문자열을 헤더로 넣을 수 있습니다. 다만 이 방법에서는 @Value 어노테이션 등의 사용이 어렵습니다.
서킷 브레이커 : 과부하 걸리면 해당 서비스로의 요청을 차단, Fail Fast
FeignClient는 MSA 환경에 필요한 것들을 많이 지원하고 있습니다. 그 중 하나가 CircuitBreaker와의 연동입니다. Resilience4j 를 사용하여 circuitBreaker를 연동해봅시다.
```bash
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
```
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true # 사용 선언
alphanumeric-ids: # circuit breaker의 id를 알파벳,_,숫자 만으로 변환, 호환성 증가
enabled: true
resilience4j:
circuitbreaker:
configs:
default: # 기본 구성 이름
registerHealthIndicator: true # 애플리케이션의 헬스 체크에 서킷 브레이커 상태를 추가하여 모니터링 가능
# 서킷 브레이커가 동작할 때 사용할 슬라이딩 윈도우의 타입을 설정
# COUNT_BASED: 마지막 N번의 호출 결과를 기반으로 상태를 결정
# TIME_BASED: 마지막 N초 동안의 호출 결과를 기반으로 상태를 결정
slidingWindowType: COUNT_BASED # 슬라이딩 윈도우의 타입을 호출 수 기반(COUNT_BASED)으로 설정
# 슬라이딩 윈도우의 크기를 설정
# COUNT_BASED일 경우: 최근 N번의 호출을 저장
# TIME_BASED일 경우: 최근 N초 동안의 호출을 저장
slidingWindowSize: 5 # 슬라이딩 윈도우의 크기를 5번의 호출로 설정
minimumNumberOfCalls: 5 # 서킷 브레이커가 동작하기 위해 필요한 최소한의 호출 수를 5로 설정
slowCallRateThreshold: 100 # 느린 호출의 비율이 이 임계값(100%)을 초과하면 서킷 브레이커가 동작
slowCallDurationThreshold: 60000 # 느린 호출의 기준 시간(밀리초)으로, 60초 이상 걸리면 느린 호출로 간주
failureRateThreshold: 50 # 실패율이 이 임계값(50%)을 초과하면 서킷 브레이커가 동작
permittedNumberOfCallsInHalfOpenState: 3 # 서킷 브레이커가 Half-open 상태에서 허용하는 최대 호출 수를 3으로 설정
# 서킷 브레이커가 Open 상태에서 Half-open 상태로 전환되기 전에 기다리는 시간
waitDurationInOpenState: 20s # Open 상태에서 Half-open 상태로 전환되기 전에 대기하는 시간을 20초로 설정
활성화 이후 슬라이딩 윈도우를 초과할 경우 excepcion 에러 발생하는 것을 확인할 수 있다.
Circuit breaker를 설정한 메소드에서 exception 발생 시 실행된다.
즉, 서비스를 차단한 경우 예외 발생 대신 대비책을 제공 / 미리 준비된 동작을 실행
@Component
public class ProductFallback implements ProductClient {
@Override
public ProductResponseDto getProduct(Long productId) {
return new ProductResponseDto(); // 호출 실패 시 반환 할 값, 기본 id 값을 이용해 dto를 반환하도록 설정
}
}
FeignClient 인터페이스를 상속하여 각 함수마다 fallback method를 지정할 수 있습니다.
ProductClient : FeignClient 인터페이스, circuit breaker를 통한 exception 발생
```java
@FeignClient(name = "product",
fallback = ProductFallback.class) // 지정
public interface ProductClient extends ProductService {
@GetMapping("/products/{productId}")
ProductResponseDto getProduct(@PathVariable("productId") Long productId);
}
```
이렇게 fallback method 들을 관리하는 클래스를 만들고 연동할 수 있습니다. 하지만 이렇게 fallback을 구성할 경우 대상 service에서 어떤 에러를 일으키는지 확인할 수 없습니다. 이를 위해서 fallback factory를 구현해봅시다.
@Component
@Slf4j
public class ProductFallbackFactory implements FallbackFactory<ProductClient> {
@Override
public ProductClient create(Throwable cause) { // exception을 받아올 수 있다.
log.info(cause.toString()); // cause를 옮겨준다
return new ProductFallback();
}
}
@FeignClient(name = "product", **fallbackFactory = ProductFallbackFactory.class**)
@Primary
public interface ProductClient extends ProductService {
@GetMapping("/products/{productId}")
ProductResponseDto getProduct(@PathVariable("productId") Long productId);
}
// @Component 삭제
@Requiredargsconstructor
public class ProductFallback implements ProductClient {
private final Throwable cause; // 원인 받아오기
@Override
public ProductResponseDto getProduct(Long productId) {
// 객체를 받아와서 경우에 따라 분기 처리, fallback 구성
if (cause instanceof FeignExceprion.NotFound){
log.error("Not found");
}
return new ProductResponseDto(); // 호출 실패 시 반환 할 값, 기본 id 값을 이용해 dto를 반환하도록 설정
}
}
이렇게 설정하여 FeignClient 요청 시 어떤 실패가 일어났는지도 확인할 수 있습니다.
Cacheable 어노테이션으로 손쉽게 캐싱을 적용할 수 있습니다.
```java
@FeignClient(name = "product", **fallbackFactory = ProductFallbackFactory.class**)
public interface ProductClient extends ProductService {
@GetMapping("/products/{productId}")
@Cacheable(cacheNames = "product-cache", key = "#productId")
ProductResponseDto getProduct(@PathVariable("productId") Long productId);
}
```
```java
// config/ProductFeignClientConfig.java
public class ProductFeignClientConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
```
```java
// product/ProductFeignClient.java
@FeignClient(
name = "product",
fallbackFactory = ProductFallbackFactory.class,
**configuration = ProductFeignClientConfig.class**
)
@Primary
public interface ProductFeignClient extends ProductService {
@GetMapping("/products/{productId}")
ProductResponseDto getProduct(@PathVariable("productId") Long productId);
}
```
```yaml
# application.yml
logging:
level:
com: # Package의 특정 클래스 로깅 레벨 설정
feignClient:
practice:
order:
product:
ProductFeignClient: **DEBUG** # Feign logging은 DEBUG 에서만 동작함
```
자료