Post

(Spring) WebClient 생성/설정/로깅

WebClient vs. RestTemplate

왜 WebClient ?: RestTemplate은 deprecated 예정.

docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html

As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.

  • RestTemplate은 synchronous, blocking call 밖에 할 수 없으나, WebClient는 여기에 추가로 async, nonblocking IO를 지원한다.
  • 그래서 Spring MVC를 쓰든, WebFlux를 쓰든 WebClient를 사용하는 것이 좋다.

WebClient / RestTemplate 비교

  • 요 것만 보면 이해됨. www.baeldung.com/spring-webclient-resttemplate
  • “Mono나 Flux를 리턴하는 컨트롤러는 바로 리턴하며 끝나버리고, 나중에 Mono가 준비되면 이를 컨슘하며 클라이언트 쪽으로 내려준다.”
    • 이 것을 Framework 단에서 지원해주어야 하는 것이고, 그래서 NIO 지원이 중요하다고 하는 것
1
2
3
4
5
6
7
8
9
10
11
12
13
Starting BLOCKING Controller!
Tweet(text=RestTemplate rules, username=@user1)
Tweet(text=WebClient is better, username=@user2)
Tweet(text=OK, both are useful, username=@user1)
Exiting BLOCKING Controller!
--------------------------------------
// WebClient는 non-blocking으로 호출되자마자 바로 Flux<Tweet>를 리턴하면서 지나감.
// 따라서 컨트롤러도 바로 Exit.
Starting NON-BLOCKING Controller!
Exiting NON-BLOCKING Controller!
Tweet(text=RestTemplate rules, username=@user1)
Tweet(text=WebClient is better, username=@user2)
Tweet(text=OK, both are useful, username=@user1)

WebClient 객체 생성 및 설정

생성

WebClient.create()로 만드는 방법

  • 이렇게 직접 만들면, 당연히 Spring auto config 적용 받지 않는다.
  • 따라서 application.properties 설정도 당연히 적용되지 않는다.

WebClient.Builder DI 받아서 만드는 방법 (추천)

  • Spring WebClientAutoConfiguration 이 제공하는 WebClient.Builder를 DI 받아 WebClient를 생성하는 방법.
  • Spring Boot는 WebClient를 기본 Bean으로 제공하지 않는다. 대신 WebClient.Builder를 기본 Bean으로 제공하고 있다.
    • WebClient 자체가 아니라 Builder를 기본 Bean으로 제공하는 것은 안정성과 확장성 때문 인 듯.
    • 기존 RestTemplate은 빌더가 아니라 RestTemplate 자체를 Bean으로 제공하고 있어서, 클라이언트가 기본 설정에 설정을 덧씌우고 싶은 경우 RestTemplate 자체에 설정 메서드를 호출해서 상태를 변경했다.
    • 이처럼 RestTemplate은 런타임에 설정 수정이 가능했는데, 상당히 fragile하다. (같은 bean을 공유해서 사용하고 있는데 어디선가 갑자기 설정을 바꿔버리면?)
    • 반면 Builder를 DI하는 방식은, 클라이언트가 추가 설정을 덧씌우고 싶은 경우 build 하기 전에 얼마든지 설정 가능하다. 설정이 끝나고 build 한 다음 부터는 해당 인스턴스의 설정이 수정되지 않도록 설계 할 수 있어 안정성도 좋아진다.
  • auto config를 통해 생성된 Bean이므로 기본 설정이 적용되어 있다.
  • 내부적으로는 Spring 전역 ObjectMapper 사용하게 된다.

docs.spring.io/spring-boot/docs/2.1.18.RELEASE/reference/html/boot-features-webclient.html

Spring Boot creates and pre-configures a WebClient.Builder for you; it is strongly advised to inject it in your components and use it to create WebClient instances. Spring Boot is configuring that builder to share HTTP resources, reflect codecs setup in the same fashion as the server ones (see WebFlux HTTP codecs auto-configuration), and more.

로깅

logging : SpringBoot maintainer Brian Clozel의 답변.

1
2
3
4
5
6
7
8
9
10
11
spring.http.log-request-details=true   // Boot 버전 2.1.0 이상일 때
 
// 그 이하라면.
@Configuration
static class LoggingCodecConfig {
  @Bean
  @Order(0)
  public CodecCustomizer loggingCodecCustomizer() {
    return (configurer) -> configurer.defaultCodecs().enableLoggingRequestDetails(true);
  }
}

logging 비교 : 전역 로깅과 별도로, 필요한 응답만 INFO 로깅하려는 경우

e.g., 전역 로깅은 DEBUG로, 모든 요청/응답 헤더바디를 로깅하고 있다. 따라서 INFO 로깅에서는 결과매핑객체를 로깅하려 한다. Mono의 소비 방법(block, awaitSingle…)은 외부에게 맡기고, 내부에서는 로깅만 강제하고 싶은 상황

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1.
.log(this.javaClass.simpleName, Level.INFO)  
// => Rx step이 모조리 로깅되기 때문에 권장하지 않음.
 
2.
.doOnSuccess { log().info("$it") }
// => Mono에는 사용 가능. 그러나 Flux에서는 사용 불가.
// Flux는 리스트도 맵도 아니기 때문에 Flux를 로깅한다는게 애매하다. 
// Mono는 doOnSuccess 시점에는 T타입이 되는데, Flux는 비슷하게 끝나는 시점에 ?<T> 타입이 된다.(e.g., List<T>, Map<T>) 
// 근데 Flux가 반환 타입이면 ?<T>를 결정 할 수가 없고... 그래서 파라미터로 받는 타입을 결정할 수 없어 로깅 불가.
 
2-1. Flux<T> Mono<Collection<T>>으로 바꿔서 리턴하도록 하면 doOnSuccess 가능.
// 단점은... Mono<Collection<T>> 타입이므로 코드가 아래와 같이 바뀌어야 해서 nested depth가 생긴다.
As-is : Flux
  .filter { it.symbol.endsWith("US") }
  .map { it.symbol }
 
To-be1 : Mono
  .map {
    it.filter { it.symbol.endsWith("US") }
      .map { it.symbol }
  }
To-be2 : Mono
  .flatMapIterable { it }
  .filter { it.symbol.endsWith("US") }
  .map { it.symbol }
 
3.
아예 외부에서 별도로 logging 하는 방법
// 로깅 누락 가능성 있음.
  • 사실 생각해보면,Flux라는건 Rx에서 스트림 형태의 데이터가 순차적으로 준비될 때, 준비되는 대로 그때그때 받는 것에 의미 가 있는건데 API call 응답으로 list를 수신해서 한 방에 모든 Flux element가 준비되는 상황에서는, 굳이 Flux를 쓸 이유가 없다.
  • 방법 2. & 2-1. 을 사용하고 To-be1, To-be2는 편한대로 상황에 맞게 사용하면 그나마 깔끔하게 처리 가능하며 데이터의 성격도 잘 대변할 수 있다.
에러 로깅?
1
2
.doOnError(WebClientResponseException::class.java) {
    logger.error("### [WebClientResponseException] uri=${t.request?.uri}, Status=${t.rawStatusCode}, Body=${t.responseBodyAsString}")}
This post is licensed under CC BY 4.0 by the author.