원본 글: https://www.baeldung.com/spring-5-webclient
이 글은 Sprign5에서 소개된 reactive webClient인 WebClient에 대해 다룹니다.
또한, WebClient를 테스트하기 위해 제작된 WebTestclient에 대해서도 다룹니다.
1. What Is the WebClient?
- WebClient는 웹 요청을 수행하기 위한 주요 진입점을 나타내는 인터페이스입니다.
- Spring Web Reactive 모듈의 일부로 만들어졌으며, 이러한 시나리오에서 기존의 RestTemplate을 대체할 예정입니다.
또한, 새로운 클라이언트는 반응형(Reactive)이고 비동기(Non-blocking) 방식으로 동작하며, HTTP/1.1 프로토콜 위에서 작동합니다. - WebClient는 실제로 비동기 클라이언트이며 spring-webflux 라이브러리에 속하지만, 동기(Synchronous)와 비동기(Asynchronous) 작업 모두를 지원합니다.
- WebClient 인터페이스는 단 하나의 구현체(Implementation) 를 가지는데, 그것이 바로 DefaultWebClient 클래스이며, 우리가 실제로 다룰 대상입니다.
2. Dependencies
implementation 'org.springframework.boot:spring-boot-starter-webflux'
3. Working with the WebClient
client와 상호간 소통을 위해서는 다음 방법들을 숙지하고 있어야합니다.
- Webclient instance를 만드는법
- Request를 만드는법
- Response를 다루는법
3.1 Creating a WebClient Instance
Webclient를 만드는 방법으로는 크게 3가지가 있습니다.
그 중 하나는 기본 셋팅이 되어있는 WebClient 객체를 생성하는 방법입니다.
WebClient client = WebClient.create();
두 번째 방법은 baseURI을 지정하여 생성하는 방법입니다.
WebClient client = WebClient.create("http://localhost:8080");
세 번째 방법은 DefaultWebClientBuilder 클래스를 통하여 커스텀된 client를 생성합니다.
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();
3.2 Creating a WebClient Instance with Timeouts
기본적으로 HTTP 타임아웃은 30초로 설정되어 있지만, 이 값이 너무 길어서 성능에 영향을 줄 수 있습니다.
이러한 동작을 커스텀하려면 HttpClient 인스턴스를 생성하고 WebClient에서 이를 사용하도록 구성해야 합니다.
우리는 다음과 같은 설정을 할 수 있습니다:
- 연결 타임아웃 (connection timeout) 설정
- ChannelOption.CONNECT_TIMEOUT_MILLIS 옵션을 사용하여 서버와 연결하는 데 걸리는 최대 시간을 조정할 수 있습니다.
- 읽기 및 쓰기 타임아웃 (read & write timeout) 설정
- ReadTimeoutHandler와 WriteTimeoutHandler를 사용하여 데이터를 읽거나 쓰는 데 걸리는 시간 제한을 설정할 수 있습니다.
- 응답 타임아웃 (response timeout) 설정
- responseTimeout 지시어를 사용하여 서버로부터 응답을 기다리는 최대 시간을 설정할 수 있습니다.
이 모든 설정은 HttpClient 인스턴스에서 구성해야 하며, 이후 WebClient에서 이를 참조하여 적용할 수 있습니다.
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();
참고로, 클라이언트 요청에서 timeout을 호출할 수도 있지만, 이것은 신호(timeout signal) 타임아웃이지,
HTTP 연결, 읽기/쓰기, 또는 응답 타임아웃이 아닙니다.
즉, Mono/Flux 퍼블리셔의 실행 시간을 제한하는 것일 뿐이며,
실제 HTTP 요청 및 응답의 타임아웃과는 다릅니다.
3.3 Preparing a Request - Define the Method
요청을 보내기 위해서 먼저, HTTP method를 지정해줘야합니다.
UriSpec<RequestBodySpec> uriSpec = client.method(HttpMethod.POST);
또는 아래처럼 짧게 구성할 수 있습니다.
UriSpec<RequestBodySpec> uriSpec = client.post();
3.4 Preparing a Request - Define the URL
HTTP method를 지정했으면 요청 보낼 URL을 지정해야합니다.
RequestBodySpec bodySpec = uriSpec.uri("/resource");
String으로 넘겨줄 수 있고,
RequestBodySpec bodySpec = uriSpec.uri(
uriBuilder -> uriBuilder.pathSegment("/resource").build());
UriBuilder를 통해 넘겨줄 수 있고
RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));
마지막으로 java.net.URL인스턴스를 추가하여 제공할 수 있습니다.
3.5 Preparing a Request - Define the Body
그럼 이제, 요청 본문(request body), 콘텐츠 유형(content type), 길이(length), 쿠키(cookies), 또는 헤더(headers)를 설정할 수도 있습니다.
예를 들어, 요청 본문을 설정하려면 여러 가지 방법이 있습니다.
아마도 가장 일반적이고 직관적인 방법은 bodyValue 메서드를 사용하는 것입니다:
RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");
또는, body 메서드에 Publisher(및 퍼블리싱할 요소의 타입)를 전달하는 방법도 있습니다:
RequestHeadersSpec<?> headersSpec = bodySpec.body(
Mono.just(new Foo("name")), Foo.class);
대안으로, BodyInserters 유틸리티 클래스를 사용할 수도 있습니다.
예를 들어, bodyValue 메서드와 동일하게 단순 객체를 요청 본문으로 설정하려면
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromValue("data"));
마찬가지로, BodyInserters#fromPublisher 메서드를 사용하여 Reactor의 Mono를 전달할 수도 있습니다:
RequestHeadersSpec headersSpec = bodySpec.body(
BodyInserters.fromPublisher(Mono.just("data")),
String.class);
이 BodyInserters 클래스는 다양한 고급 시나리오를 지원하는 직관적인 기능들도 제공합니다.
예를 들어, 멀티파트 요청(multipart request)을 보내야 할 경우
LinkedMultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("key1", "value1");
map.add("key2", "value2");
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromMultipartData(map));
이 모든 메서드는 BodyInserter 인스턴스를 생성하며, 이는 요청의 본문으로 사용될 수 있습니다.
3.6 Preparing a Request - Define the Headers
요청 본문(body)을 설정한 후에는 헤더(headers), 쿠키(cookies), 그리고 허용할 미디어 타입(acceptable media types)을 설정할 수 있습니다.
이 값들은 클라이언트를 인스턴스화할 때 이미 설정된 값들에 추가됩니다.
또한, 자주 사용되는 몇 가지 헤더에 대한 추가 지원이 제공됩니다.
예를 들어:
- “If-None-Match”
- “If-Modified-Since”
- “Accept”
- “Accept-Charset”
ResponseSpec responseSpec = headersSpec.header(
HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // 콘텐츠 타입을 JSON으로 설정
.accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML) // 응답으로 JSON, XML 허용
.acceptCharset(StandardCharsets.UTF_8) // 문자 인코딩을 UTF-8로 설정
.ifNoneMatch("*") // ETag 기반 캐싱 제어 (변경된 경우만 요청)
.ifModifiedSince(ZonedDateTime.now()) // 특정 시간 이후 변경된 경우만 요청
.retrieve(); // 요청 실행
3.7 Getting a Response
마지막 단계는 요청을 보내고 응답을 받는 것입니다.
이를 수행하는 방법은 exchangeToMono / exchangeToFlux 또는 retrieve 메서드를 사용하는 것입니다.
//exchangeToMono/exchangeToFlux
Mono<String> response = headersSpec.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(String.class); // 정상 응답(200 OK)이면 본문(body) 반환
} else if (response.statusCode().is4xxClientError()) {
return Mono.just("Error response"); // 클라이언트 오류(4xx)이면 "Error response" 반환
} else {
return response.createException()
.flatMap(Mono::error); // 기타 오류(5xx 등) 발생 시 예외 처리
}
});
//retrieve
Mono<String> response = headersSpec.retrieve()
.bodyToMono(String.class);
4. Working with the WebTestClient
WebTestClient는 WebFlux 서버 엔드포인트를 테스트하는 주요 진입점입니다.
이는 WebClient와 매우 유사한 API를 가지고 있으며, 대부분의 작업을 내부의 WebClient 인스턴스에 위임하여 주로 테스트 환경(context)을 제공합니다. DefaultWebTestClient 클래스는 이 인터페이스의 유일한 구현체입니다.
테스트용 클라이언트는 실제 서버에 바인딩될 수 있으며, 특정 컨트롤러나 함수와 함께 사용할 수도 있습니다.
4.1 Binding to a Server
실제 서버에 대한 요청을 사용하여 엔드 투 엔드 통합 테스트를 완료하려면, bindToServer 메서드를 사용할 수 있습니다.
WebTestClient testClient = WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build();
4.2 Binding to a Router
Routerfunction을 bindToRouterFunction에 넘겨서 테스트가 가능합니다.
@Test
public void binding_router() {
RouterFunction<ServerResponse> route = RouterFunctions.route(
RequestPredicates.GET("/resource"),
request -> ServerResponse.ok().build());
WebTestClient.bindToRouterFunction(route)
.build().get().uri("/resource")
.exchange()
.expectStatus().isOk()
.expectBody().isEmpty();
}
4.3 Binding to a Web Handler
핸들러를 테스트하기 위해서 제공되는 유틸리티도 있습니다.
WebHandler handler = exchange -> Mono.empty();
WebTestClient.bindToWebHandler(handler).build();
4.4 Binding to an Application Context
Application Context도 테스트할 수 있습니다.
이 메서드는 ApplicationContext를 받아들여 컨트롤러 빈(controller beans)과 @EnableWebFlux 설정을 분석합니다.
//@SpringBootTest 있어야함
@Autowired
private ApplicationContext context;
WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
.build();
4.5 Binding to a Controller
더 간단한 방법은 bindToController 메서드를 사용하여 테스트할 컨트롤러의 배열을 주는것입니다.
컨트롤러 클래스를 가지고 있고, 이를 필요한 클래스에 주입했다고 가정하면 다음과 같이 작성할 수 있습니다.
@Autowired
private Controller controller;
WebTestClient testClient = WebTestClient.bindToController(controller).build();
4.6 Making a Request
WebTestClient 객체를 생성한 후에는, exchange 메서드까지의 모든 연산이 WebClient와 유사하게 동작합니다.
exchange 메서드는 응답을 가져오는 한 가지 방법이며, 이를 통해 WebTestClient.ResponseSpec 인터페이스를 얻을 수 있습니다.
이 인터페이스는 expectStatus, expectBody, expectHeader 같은 유용한 메서드를 제공합니다.
WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build()
.post()
.uri("/resource")
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals("Content-Type", "application/json")
.expectBody().jsonPath("field").isEqualTo("value");
5. 결론
글에서는 클라이언트 측에서 요청을 보내기 위한 새로운 향상된 Spring 메커니즘인 WebClient를 탐색했습니다.
또한, 클라이언트를 구성하고, 요청을 준비하며, 응답을 처리하는 과정을 살펴보면서 WebClient가 제공하는 이점에 대해서도 알아보았습니다.
'Baeldung번역&공부 > Spring-Reactive' 카테고리의 다른 글
WebClient사용할때 파라미터 넣는방법 (0) | 2025.03.05 |
---|---|
Spring WebClient vs RestTemplate (0) | 2025.03.02 |
WebFlux-Retry(Guide to Retry in Spring WebFlux) (0) | 2025.02.28 |
Mono와 Flux의 차이점(Difference Between Flux and Mono) (0) | 2025.02.27 |
SpringMVC Async와 SpringWebFlux차이(Spring MVC Async vs Spring WebFlux) (0) | 2025.02.26 |