Spring WebClient, 어렵지 않게 사용하기

2022. 3. 17. 23:47Spring

WebClient는 스프링 5.0에서 추가된 Blocking과 Non-Blocking 방식을 지원하는 HTTP 클라이언트입니다.

 

 

- Reactor, 제대로 사용하기 - Error Handling

Reactive Programming, 제대로 이해하기

👉🏻 WebClient 소개

- Spring WebClient, 제대로 사용하기 - retrieve

- Spring WebClient, 제대로 사용하기 - exchange

 

 

WebClient의 설정 및 요청, 응답 처리 등의 사용법을 학습하는 것이 본 포스팅의 목적입니다.

 

-----------------    INDEX     -----------------

 

WebClient?

Create

📌 create()

📌 build()

Configuration

📌 Timeout

📌 mutate()

Request

📌 GET

📌 POST

Response

📌 retrieve()

📌 exchangeToXXXX()

📌 Mono<>, Flux<> 사용

ErrorHandling

📌 retrieve()

📌 exchangeToXXXX() 

 

----------------------------------------------

 

WebClient?

🔗 Spring.io Link

 

WebClient는 RestTemplate를 대체하는 HTTP 클라이언트입니다. 

기존의 동기 API를 제공할 뿐만 아니라, 논블로킹 및 비동기 접근 방식을 지원해서 효율적인 통신이 가능합니다.

 

WebClient는 요청을 나타내고 전송하게 해주는 빌더 방식의 인터페이스를 사용하며,

외부 API로 요청을 할 때 리액티브 타입의 전송과 수신을 합니다. (Mono, Flux)

 

WebClient은 아래와 같은 특징을 정리하면 아래와 같습니다.

- 싱글 스레드 방식을 사용

- Non-Blocking 방식을 사용

- JSON, XML을 쉽게 응답받는다.

 

 

Non-Blocking

위에서 잠시 Non-Blocking 이라는 특징을 언급했습니다.

노드를 사용해보신 분들은 아마 익숙한 단어일텐데요.

 

✔️  제어권 반환

Blocking

: Application이 kernel로 작업 요청을 할 때, kernel에서는 요청에 대한 로직을 실행합니다.

이 때, Application은 요청에 대한 응답을 받을 때까지 대기를 합니다.

Application은 kernel이 작업을 끝낼 때까지 백그라운드에서 작업이 끝났는지 지속적으로 확인합니다.

 

Non-blocking

: Application이 요청을 하고 바로 제어권을 받습니다.

다른 로직을 실행할 수 있게끔 말이죠.

이 것이 바로 blocking이 되지 않았다고 해서 non-blocking I/O 입니다.

 

 

Async VS Non-Blocking

블로킹/논블로킹이 제어권 반환에 중점을 두었다면,

동기/비동기는 결과를 바로 주는 지에 초점을 맞추면 이해할 수 있습니다.

 

동기는 작업 결과값을 직접 받아 처리하는 반면

비동기는 결과값을 받으면 어떻게 할지의 콜백함수를 미리 정의해둡니다.

 

그림은 왼쪽인 Application, 오른쪽이 Kernel입니다.

 

 

Sync-Blocking

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

 

Kernel: 작업 동안 기다려
Application: 결과 나올 때까지 기다릴게

...

Kernel: 작업 끝남. 결과 줄게

 

 

Sync-Nonblocking

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

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

 

Kernel: 작업 동안 다른 작업을 해

Application: (다른일...) 결과 나옴?

Kernel: 아직

Application: (다른일...) 결과 나옴?

Kernel: 응

 

 

Async-Blocking

비동기 방식의 블로킹은 작업은 비동기의 장점을 못살리는 경우입니다. 그래서 납득이 잘 안갈 수 있죠.

 

Application: 결과 나오면 알려줘

Kernel: 아니, 작업 동안 기다려

...

Kernel: 작업 끝남

Application:  그래 ..

 

 

Async-NonBlocking

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

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

 

Application: 결과 나오면 알려줘

Kernel: 그래, 이 작업 동안 다른 작업을 해

...

Kernel: 작업 끝남

...

Application:  그랭

 

 

이제 WebClient를 사용하는 방법에 대해 다뤄보겠습니다.

 

 

Dependencies

먼저, Webclient 의존성을 추가해줍니다.

 

// maven
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>


// gradle
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-webflux'
}

 

 

Create

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

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

 

📌  create()

🔗 official link

 

정말 단순하게 WebClient의 디폴트 세팅으로 아래와 같이 생성할 수 있으며, 요청할 uri와 함께 생성할 수도 있습니다.

 

import org.springframework.web.reactive.function.client.WebClient;

