📘 Backend/Spring

Spring WebClient

신건우 2023. 11. 27. 22:09

📘 Spring WebClient란?

Spring WebClient를 계속 써왔었는데, 글로 남기는걸 자꾸 미루다가 이제서야 포스팅합니다.

Spring WebClient는 RestTemplate을 대체하는 HTTP Client 입니다.

기존 Sync API를 제공하며, Non-Blocking & Async 방식을 지원해서 효율적인 통신이 가능합니다.


WebClient는 Builder 방식의 인터페이스를 사용하며, 외부 API로 요청을 할 때 리액티브 타입의 전송과 수신을 합니다. (Mono, Flux)


📕 WebClient의 특징

  • Single Thread 방식
  • Non-Blocking 방식
  • Json, Xml의 쉬운 응답

📕 제어권 반환 : Blocking & Non-Blocking

Blocking

  • Application이 Kernel로 작업 요청을 할 때, Kernel에서는 요청에 대한 로직을 실행합니다.
  • 이 때, Application은 요청에 대한 응답을 받을 때까지 대기합니다.
  • Application은 Kernel이 작업을 끝낼때까지 백그라운드에서 작업이 끝났는지 지속적으로 확인합니다. (Polling)

Non-Blocking

  • Application이 요청을 하고 바로 제어권을 돌려받습니다.
  • 즉, 요청이 Blocking 되지 않았으므로 Non-Blocking이라고 불립니다.

📘 Sync & Asyn / Blocking & Non-Blocking 개념

Blocking & Non-Blocking은 제어권 반환에 중점을 두지만, Sync & Async는 응답값 반환에 중점을 둡니다.

Sync는 결과값을 직접 받아 처리하는 반면, Async는 결과값을 받을때 어떻게 할지의 CallBack 함수를 미리 정의합니다.

img


📌 Sync Blocking / Sync Non-Blocking

Sync Blocking

동기 방식의 블로킹은 작업을 수행한 순서에 맞게 수행됩니다.

  • Kernel : 작업 할동안 대기하셈
  • Application : 기다리는중
  • Kernel : 끝남, 결과 받으셈

Sync Non-Blocking

동기 방식의 논블로킹은 작업을 시작하고 제어권을 다시 돌려주기 때문에 다른 작업을 수행할 수 있습니다.

종료 시점은 Application단의 Process or Thread가 Polling(지속적인 완료 확인) 을 합니다.

  • Kernel : 나 작업할 동안 딴거 하고 있으셈
  • Application : (다른 일 하면서) 다 됨?
  • Kernel : ㄴㄴ
  • Application : (다른 일 하면서2) 다 됨?
  • Kernel : ㅇㅇ 받으셈 (결과값)

📌 Async Blocking / Async Non-Blocking

Async Blocking

비동기 블로킹 방식은 비동기의 장점을 못살리는 대표적인 경우입니다.

  • Application : 결과 나오면 알려주셈
  • Kernel : ㄴㄴ 작업 동안 기달리셈 (Blocking)
  • .....
  • Kernel : 끝남 가져가셈 (결과값)
  • Application : ㅇㅇ..

Async Non-Blocking

비동기 논블로킹 방식은 작업에 대한 서로의 자유도가 높습니다.

각자 할일을 수행하며, 필요한 시점에 각자 결과를 처리합니다.

  • Application : 결과 나오면 알려주셈
  • Kernel : ㅇㅇ 다른거 해
  • .....
  • Kernel : 작업 끝남 (결과물)
  • Application : ㄱㅅ

📘 WebClient 생성

WebClient를 생성하는 방법은 2가지가 있습니다.

단순하게 create() 이용하는 방법과, option을 추가할 수 있는 build()를 사용한 생성이 있습니다.


📕 Dependencies

implementation 'org.springframework.boot:spring-boot-starter-webflux'

📕 create()

  • 단순하게 WebClient의 Default Setting으로 아래와 같이 생성합니다. Base URL과 함께 생성도 가능합니다.
WebClient.create();
// or
WebClient.create("http://localhost:8080");

📕 builder()

설정을 Custom하게 바꿔서 널을 수 있는 DefaultWebClientBuilder 클래스를 사용하는 build()를 사용합니다.

Options

  • uriBuilderFactory : Base URL을 커스텀 한 UriBuilderFactory
  • defaultHeader : 모든 요청에 사용할 헤더
  • defaultCookie : 모든 요청에 사용할 쿠키
  • defaultRequest : 모든 요청을 커스텀할 Consumer
  • filter : 묘든 요청에 사용할 클라이언트 필터
  • exchangeStrategies : HTTP Message Reader & Writer 커스터마이징
  • clientConnector : HTTP Client Library Settings

// 예시
@Bean 
public WebClient webClient() {  
    return WebClient.builder()  
            .baseUrl("http://localhost:8080")  
            .defaultCookie("cookieKey", "cookieValue")  
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)  
            .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))  
            .build();  
}

📘 WebClient Configuration

WebClient에서의 TimeOut 처리나 ErrorHandling 같은 세부적인 설정 방법입니다.


📕 TimeOut

