FeignClient

ayboori·2024년 9월 10일

MSA

목록 보기
7/8

이전 작성한 로드 밸런싱

FeignClient 이란?

FeignClient는 선언적 웹 서비스 클라이언트 (RestTemplate 비슷한 것..)입니다. 손쉽게 웹 서비스 클라이언트를 만들 수 있으며 Spring Cloud 와 함께 사용할 경우 Eureka, Ribbon 이 함께 사용 되어 각 microservice 들에 손쉽게 요청 및 인스턴스가 여러개일 경우 자동으로 로드 밸런싱을 지원합니다.

"선언적" 이라는게 무슨 표현인가요?
Declarative (선언적)은 작업을 어떻게 수행하는지를 명시하는 것 대신 무엇을 수행하는지 명시하는 방식을 의미합니다. FeignClient는 interface 와 annotation 을 사용해서 무엇을 할지 명시하는 방법으로 웹 서비스 클라이언트를 생성합니다.
즉, 개발자는 구현체를 직접 작성하는 게 아니고 interface, annotation만 사용하고 구현체는 알아서 만들어진다.

FeignClient 사용 방법

1. OpenFeign 의존성을 build.gradle 에 추가합니다.

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

2. @EnableFeignClients 어노테이션을 추가합니다.

@SpringBootApplication
@EnableFeignClients // <-- 여기에 추가
public class OrderApplication {

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

3. FeignClient를 만듭니다.

    @FeignClient(name = "product")
    public interface ProductFeignClient extends ProductService {
        @GetMapping("/products/{productId}")
        ProductResponseDto getProduct(@PathVariable("productId") Long productId);
    }
  1. @FeignClient(name = “product”) - FeignClient로 선언합니다. Eureka 서버에 연결되어 있다면 클라이언트의 url을 name을 기반으로 탐색합니다. 이를 통해 별도로 url을 지정하지 않아도 Eureka 서버에 등록된 클라이언트로 요청을 보낼 수 있습니다.

Eureka 서버에 연결되지 않은 경우 (RestTemplate 사용) url을 지정하여 요청을 보낼 수 있습니다.

  1. @GetMapping(”/products/{productId}”) - controller에서 사용하는 것과 같이 Http Method와 path를 지정합니다.

  2. ProductResponseDto getProduct(@PathVariable("productId") Long productId); - controller와 같이 함수 시그니처를 정의합니다. 후에 이 함수를 사용할 때 productId는 path variable로 들어가며, 반환 값은 ProductResponseDto 로 받게 될 것입니다.

Service, FeignClient를 인터페이스로 구현하여 결합도를 낮춘다.
bean으로 등록이 되었기 때문에 다른 Service에서 가져다 사용할 수 있다.

4. FeignClient를 사용합니다.

    ProductResponseDto product = productService.getProduct(productId);

함수를 사용하는 것과 같이 편리하게 사용할 수 있습니다.

Header 를 설정해서 사용

Request header 를 다양한 방법으로 넣을 수 있습니다. 방법들을 살펴보며 장단점 들을 생각해보겠습니다.

1. RequestInterceptor 사용하여 헤더 넣기

// @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, @Client 내의 configuration

@Configuration : 지정된 클래스가 스프링의 설정정보를 포함한 클래스임을 표시
: Spring 컨테이너에 해당 클래스를 빈 설정 파일로 등록, 내부 정의 메소드들이 bean으로 등록된다.

전역적으로 적용되어야 하는 설정이나 빈을 정의할 때 사용한다.

@FeignClient의 configuration 옵션
: FeignClient에만 특정한 설정을 적용할 수 있다.
: Config 파일을 전역 빈으로 등록하지 않고, 해당 클라이언트에만 사용하는 설정으로 인식한다.
: 특정 API 호출에만 적용해야 하는 헤더나 로깅 레벨 같은 설정을 격리하기 위해 유용하다.

RequestInterceptor

참고 블로그
request를 낚아채서 controller로 가기 전 request 객체를 가져올 수 있다.

2. 인자로 헤더 받기

@GetMapping(value = "/products/{productId}")
ProductResponseDto getProduct(
	@PathVariable("productId") Long productId,
	**@RequestHeader("key3") String headers**
);

파라미터로 헤더에 사용할 값을 받아서 넣을 수도 있습니다.

3. 고정된 문자열 헤더 넣기

@GetMapping(value = "/products/{productId}", headers = "key2=value2")
ProductResponseDto getProduct(@PathVariable("productId") Long productId);

고정된 문자열을 헤더로 넣을 수 있습니다. 다만 이 방법에서는 @Value 어노테이션 등의 사용이 어렵습니다.

연동 사용

CircuitBreaker

서킷 브레이커 : 과부하 걸리면 해당 서비스로의 요청을 차단, Fail Fast

FeignClient는 MSA 환경에 필요한 것들을 많이 지원하고 있습니다. 그 중 하나가 CircuitBreaker와의 연동입니다. Resilience4j 를 사용하여 circuitBreaker를 연동해봅시다.

활성화

1) build.gradle 에 circuit breaker 의존성을 추가합니다.

```bash
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
```

2) application.yml 에서 open feign에 대한 circuit breaker 사용을 활성화 합니다.

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 에러 발생하는 것을 확인할 수 있다.

Fallback

Circuit breaker를 설정한 메소드에서 exception 발생 시 실행된다.
즉, 서비스를 차단한 경우 예외 발생 대신 대비책을 제공 / 미리 준비된 동작을 실행

Fallback method 담당 Class 생성

1. Fallback class를 생성합니다.

    @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 발생

2. Feign Client에서 fallback class를 지정합니다.

```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를 구현해봅시다.

Fallback Factory 구현

1. FallbackFactory class를 생성합니다.

    @Component
    @Slf4j
    public class ProductFallbackFactory implements FallbackFactory<ProductClient> {
        @Override
        public ProductClient create(Throwable cause) { // exception을 받아올 수 있다.
            log.info(cause.toString()); // cause를 옮겨준다
            return new ProductFallback();
        }
    }
    

2. FallbackFactory class를 FeignClient에서 Fallback class 대신 설정합니다.

    @FeignClient(name = "product", **fallbackFactory = ProductFallbackFactory.class**)
    @Primary
    public interface ProductClient extends ProductService {
        @GetMapping("/products/{productId}")
        ProductResponseDto getProduct(@PathVariable("productId") Long productId);
    }
    

3. Fallback 메소드를 수정합니다.

    // @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 에서만 동작함
```

자료

profile
프로 개발자가 되기 위해 뚜벅뚜벅.. 뚜벅초

0개의 댓글