WebClient.create();
// or
WebClient.create("http://localhost:8080");

 

📌  builder()

🔗 official link

 
혹은 모든 설정을 customization할 수 있도록 
DefaultWebClientBuilder 클래스을 사용하는 build() 메서드를 사용할 수도 있습니다.
 
적용할 수 있는 옵션은 아래와 같습니다.
 
 

Options

- uriBuilderFactory : base url을 커스텀한 UriBuilderFactory

- defaultHeader : 모든 요청에 사용할 헤더

- defaultCookie : 모든 요청에 사용할 쿠키

- defaultRequest : 모든 요청을 커스텀할 Consumer

- filter : 모든 요청에 사용할 클라이언트 필터

- exchangeStrategies : HTTP 메시지 reader & writer 커스터마이징

- clientConnector : HTTP 클라이언트 라이브러리 세팅

 

 

실제 사용을 확인해 보면 더욱 이해가 잘 가겠죠?

아래 코드를 확인해 봅시다.

 

import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import java.util.Collections;

WebClient client = 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();
 

 

위와 같이 세팅할 수 있습니다.

 

 

Configuration

통신을 할 때에는 세부적인 설정이 중요하곤 합니다.

Timeout 처리나 ErrorHandling과 같은 부분에서 많이 느낄 수 있는데요.

각각 어떻게 설정할 수 있을지 확인해봅시다.

 

📌  Timeout

🔗 baeldung link

 

요청에 대한 timeout은 아래와 같이 설정할 수 있습니다.

 

import reactor.netty.http.client.HttpClient;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

HttpClient httpClient = HttpClient.create()
  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
  .responseTimeout(Duration.ofMillis(5000))
  .doOnConnected(conn -> 
    conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
      .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));

WebClient client = WebClient.builder()
  .clientConnector(new ReactorClientHttpConnector(httpClient))
  .build();


Connect Timeout 과 ReadTimeout, WriteTimeout을 모두 5000ms로 지정한 HttpClient 객체를 만들어 주입할 수 있습니다.

 

⚠️ 혹시 .tcpConfiguration() 을 통해 Connect Timeout을 사용했던 분이라면,

위와 같이 option으로 바로 적용하게끔 바꿔주세요.

 

tcpConfigurationdeprecated 예정이며, option을 하면 기존의 방식과 동일한 설정을 할 수 있습니다.

 

 

📌  mutate()

🔗 official link (참조는 하되, 내용 거의 없음)

 

한 번 빌드한 뒤부터 WebClient는 immutable 한데요. 
WebClient를 Singleton으로 사용할 때 default setting과 다르게 사용하고 싶을 수도 있겠죠.

그럴 때에는 mutate()를 사용할 수 있습니다.

 

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

WebClient client2 = client1.mutate()
        .filter(filterC).filter(filterD).build();

// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD

 

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

 

 

 

Request

그럼, 이제 요청을 보내볼까요?

WebFlux와 함께 나온만큼 리액터 Mono와 Flux를 메인으로 다루는데요.

만약, 해당 개념이 낯설다면 Reactive Programming, 제대로 이해하기를 참고하시길 바랍니다.

또, 정리가 잘되어있지는 않지만 Reactor - Github 정리본을 참고하셔도 좋을 듯 합니다.

 

요청은 GET과 POST만 알아도 PUT, DELETE는 비슷하여 사용할 때 어려움이 없을테니 두 개만 확인하고 가겠습니다.

 

📌  GET

일반적으로 GET API를 사용하여 리소스 모음 또는 단일 리소스를 가져옵니다.

get() 메서드 호출을 사용한 두 사용 사례의 예를 살펴보겠습니다.

 

 

1. Flux 

GET /employees
Request: collection of employees as Flux

 

@Autowired
WebClient webClient;

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

 

 

2. Mono

GET /employees/{id}
Request: single employee by id as Mono

 

@Autowired
WebClient webClient;

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

 

위와 같이 어렵지 않게 사용하실 수 있습니다.

 

지금 retrieve와 bodyToMono()는 눈으로만 익혀주세요.

아래 Response에서 자세히 확인하실 수 있습니다.

조금 내용을 드리자면 응답을 위한 메소드입니다.

 

 

 

 

📌  POST

계속해서 body를 포함하는 POST의 예시를 보겠습니다.

 

POST /employees
Request : creates a new employee from request body
Reponse : returns the created employee

 

@Autowired
WebClient webClient;

public Mono<Employee> create(Employee empl) {
	return webClient.post()
		.uri("/employees")
		.body(Mono.just(empl), Employee.class)
		.retrieve()
		.bodyToMono(Employee.class);
}

 

