맨땅에 헤딩하는 개바른자

[Spring] WebClient 빈 별로 RestClient 빈 구성 본문

SPRING

[Spring] WebClient 빈 별로 RestClient 빈 구성

앵낄낄 2024. 2. 26. 16:00
반응형

목차

  1. 개요
  2. 구성 파일
  3. 구성 파일 설명
    • WooForwardFilterFunction (필터 기능)
    • AddExchangeFilterFunctions (필터 기능 주입)
    • WooRestClientAutoConfiguration (AutoConfiguration 설정)
    • WooRestClientAutoConfigureRegistrar (WebClient별 RestClient 빈 생성)
  4. 간략한 정리

 

1. 개요

RestTemplate와 같이 WebClient가 여러 주소로 통신 하기위해서는 주소별 WebClient빈을 생성해야합니다. 생성 된 빈들은 사용 클래스에서 빈 이름으로 구별하여 주입받아 사용하게 됩니다.

이번 포스팅에서는 여러 WebClient빈을 특정 어노테이션을 이용하여 통신 기능을 담당하는 클래스(Repository)를 생성하고, 이 클래스(Repository)를 실제 구현 클래스(Service) 로직에서 주입받아 사용 할 수 있는 구성을 하는 과정을 설명하겠습니다.

2. 구성 파일

 

3. 구성 파일 설명

WooForwardFilterFunction

Request Header 부분에 X-Forwarded-For 항목을 추가하는 설정으로 WebClient 전역에 적용하기위해 ExchangeFilterFunction로 역활을 추가합니다.

public class WooForwardFilterFunction implements ExchangeFilterFunction {
    @Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {

        return forwardHeader(request)
                .flatMap(next::exchange);
    }

    private Mono<ClientRequest> forwardHeader(ClientRequest request) {

        return Mono.just(
                ClientRequest.from(request)
                        .header(X_FORWARDED_FOR_VALUE, MDC.get(X_FORWARDED_FOR_VALUE))
                        .build()
        );
    }
}

Q. 오잉 왜 이렇게? 일반적인 Filter로는 못하나 차이가 뭔지 궁금해

A. ExchangeFilterFunctionWebClientfilter 메서드를 사용하여 추가할 수 있는 Filter는 모두 Spring WebFlux 프레임워크에서 사용되는 요청 및 응답의 가로채기, 수정 및 추가 기능을 제공하는 데 사용됩니다. 그러나 두 가지 접근 방식 간에는 몇 가지 차이가 있습니다.

  1. 인터페이스 vs 클래스:
    • ExchangeFilterFunction: 이 인터페이스는 함수형 프로그래밍 스타일을 지원하며, 단일 메서드 filter(ClientRequest request, ExchangeFunction next)를 가지고 있습니다. 이 메서드에서 ExchangeFunction을 통해 실제 요청을 보낼 수 있습니다.
    • Filter: 이 클래스는 추상 클래스 또는 인터페이스일 수 있으며, 일반적으로 여러 메서드를 구현해야 합니다. Spring WebFlux에서 WebClient에 대한 ExchangeFilterFunction을 추가하려면 filter 메서드를 구현해야 합니다.
  2. 유연성:
    • ExchangeFilterFunction: 함수형 프로그래밍 스타일로 제공되기 때문에 유연성이 있습니다. 람다 표현식을 사용하여 간결하게 작성할 수 있습니다.
    • Filter: 클래스의 형태를 가지므로 상태를 유지할 수 있습니다. 필터 내에서 상태를 저장하거나 여러 메서드를 통해 다양한 동작을 구현할 수 있습니다.
  3. 구성 및 적용 방법:
    • ExchangeFilterFunction: WebClient.Builder를 통해 직접 WebClient 빌드 시 filter 메서드를 사용하여 추가할 수 있습니다.
    • Filter: WebClientConfigurer 인터페이스를 구현하거나, WebClient.Builder를 통해 filter 메서드를 사용하여 추가할 수 있습니다. 이때 클래스를 생성하고 빈으로 등록해야 합니다.

 

AddExchangeFilterFunctions

public class AddExchangeFilterFunctions implements Consumer<List<ExchangeFilterFunction>> {
    ExchangeFilterFunction[] filterFunctions;

    public AddExchangeFilterFunctions(ExchangeFilterFunction...filterFunctions) {
        this.filterFunctions = filterFunctions;
    }

    @Override
    public void accept(List<ExchangeFilterFunction> exchangeFilterFunctions) {
        exchangeFilterFunctions.addAll(getFilterFunctions());
    }

    private List<ExchangeFilterFunction> getFilterFunctions() {
        return Arrays.stream(filterFunctions).toList();
    }
}

이 코드는 Consumer<List<ExchangeFilterFunction>> 인터페이스를 구현하는 AddExchangeFilterFunctions 클래스입니다. 이 클래스는 WebClient의 filter를 추가할 때 사용할 수 있는 편의성을 제공합니다.

여러 개의 ExchangeFilterFunction을 한 번에 추가하기 위해 사용됩니다. Consumer 인터페이스를 구현하여 함수형 프로그래밍 스타일로 WebClient.Builder에 필터를 추가할 수 있도록 합니다.

public class GlobalExchangeFilterFunctions implements Consumer<List<ExchangeFilterFunction>> {
    ExchangeFilterFunction[] filterFunctions;

