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을 쉽게 응답받는다.

 

 

RestTemplate

Deprecated

 

WebClient는 기존에 많이 사용하던 RestTemplate을 대체합니다.

향후 버전에서는 RestTemplate이 곧 폐지될 예정입니다.

Spring Framework 5에서 부터 WebFlux 스택과 함께 Spring은 WebClient라는 새로운 HTTP 클라이언트를 도입했기 때문에 RestTemplate 사용은 지양하는 것을 추천드립니다.

폐지가 될테니 새로운 애플리케이션을 개발하거나 오래된 애플리케이션을 리팩터링 하여 WebClient를 사용하는 것이 좋겠죠?

 

 

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

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

 

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

 

📌  build()

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

Options

- uriBuilderFactory : base url을 커스텀한 UriBuilderFactory

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

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

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

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

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

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

 

 

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

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

 

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

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

 

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

한 번 빌드한 뒤부터 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

 

반응형