여기서 body는 아래와 같이 정의되어있는데요.

body를 보면 Mono.just(empl)이 있으며 empl의 타입이 Employee이라는 클래스 정의를 합니다.

여기서 입력값이 Mono<Employee> 타입이라는 것을 확인할 수 있습니다.

 

만약, POST를 하고 받을 body가 없다면 Void.class를 입력하면 됩니다.

 

// Empty body
public Mono<Void> create(Employee empl) {
	return webClient.post()
		.uri("/employees")
		.body(Mono.just(empl), Employee.class)
		.retrieve()
		.bodyToMono(Void.class);
}

 

bodyToMono()는 아래 Response에서 확인하실 수 있습니다.

body(Object producer, Class<?> elementClass);

body(Object producer, ParameterizedTypeReference<?> elementTypeRef);

body(BodyInserter<?, ? super ClientHttpRequest> inserter);

 

요청할 데이터(producer)와 그 타입(elementClass / elementTypeRef)을 명시해주면 됩니다.

 

 

 

Response

요청을 한 후에는 응답을 받아 처리해야 겠죠.

응답을 받을 때에는 아래의 두 가지 메소드 중 적절한 것을 선택해서 사용하시면 됩니다.

 

- retrieve() : body를 받아 디코딩하는 간단한 메소드

- exchange() : ClientResponse를 상태값 그리고 헤더와 함께 가져오는 메소드

 

exchange()를 통해 세세한 컨트롤이 가능하지만, Response 컨텐츠에 대한 모든 처리를 직접 하면서 메모리 누수 가능성 때문에 retrieve()를 권고하고 있습니다.

 

bodyToFlux, bodyToMono 를 위에서 계속해서 봤는데요.

bodyToFlux, bodyToMono 는 가져온 body를 각각 Reactor의 Flux와 Mono 객체로 바꿔줍니다.

Mono 객체는 0-1개의 결과를 처리하는 객체이고, Flux는 0-N개의 결과를 처리하는 객체입니다.

이 글에서는 설명보다 예시가 더 도움이 될 것 같아 코드 예시를 가져왔습니다.

 

📌  retrieve()

retrieve를 사용한 후의 데이터는 크게 두 가지 형태로 받을 수 있습니다.

 

✔️  toEntity()

status, headers, body를 포함하는 ResponseEntity 타입으로 받을 수 있습니다.

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

 

✔️  toMono() , toFlux()

body의 데이터로만 받고싶다면 아래와 같이 사용할 수 있습니다.
Mono<Person> entityMono = client.get()
    .uri("/persons/1")
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .bodyToMono(Person.class);
 
 

📌  exchangeToXXXX()

Mono<Person> entityMono = 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);
        }
    });

 

exchange() 는 deprecated 됩니다.
exchangeToXXXX() 메소드를 사용하세요!

 

 

 

📌  Mono<>, Flux<> 사용

Mono<> Flux<>는 어떻게 사용할 수 있을까요? 

아래와 같이 Blocking 방식으로 처리하고자 할 때에는 .block(),

Non-Blocking 방식으로 처리하고자 할 때에는 .subscribe() 를 통해 callback 함수를 지정할 수 있습니다.

 

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

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

 

 

 
 

ErrorHandling

에러 핸들링은 결과 값을 반환받을 때의 상황에 따라 적절히 처리할 수 있습니다.

retrieveexchangeToXXXX 각각 어떻게 처리할지 살펴봅시다.

자세한 내용은 Reactor, 제대로 사용하기 - Error Handling 를 참고하시길 바랍니다.

 

📌  retrieve()

retrieve는 1xx, 2xx, 3xx, ... StatusCode 별로 아래와 같이 처리할 수 있습니다.

필자는 4xx, 5xx 의 에러코드일 때에만 new RuntimeException으로 처리해주도록 제작했습니다.

 

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

 

📌  exchangeToXXXX() 

exchange를 통해 받은 응답에 대한 statusCode를 이용해 분기처리하여 핸들링할 수 있습니다.

 

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

 

위와 같이 사용할 수 있습니다.

 

 

 

그럼 지금까지 WebClient에 대해 알아보았습니다.

오타나 잘못된 내용을 댓글로 남겨주세요!

감사합니다 ☺️ 

 

 

| Series |

Reactor, 제대로 사용하기 - Error Handling

Reactive Programming, 제대로 이해하기

👉🏻 WebClient 소개

Spring WebClient, 제대로 사용하기 - retrieve

Spring WebClient, 제대로 사용하기 - exchange

 

 

| 참고 |

Baedung : Spring 5 WebClient

Baedung : RestTemplate

Spring.io : WebReactive

WebClient Get Post Excample