Spring/스프링으로 시작하는 리액티브 프로그래밍

[Spring WebFlux] WebClient

꾸준함. 2024. 10. 7. 18:57

WebClient

  • 비동기적이고 non-blocking 방식으로 HTTP 요청을 처리할 수 있는 클라이언트
  • 전통적인 Spring MVC의 RestTemplate의 대안으로 Spring 5부터 지원하는 non-blocking HTTP 요청을 위한 리액티브 웹 클라이언트로서 함수형 기반의 향상된 API 제공
    • 비동기적 호출 외에도 block() 메서드를 사용하여 동기적으로 데이터를 받을 수 있지만 WebFlux 환경에서는 가능한 비동기 방식으로 사용하는 것이 성능에 유리

 

  • WebFlux 환경에서의 고성능, 비동기적 애플리케이션 개발을 위해 설계됐으며 기본 HTTP 클라이언트 라이브러리는 Reactor Netty
  • WebClient는 Mono와 Flux를 지원하여, 리액티브 스트림을 통해 데이터를 처리
    • Mono는 하나의 요소를 반환할 때 사용
    • Flux는 여러 요소를 처리할 때 사용

 

WebClient  코드 예시 및 사용법

  • Spring에서 지원하는 ApplicationRunner를 이용해 도서 정보 샘플 애플리케이션이 실행되는 시점에 총 네 번의 HTTP 요청을 전송하도록 설정

 

@Slf4j
@Configuration
public class WebClientExample01 {
@Bean
public ApplicationRunner examplesWebClient() {
return (ApplicationArguments arguments) -> {
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 =
new BookDto.Patch.PatchBuilder().titleKorean("Java 고급")
.titleEnglish("Advanced Java")
.description("Java 고급 프로그래밍 마스터")
.author("Tom")
.build();
WebClient webClient = WebClient.create("http://localhost:8080");
Mono<BookDto.Response> response =
webClient
.patch()
.uri("http://localhost:8080/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));
}
}
view raw .java hosted with ❤ by GitHub

 

1. exampleWebClient01 메서드 설명

  • 테스트 대상인 BookRouter의 POST()로 전송할 request body를 작성
  • WebClient.create() 메서드를 통해 WebClient 인터페이스의 구현 객체 생성
  • post() 메서드를 통해 HTTP 메서드 타입을 POST로 지정
  • uri() 메서드로 request를 전송할 URI 지정
  • bodyValue() 메서드로 request body를 설정하며 bodyValue() 메서드는 body(BodyInserter)의 단축 메서드
  • retrieve() 메서드는 응답을 어떤 형태로 얻을지에 대한 프로세스의 시작을 선언하는 역할
  • toEntity(Void.class) 메서드는 파라미터로 주어진 클래스의 형태로 변환한 response body가 포함된 ResponseEntity 객체를 반환
    • BookHandler의 createMember() 메서드가 반환하는 ServerResponse에 body 데이터가 추가되지 않기 때문에 toEntity()의 파라미터는 Void.class가 됨

 

  • 최종 응답으로 전달받은 Mono<ResponseEntity<Void>>를 구독하여 응답으로 전달받은 status code와 location header 정보를 로그로 출력

 

2. exampleWebClient02 메서드 설명

  • 테스트 대상인 BookRouter의 PATCH()로 전송할 request body를 작성
  • WebClient.create(baseUrl) 메서드를 통해 WebClient 인터페이스의 구현 객체 생성
  • patch() 메서드를 통해 HTTP 메서드 타입을 PATCH로 지정
  • uri() 메서드로 요청을 전송할 URI를 지정하며 path variable이 포함된 URI의 경우 Varargs의 형태로 path variable 값 전달
  • bodyToMono()를 통해 response body를 파라미터로 전달된 타입의 객체로 디코딩

 

3. exampleWebClient03 메서드 설명

  • get() 메서드를 통해 HTTP 메서드 타입을 GET으로 지정
  • uri() 메서드로 요청을 전송할 URI를 지정하며 uriBuilder를 통해 path variable의 값을 포함한 URI를 생성

 

4. exampleWebClient04 메서드 설명

  • uri() 메서드로 요청을 전송할 URI를 지정하며 uriBuilder의 queryParam() 메서드를 이용해 페이지네이션을 위한 쿼리 파라미터인 page와 size의 값을 포함한 URI 생성
  • bodyToFlux()를 통해 response body를 파라미터로 전달된 타입의 객체로 디코딩
    • bodyToFlux()는 bodyToMoo()와 달리 자바 컬렉션 타입의 response body 수신

 

WebClient Connection Timeout 설정

  • WebClient는 특정 서버 엔진의 HTTP Client Connector 설정을 통해 HTTP Connection에 대한 timeout을 설정 가능


@Slf4j
@Configuration
public class WebClientExample02 {
@Bean
public ApplicationRunner examplesWebClient02() {
return (ApplicationArguments arguments) -> {
exampleWebClient01();
};
}
private void exampleWebClient01() {
HttpClient httpClient =
HttpClient
.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 500)
.responseTimeout(Duration.ofMillis(500))
.doOnConnected(connection ->
connection
.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));
}
}
view raw .java hosted with ❤ by GitHub

 

코드 설명

  • option() 메서드로 Connection 설정을 위한 HTTP Connection이 연결되기까지의 시간인 timeout 시간 설정
  • responseTimeout() 메서드로 응답을 수신하기까지의 Timeout 시간 설정
  • doOnConnected()의 파라미터로 전달되는 람다 표현식을 통해 Connection이 연결된 이후에 수행할 동작 정의
    • 특정 시간 동안 읽을 수 있는 데이터가 없을 경우 ReadTimeoutException 예외를 발생시키는 ReadTimeoutHandler 등록
    • 특정 시간 동안 쓰기 작업을 종료할 수 없을 경우 WriteTimeoutException을 발생시키는 WriteTimeoutHandler 등록

 

  • WebClient의 객체는 Builder 패턴을 통해서도 생성할 수 있는데 이 경우 clientConnector() 메서드를 이용해 앞에서 설정한 서버 엔진의 HTTP Client Connector 설정
    • 여기서는 Reactor Netty에서 제공하는 HttpClient 객체를 생성자 파라미터로 가지는 ReactorClientHttpConnector를 설정

 

exchangeToMono()를 사용한 응답 디코딩

  • retrieve() 대신에 exchangeToMono()나 exchangeToFlux() 메서드를 이용하면 response를 사용자의 요구 조건에 맞게 제어 가능


@Slf4j
@Configuration
public class WebClientExample02 {
@Bean
public ApplicationRunner examplesWebClient02() {
return (ApplicationArguments arguments) -> {
exampleWebClient02();
};
}
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().equals(HttpStatus.CREATED))
return response.toEntity(Void.class);
else
return response
.createException()
.flatMap(throwable -> Mono.error(throwable));
})
.subscribe(res -> {
log.info("response status2: {}", res.getStatusCode());
log.info("Header Location2: {}", res.getHeaders().get("Location"));
},
error -> log.error("Error happened: ", error));
}
}
view raw .java hosted with ❤ by GitHub

 

코드 설명

  • exchangeToMono()를 통해 응답을 수신한 다음 HttpStatus가 CREATED이면 ResponseEntity를 반환하고 그 외에는 Exception을 던지도록 처리
  • ClientResponse의 createException() 메서드는 request/response 정보를 포함한 WebClientResponseException 생성

 

참고

  • 스프링으로 시작하는 리액티브 프로그래밍 (황정식 저자)

 

반응형