특정 WebClient Bean에 대한 모든 요청의 TimeOut을 전역적으로 설정합니다.

  • ConnectTimeOut : 5000
  • ReadTimeOut : 5000
  • WriteTimeOut : 5000
  • 위 같이 설정한 HttpClient 객체를 clientConnector()에 주입합니다.
import io.netty.channel.ChannelOption;  
import io.netty.handler.timeout.ReadTimeoutHandler;  
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import java.time.Duration;  
import java.util.concurrent.TimeUnit;

@Bean  
public WebClient webClient() {  
    HttpClient httpClient = HttpClient.create()  
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)  
            .responseTimeout(Duration.ofMillis(5000))  
            .doOnConnected(i ->   
                    i.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))  
                    .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));  

    return WebClient.builder()  
            .clientConnector(new ReactorClientHttpConnector(httpClient))  
            .build();  
}

📕 mutate()

한 번 빌드된 WebClient는 Immutable (불변) 합니다.

WebClient를 Singleton으로 사용할 때 Default Setting과 다르게 사용하고 싶을때도 분명 있을겁니다.

그럴 때, mutate()를 사용하여 Singleton인 WebClient Bean 객체에 다른 설정값을 가지는 요청을 할 수 있습니다.

WebClient Server = WebClient.builder().filter(filterA).filter(filterB).build();

WebClient Client = WebClient.builder().filter(filterA).filter(filterB).filter(filterC).build();

같은 Singleton WebClient 인스턴스지만, mutate() 를 이용해 서로 다른 설정 값을 가지는 요청을 할 수 있습니다.


📘 Request

WebClient는 WebFlux와 같이 나온만큼 Mono와 Flux를 메인으로 사용합니다.

만약 Reactive에 대한 이해가 부족 하다면 Reactive 이해하기를 참고하시길 바랍니다.

요청은 Get과 Post만 알면 Put, Delete는 비슷하게 사용할 수 있으니 Get, Post만 알아보겠습니다.


📕 Get

일반적으로 Get은 단일 리소스(Mono) 혹은 리소스 모음(Flux)를 가져옵니다.

대부분 어렵지 않게 사용할 수 있습니다.

코드에 나오는 retrive()와 bodyToXX는 Response에서 알아보겠습니다.


Mono

public Mono<Person> findById(final Integer id) {
    return webClient.get()
                            .uri("/person/" + id)
                            .retrieve()
                            .bodyToMono(Person.class);
}

Flux

public Flux<Person> findAll() {
    return webClient.get()
                            .uri("/persons")
                            .retrieve()
                            .bodyToFlux(Person.class);
}

📕 Post

Post의 body() 부분에 반환 타입이 있으면 Mono<Person> 없으면 Mono<Void>를 지정해 줍니다.

public Mono<Person> create(Person data) {
    return webClient.post()
                            .uri("/person")
                            .body(Mono.just(data), Person.class)
                            .retrieve()
                            .bodyToMono(Person.class);
}

📘 Response

응답을 받을땐 2개의 함수 중 적절하게 선택해서 사용하면 됩니다.

  • retrieve() : Body를 받아 디코딩 하는 간단한 함수
  • exchange() : ClientResponse를 상태값, 헤더와 같이 가져오는 함수

exchange()를 통해 Response의 세부적인 컨트롤이 가능하지만, Response 컨텐츠에 대한 모든 처리를 직접 하면,

메모리 누수 가능성 때문에 retrieve()를 권장하고 있습니다.


📕 retrieve()

retrieve()를 사용한 후 데어터는 크게 2가지 형태로 받을 수 있습니다.


toEntity()

Status, Header, Body를 포함하는 ResponseEntity 객체로 받기

Mono<ResponseEntity<Person>> monoEntity = client.get()
      .uri("/persons/1")
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .toEntity(Person.class);

toMono() / toFlux()

Body의 데이터만 받기

Mono<Person> monoEntity = client.get()
      .uri("/persons/1")
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .bodyToMono(Person.class);

exchangeToXX()

exchange()는 Deprecated 예정이니 exchangeToXX()를 사용합니다.

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

📕 block() & subscribe()

Blocking 방식을 사용하려면 block(), Non-Blocking 방식을 사용하려면 subscribe() 를 통해 콜백 함수를 지정할 수 있습니다.

// blocking
Mono<Employee> employeeMono = webClient.get(). ...
employeeMono.block()

// non-blocking
Mono<Employee> employeeFlux = webClient.get(). ...
employeeFlux.subscribe(employee -> { ... });

📘 ErrorHandling

에러 핸들링은 결과값을 반환받을 상황에 따라 적절히 처리가 가능합니다.

retrive()와 exchangeToXX() 를 각각 어떻게 처리할 지 살펴봅시다.


📕 retrieve()

retrieve는 1xx, 2xx, 3xx ... StatusCode 별로 처리가 가능합니다.

Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError, response -> ...)
        .onStatus(HttpStatus::is5xxServerError, response -> ...)
        .bodyToMono(Person.class);

📕 exchangeToXX()

exchange를 통해 받은 결과값에 대한 StatusCode를 이용해 분기 처리하여 핸들링 할 수 있습니다.

Mono<Object> monoEntity = 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());
           }
       });