
WebClient는 Spring 5에서 도입된 비동기 HTTP 클라이언트이다. 함수형 API를 제공하며, Non-Blocking 요청을 처리할 수 있도록 설계되었다. 내부적으로는 Netty 기반의 Reactor Netty를 사용하며, 기존 RestTemplate과 달리 Non-Blocking과 Blocking 요청을 모두 지원한다.
이러한 특징 덕분에 WebClient는 WebFlux 기반 애플리케이션에서 RestTemplate의 대체 수단으로 널리 사용된다.
예제에서는 ApplicationRunner를 통해 애플리케이션 실행 시점에 WebClient로 4개의 HTTP 요청을 수행한다. 각각 POST, PATCH, GET 단건 조회, GET 목록 조회 요청으로 구성되어 있다.
@Slf4j
@Configuration
public class WebClientExample01 {
@Bean
public ApplicationRunner examplesWebClient() {
return args -> {
exampleWebClient01();
exampleWebClient02();
exampleWebClient03();
exampleWebClient04();
};
}
private void exampleWebClient01() {
BookDto.Post requestBody = new BookDto.Post(
"Java 중급",
"Intermediate Java",
"Java 고급 프로그래밍 마스터",
"Kevin1",
"222-22-2222-222-2",
"2022-03-22"
);
WebClient webClient = WebClient.create();
Mono<ResponseEntity<Void>> response = webClient
.post()
.uri("http://localhost:8080/v10/books")
.bodyValue(requestBody)
.retrieve()
.toEntity(Void.class);
response.subscribe(res -> {
log.info("response status: {}", res.getStatusCode());
log.info("Header Location: {}", res.getHeaders().get("Location"));
});
}
private void exampleWebClient02() {
BookDto.Patch requestBody = BookDto.Patch.builder()
.titleKorean("Java 고급")
.titleEnglish("Advanced Java")
.description("Java 고급 프로그래밍 마스터")
.author("Tom")
.build();
WebClient webClient = WebClient.create("http://localhost:8080");
Mono<BookDto.Response> response = webClient
.patch()
.uri("/v10/books/{book-id}", 20)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(BookDto.Response.class);
response.subscribe(book -> {
log.info("bookId: {}", book.getBookId());
log.info("titleKorean: {}", book.getTitleKorean());
log.info("titleEnglish: {}", book.getTitleEnglish());
log.info("description: {}", book.getDescription());
log.info("author: {}", book.getAuthor());
});
}
private void exampleWebClient03() {
Mono<BookDto.Response> response = WebClient
.create("http://localhost:8080")
.get()
.uri(uriBuilder -> uriBuilder
.path("/v10/books/{book-id}")
.build(21))
.retrieve()
.bodyToMono(BookDto.Response.class);
response.subscribe(book -> {
log.info("bookId: {}", book.getBookId());
log.info("titleKorean: {}", book.getTitleKorean());
log.info("titleEnglish: {}", book.getTitleEnglish());
log.info("description: {}", book.getDescription());
log.info("author: {}", book.getAuthor());
});
}
private void exampleWebClient04() {
Flux<BookDto.Response> response = WebClient
.create("http://localhost:8080")
.get()
.uri(uriBuilder -> uriBuilder
.path("/v10/books")
.queryParam("page", "1")
.queryParam("size", "10")
.build())
.retrieve()
.bodyToFlux(BookDto.Response.class);
response.map(book -> book.getTitleKorean())
.subscribe(bookName -> log.info("book name: {}", bookName));
}
Reactor Netty 기반의 커넥터를 사용하여 커넥션, 읽기, 쓰기 타임아웃을 설정할 수 있다.
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 500)
.responseTimeout(Duration.ofMillis(500))
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(500, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(500, TimeUnit.MILLISECONDS)));
Flux<BookDto.Response> response = WebClient.builder()
.baseUrl("http://localhost:8080")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build()
.get()
.uri(uriBuilder -> uriBuilder
.path("/v10/books")
.queryParam("page", "1")
.queryParam("size", "10")
.build())
.retrieve()
.bodyToFlux(BookDto.Response.class);
response.map(book -> book.getTitleKorean())
.subscribe(bookName -> log.info("book name2: {}", bookName));
retrieve() 대신 exchangeToMono()를 사용하면 상태 코드나 헤더 등을 기반으로 응답 처리 로직을 분기할 수 있다.
private void exampleWebClient02() {
BookDto.Post post = new BookDto.Post(
"Java 중급",
"Intermediate Java",
"Java 고급 프로그래밍 마스터",
"Kevin1",
"333-33-3333-333-3",
"2022-03-22"
);
WebClient webClient = WebClient.create();
webClient.post()
.uri("http://localhost:8080/v10/books")
.bodyValue(post)
.exchangeToMono(response -> {
if (response.statusCode().is2xxSuccessful()) {
return response.bodyToMono(Void.class);
} else {
return response.createException().flatMap(Mono::error);
}
})
.subscribe();
}