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 요청을 전송하도록 설정
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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)); | |
} | |
} |
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을 설정 가능
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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)); | |
} | |
} |
코드 설명
- 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를 사용자의 요구 조건에 맞게 제어 가능
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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)); | |
} | |
} |
코드 설명
- exchangeToMono()를 통해 응답을 수신한 다음 HttpStatus가 CREATED이면 ResponseEntity를 반환하고 그 외에는 Exception을 던지도록 처리
- ClientResponse의 createException() 메서드는 request/response 정보를 포함한 WebClientResponseException 생성
참고
- 스프링으로 시작하는 리액티브 프로그래밍 (황정식 저자)
반응형
'Spring > 스프링으로 시작하는 리액티브 프로그래밍' 카테고리의 다른 글
[Spring WebFlux] Reactive Streaming 데이터 처리 (0) | 2024.10.08 |
---|---|
[Spring WebFlux] 예외 처리 (0) | 2024.09.16 |
Spring Data R2DBC 간단 정리 (0) | 2024.09.11 |
[Spring WebFlux] 함수형 엔드포인트(Functional Endpoint) (0) | 2024.09.06 |
[Spring WebFlux] 애너테이션 기반 컨트롤러 (0) | 2024.08.20 |