WebClient
1. WebClient
- Spring WebFlux는 HTTP 요청을 수행하기 위한 클라이언트를 포함합니다.
- WebClient는 비동기 및 논블로킹 방식으로 HTTP 요청을 수행할 수 있는 클라이언트입니다.
1.1 HTTP client library
- WebClient는 실제 HTTP 요청을 수행하기 위해 하위 수준의 HTTP 클라이언트 라이브러리가 필요합니다.
- WebClient 자체는 리액티브 프로그래밍 모델을 위한 고수준 API를 제공하지만, 실제 네트워크 통신은 이러한 HTTP 클라이언트 라이브러리를 통해 이루어집니다.
- Spring WebFlux의 WebClient가 기본적으로 지원하는 HTTP 클라이언트 라이브러리들은 다음과 같습니다:
- Reactor Netty
- Spring WebFlux의 기본 HTTP 클라이언트 라이브러리입니다.
- Netty 기반으로 비동기 논블로킹 I/O를 제공합니다.
- WebClient.create()를 호출하면 기본적으로 Reactor Netty 클라이언트가 사용됩니다.
- 예: new ReactorClientHttpConnector(HttpClient.create())
- JDK HttpClient
- Java 11부터 도입된 표준 HTTP 클라이언트입니다.
- 비동기 요청을 지원합니다.
- 예: new JdkClientHttpConnector()
예시
// Reactor Netty 사용 (기본값)
WebClient webClient = WebClient.create();
// 또는 명시적으로 설정
HttpClient httpClient = HttpClient.create(); // Reactor Netty 클라이언트
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
// JDK HttpClient 사용
java.net.http.HttpClient jdkHttpClient = java.net.http.HttpClient.newBuilder()
.followRedirects(java.net.http.HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(10))
.build();
WebClient webClient = WebClient.builder()
.clientConnector(new JdkClientHttpConnector(jdkHttpClient))
.build();
- WebClient를 생성할 때 이러한 HTTP 클라이언트 중 하나를 선택하여 설정할 수 있습니다.
- 각 HTTP 클라이언트 라이브러리는 성능 특성, 기능, 설정 옵션이 다르므로 애플리케이션의 요구사항에 맞는 것을 선택할 수 있습니다.
2. Configuration
2.1 WebClient 생성
- WebClient를 생성하는 가장 간 단한 방법은 다음의 정적 팩토리 메소드를 사용하는 것입니다.
WebClient.create()
- 더 많은 옵션을 위해
WebClient.builder()
를 사용할 수도 있습니다.- uriBuilderFactory: 기본 URL로 사용할 맞춤형 UriBuilderFactory
- defaultUriVariables: URI 템플릿 확장 시 사용할 기본값
- defaultHeader: 모든 요청에 대한 헤더
- defaultCookie: 모든 요청에 대한 쿠키
- defaultRequest: 모든 요청을 커스터마이즈하기 위한 Consumer
- filter: 모든 요청에 대한 클라이언트 필터
- exchangeStrategies: HTTP 메시지 리더/라이터 커스터마이징
- clientConnector: HTTP 클라이언트 라이브러리 설정
- observationRegistry: 관찰성(Observability) 지원을 위한 레지스트리
- observationConvention: 기록된 관찰에서 메타데이터를 추출하기 위한 선택적 커스텀 규칙
WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
- WebClient의 builder 메서드로 WebClient를 생성할 수 있습니다.
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1은 filterA, filterB를 가짐
// client2는 filterA, filterB, filterC, filterD를 가짐
- 한 번 빌드된 WebClient는 불변입니다.
- 그러나 복제하여 수정된 복사본을 위와 같이 만들 수 있습니다
2.2 MaxInMemorySize
- 코덱은 애플리케이션 메모리 문제를 방지하기 위해 메모리에 데이터를 버퍼링하는 제한이 있습니다.
- 기본적으로 이 값은 256KB로 설정되어 있습니다.
- 이것이 충분하지 않으면 다음과 같은 오류가 발생합니다
org.springframework.core.io.buffer.DataBufferLimitException
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
- 위와 같이 maxInMemorySize를 설정하여 메모리 제한을 늘릴 수 있습니다.
3. retrieve 메서드
- retrieve() 메서드는 응답을 추출하는 방법을 선언하는 데 사용할 수 있습니다. 이 메서드를 호출한 후, 응답을 어떻게 처리할지 지정할 수 있는 다양한 메서드 체인을 제공합니다.
3.1 전체 ResponseEntity 가져오기
WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
- toEntity() 메서드는 HTTP 상태 코드, 헤더, 그리고 본문을 포함한 전체 ResponseEntity를 반환합니다.
- 이는 응답의 모든 측면에 액세스해야 할 때 유용합니다.
3.2 본문만 가져오기
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
- bodyToMono()는 응답 본문만 추출하여 지정된 타입으로 변환합니다.
- Mono는 단일 결과를 나타내는 리액티브 타입입니다.
- 즉, 하나의 Person 객체를 비동기적으로 받아오게 됩니다.
3.3 스트리밍 응답 처리
Flux<Quote> result = client.get()
.uri("/quotes")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
- bodyToFlux()는 여러 항목의 스트림을 처리할 때 사용합니다.
- Flux는 0-N개의 결과를 비동기적으로 처리할 수 있는 리액티브 타입입니다.
- 이 예제에서는 Server-Sent Events(SSE)를 사용하여 실시간으로 스트리밍되는 Quote 객체들을 처리합니다.
3.4 오류 처리 메커니즘
- 기본적으로 WebClient는 4xx나 5xx와 같은 오류 HTTP 상태 코드를 받으면 WebClientResponseException을 발생시킵니다.
- 이 예외는 HTTP 상태 코드에 따라 다양한 하위 클래스(예: WebClientResponseException.NotFound - 404 오류의 경우)를 가지고 있습니다.
- onStatus 메서드를 이용하면 오류 응답을 사용자 정의 방식으로 처리할 수 있습니다.
Mono<Person> result = client.get()
.uri("/persons/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(
HttpStatusCode::is4xxClientError,
response -> {
// 클라이언트 오류 응답(4xx) 처리 로직
if (response.statusCode().equals(HttpStatus.NOT_FOUND)) {
return Mono.error(new PersonNotFoundException(id));
}
return Mono.error(new ClientErrorException(
"Client error: " + response.statusCode()));
}
)
.onStatus(
HttpStatusCode::is5xxServerError,
response -> {
// 서버 오류 응답(5xx) 처리 로직
return Mono.error(new ServerErrorException(
"Server error: " + response.statusCode()));
}
)
.bodyToMono(Person.class);
- onStatus() 메서드는 두 개의 인자를 받습니다:
- 상태 코드를 검사하는 predicate 함수
- 조건이 일치할 때 실행할 함수(주로 커스텀 예외를 생성하는 로직)
- 이를 통해 오류 응답을 도메인별 예외로 변환하거나, 오류 응답 본문에서 상세 정보를 추출하는 등의 작업을 수행할 수 있습니다.
3.5 retrieve()와 exchange()의 차이점
- WebClient는 retrieve() 외에도 exchange() 메서드를 제공합니다.
- retrieve()는 일반적인 사용 사례에 최적화된 고수준 API로, 응답 상태 코드 처리와 본문 추출을 간소화합니다.
- exchange()는 응답 엔티티에 대한 더 많은 제어가 필요할 때 사용하는 저수준 API이지만, 리소스 해제를 직접 관리해야 하는 책임이 있습니다.
- 대부분의 경우 retrieve()를 사용하는 것이 권장됩니다.
- 이러한 방식으로 WebClient의 retrieve() 메서드는 HTTP 응답을 처리하기 위한 유연하고 강력한 API를 제공하며, 리액티브 프로그래밍 모델에 맞게 비동기적으로 응답을 처리할 수 있게 해줍니다.