왜 WebClient ?: RestTemplate은 deprecated 예정.

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

NOTE: 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 vs. RestTemplate

  • 요 것만 보면 이해됨. www.baeldung.com/spring-webclient-resttemplate  
  • "컨트롤러는 바로 리턴해서 끝나버리고, 나중에 Flux가 준비되면 클라이언트 쪽으로 내려준다."
    • 이 것을 Framework 단에서 지원해주어야 하는 것이고, 그래서 NIO 지원이 중요하다고 하는 것
  • Kotlin Coroutine을 사용하면 실행 흐름이 또 다름! [Kotlin/Spring] Kotlin Coroutines

```kt

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 의 제대로 된 생성법 : Auto-Configuration 적용

문제 인식 : json 수신 시 snake_case 변환 같은 것 application.properties 에서 설정 넣어주었으나 제대로 변환되지 않음.

BP 찍고 WebClient 내부의 ObjectMapper 확인해 본 결과, 

`` ObjectMapper - _serializationConfig - _base - _propertyNamingStrategy = null`` 이었음.

 

WebClient는 Jackson2CodecSupport.selectObjectMapper 를 통해서 ObjectMapper를 가져오는데, 이 클래스가 필드로 ObjectMapper 객체를 가지고 있음.

즉, Spring Configuration을 통해서 만들어진 ObjectMapper Bean을 사용하는게 아님!

 

왜 그랬을까?

 

애초에 Spring Boot는 WebClient를 기본 Bean으로 제공하고 있지 않음.

그래서 내가 직접 WebClient.create() 한 것을 Bean으로 만들었기 때문에, config가 안먹었다! (생각해보니 당연)

 

그렇다면 올바른 방법은? Spring으로 부터 WebClient.Builder를 DI 받아 WebClient를 각 컴포넌트에서 만드는 것!

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.

 

이렇게 해야 Auto-Configuration이 적용된다~

 

logging : SpringBoot maintainer Brian Clozel의 답변.

```java

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);

  }

}

```

 

Usage, 예제

retrieve() vs exchange()

docs.spring.io/spring-framework/docs/5.3.0-SNAPSHOT/spring-framework-reference/web-reactive.html#webflux-client-exchange

Unlike retrieve(), when using exchange(), it is the responsibility of the application to consume any response content regardless of the scenario (success, error, unexpected data, etc). Not doing so can cause a memory leak.
... 
Generally prefer using retrieve() unless you have a good reason to use exchange() which does allow to check the response status and headers before deciding how or if to consume the response.

 

docs.spring.io/spring-framework/docs/5.3.0/javadoc-api/org/springframework/web/reactive/function/client/WebClient.RequestHeadersSpec.html#exchange--

Deprecated. since 5.3 due to the possibility to leak memory and/or connections; please, use exchangeToMono(Function), exchangeToFlux(Function); consider also using retrieve() which provides access to the response status and headers via ResponseEntity along with error status handling.

 

기본적인 POST 요청 예제

https://medium.com/@odysseymoon/spring-webclient-%EC%82%AC%EC%9A%A9%EB%B2%95-5f92d295edc0

 

```java

Mono<ThreeDSecureResponse> mono = webClient.post()
    .uri(url)
    .contentType(APPLICATION_JSON)
    .bodyValue(new ThreeDSecureRequest(userId, uuid))
    .retrieve()
    .bodyToMono(ThreeDSecureResponse.class)
    .doOnSuccess(response -> response.validate())
    .doOnError(e -> log.warn(e.getMessage(), e));

 

mono.subscribeOn(Schedulers.elastic())

    .subscribe();

```

*** flatMap은 flatten하는데 사용되기도 하지만, Reactive에서는 Mono 안에 있는 것을 꺼내서 다음 체인으로 넘기는 용도로 더 많이 사용한다.  Reactor map, flatMap method는 언제 써야할까?

 

Spring reactor에서 얘기하는 blocking call wrapping하기

https://projectreactor.io/docs/core/release/reference/#faq.wrap-blocking  

 

여러 call을 묶어서 async way로 한번에 resolve 하기

www.baeldung.com/spring-webclient-simultaneous-calls  

stackoverflow.com/questions/43269275/parallelflux-vs-flatmap-for-a-blocking-i-o-task

```kt

fun example(): List<Ticker> =
  Flux.fromIterable(coinClient.getAllCoinList())
    .flatMap { coinClient.getTicker(it) }
    .filter { it.volume > it.yesterdayVolume }
    .collectSortedList { o1, o2 -> o1.currency.compareTo(o2.currency) }

    .log(this.javaClass.simpleName, Level.WARNING)
    .block()!!

```

 

Reactor 3 Reference Guide : Appendix A: Which operator do I need?

projectreactor.io/docs/core/release/reference/#which-operator

A.3. Peeking into a Sequence

  • 한개의 시퀀스가 전달 될 때마다 doOnNext 이벤트 발생
  • 모든 데이터가 전달 완료되면 Flux#doOnComplete, Mono#doOnSuccess 이벤트 발생
  • 전달 과정에서 오류가 발생하면 doOnError 이벤트발생
  • 등등!
  • doOnComplete는 Peeking이라서, 결과 값을 이용하려면 collectList()로 만든 다음에 써야 할 듯?

 

반복 : repeat, retry 처리하기

stackoverflow.com/questions/55923326/conditional-repeat-or-retry-on-mono-with-webclient-from-spring-webflux

 

```kt

getOrders(market, listOf("wait", "watch"))
  .flatMap { Mono.justOrEmpty(cancelOrder(it.uuid)) } // 각각에 대해서 취소 요청을 보내고
  .collectList() // 모든 취소 요청을 보낼 때 까지 대기. Mono<List>
  .repeatWhen(
    Repeat.onlyIf { _: RepeatContext<Any> -> getPendingOrders(market).isNotEmpty() }
    .exponentialBackoff(Duration.ofMillis(200), Duration.ofSeconds(1))
    .repeatMax(5)
  )

```

 

다양한 예제 코드 : baeldung

github.com/eugenp/tutorials/tree/master/spring-5-reactive-client  

javadoc 참고. 다른 것 보다 javadoc을 먼저 찾아볼 것.

 

특정 HttpStatus에서 Exception이 아니라 empty 반환하기

```java

webClient.get()
  .uri("https://abc.com/account/123")
  .retrieve()
  .bodyToMono(Account.class)

  .onErrorResume(WebClientResponseException::class.java) {
    if (it.statusCode == HttpStatus.NOT_FOUND) Mono.empty() else Mono.error(it)
    }

 

// 이 것도 WebClient docs에 있는 것인데... Mono.error 부분에서 에러난다. 왜?

Mono<Object> entityMono = client.get()
  .uri("/persons/1")
  .accept(MediaType.APPLICATION_JSON)
  .exchangeToMono(response -> {
    if (response.statusCode().equals(HttpStatus.OK)) {
      return response.bodyToMono(Person.class);
    }
    else if (response.statusCode().is4xxClientError()) {
      return response.bodyToMono(ErrorContainer.class);
    }
    else {
      return Mono.error(response.createException());
    }
});

```