2022. 3. 17. 23:47ㆍSpring
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?
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()
정말 단순하게 WebClient의 디폴트 세팅으로 아래와 같이 생성할 수 있으며, 요청할 uri와 함께 생성할 수도 있습니다.
import org.springframework.web.reactive.function.client.WebClient;
WebClient.create();
// or
WebClient.create("http://localhost:8080");
📌 builder()
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
요청에 대한 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으로 바로 적용하게끔 바꿔주세요.
tcpConfiguration는 deprecated 예정이며, 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()
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);
}
});
📌 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
에러 핸들링은 결과 값을 반환받을 때의 상황에 따라 적절히 처리할 수 있습니다.
retrieve와 exchangeToXXXX 각각 어떻게 처리할지 살펴봅시다.
자세한 내용은 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
| 참고 |
'Spring' 카테고리의 다른 글
Spring, Bulk Insert 성능 측정 (0) | 2022.03.29 |
---|---|
Spring Security, 어렵지 않게 설정하기 (6) | 2022.03.24 |
Spring Interceptor, 제대로 이해하기 (6) | 2022.03.15 |
Spring Transaction, 제대로 이해하기 (0) | 2022.03.13 |
@ExceptionHandler, @ControllerAdvice (3) | 2022.03.08 |
Backend Software Engineer
𝐒𝐮𝐧 · 𝙂𝙮𝙚𝙤𝙣𝙜𝙨𝙪𝙣 𝙋𝙖𝙧𝙠