    public GlobalExchangeFilterFunctions(ExchangeFilterFunction...filterFunctions) {
        this.filterFunctions = filterFunctions;
    }

    @Override
    public void accept(List<ExchangeFilterFunction> exchangeFilterFunctions) {
        exchangeFilterFunctions.addAll(getFilterFunctions());
    }

    private List<ExchangeFilterFunction> getFilterFunctions() {
        return Arrays.stream(filterFunctions).toList();
    }
}

코드를 보시면 webClient.Builder에 추가 된 filter을 호출하여 ExchangeFilterFunction에 accept 합니다. 이로볼 때 WebClient가 빈으로 등록 될 때 전역으로 관리되고 있는 ExchangeFilterFunction에 추가하는 것을 알 수 있습니다.

이로 짐작할 수 있는건 전역filter를 관리한다는건 여러개의 다른 WebClient빈들이 생성될 때 공통으로 적용되는 filter가 있다라고 볼 수 있겟습니다.

 

WooRestClientAutoConfiguration

@Slf4j
@AutoConfiguration
@ConditionalOnClass(WebClient.class)
@ConditionalOnProperty(prefix = "woo.client", name = "enabled", matchIfMissing = true)
@Import(WooRestClientAutoConfigureRegistrar.class)
@EnableConfigurationProperties(WooRestClientProperties.class)
public class WooRestClientAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public GlobalExchangeFilterFunctions wooClientFilterFunctions() {
        log.debug("### WooAutoconfigure Bean: '{}'", "wooClientFilterFunctions");

        return new GlobalExchangeFilterFunctions(
                new ServletBearerExchangeFilterFunction(),
                new WooForwardFilterFunction()
        );
    }
}

AutoConfiguration을 등록하는 구간입니다. GlocalExchaneFilterFunctions에 설정 된 filter를 Bean으로 등록 합니다. 여기서 WebClient를 Bean으로 등록하는 메소드가 빠져보이지만 자세히보면 @Import(WooRestClientAutoConfigureRegistrar.class)  어노테이션으로 별도 등록을 하고 있습니다.

 

WooRestClientAutoConfigureRegistrar

이 구간에서는 RestClient를 빈으로 등록하는 과정이 이루워 입니다. 자세한 코드는 GitHub 를 통해서 확인하실 수 있습니다. (GitHub)

우선 설명에 앞서 컨셉에 대해서 설명드리겠습니다.
@WooRestClient라는 어노테이션 인터페이스를 생성하였습니다. 

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface WooRestClient {

    String webClientBeanName();
}

해당 어노테이션은 빈으로 생성 된 여러 종유의 WebClient를 주입받아 사용하는 용도의 어노테이션입니다.

예를들어

@WooRestClient(webClientBeanName = "cust1webClient")
interface HelloRepository {
    @GetExchange("/hello")
    MyResponse hello();
}

record MyResponse(String message) {
}

@AutoConfigurationPackage
@TestConfiguration
static class Cust1WebClientConfiguration {
    @Bean
    public WebClient cust1WebClient() {
        WebClient webClient = WebClient.builder()
                .baseUrl("https://cust1.com")
                .build();
        return mock(webClient);
    }
}

 

Cust1WebClientConfiguration클래스에서는 cust1webClient란 이름으로 빈을 만듭니다. 해당 빈은 https://cust1.com 으로 통신용도로 생성되는 빈입니다. 
HelloRepository 인터페이스는 cust1.com으로 통신을 하기위해 cust1WebClient WebClient빈을 주입받게끔 @WooRestClient(webClientBeanName = "cust1WebClient")를 선언합니다.

본 설정으로 다시 이동하여 WooRestClientAutoConfigureRegistrar 클래스에서는 @WooRestClient로 선언 된 항목을 모두 조회하여 RestClient Bean을 등록합니다. 
이과정에서 implements FactoryBean<Object>, ApplicationContextAware 기술이 사용되는데 BeanDefinitionBuilder를 genericBeanDefinition() 기능을 사용할 때 필요로 합니다.

 

4. 간략한 정리

  • 사용 케이스
    • RestClient 통신용 기능을 담당하는 Repository 클래스 구성
      • 해당 클래스에 통신정보를 주입하는 WebClient를 지정
        • @WooRestClient(webClientBeanName = "custWebClient")
      • GET, POST 등의 기능 메소드 작성 
    • Service 구현로직에서는 Repository를 주입받아서 통신용 메소드를 사용
  • 구성
    • spring boot starter 구성 컨셉으로 AutoConfiguration으로 WebClient 필터 전역설정 등록
      • ExchangeFilterFunction 기능을 이용하여 필요한 필터를 등록합니다.
    • spring boot starter 구성 컨셉으로 AutoConfiguration으로 WebClient빈 별로 Repository 빈 생성
      • AutoConfiguration 클래스 설정 파일에 @Import(WooRestClientAutoConfigureRegistrar.class)로 등록합니다.
      • AutoConfiguration과 별개로 주소별 기능별 WebClient 빈을 각각 생성합니다.
      • 각 Repository에는 @WooRestClient를 선언하여 용도에 맞는 WebClient빈을 지정합니다.
      • BeanRegister에서 각 Repository에 선언 된 @WooRestClient 정보를 스캔하여 Repository를 빈으로 등록합니다.
    • 위 과정들은 스프링 프로세스가 기동 될 때 처리됩니다.
반응형