스프링 WebFlux의 WebClient는 비동기적이고 논블로킹 방식으로 HTTP 요청을 보내기 위한 리액티브 클라이언트이다.
기존 스프링 MVC에서 주로 사용되던 RestTemplate의 리액티브 버전이라고 할 수 있다.
WebClient는 논블로킹 I/O를 기반으로 하여 대규모 동시 요청 처리 및 스트리밍 데이터 처리에 강점을 가진다. Reactor(Mono, Flux)를 사용하여 비동기 결과를 처리하며 백프레셔를 지원한다.
| 특징 | RestTemplate | WebClient |
|---|---|---|
| 스레드 모델 | 블로킹 | 논블로킹 |
| I/O 모델 | 요청당 스레드 모델. 동시 요청 증가 시 스레드 오버헤드 증가 | 이벤트 루프 기반. 적은 스레드로도 높은 동시성 처리 가능 |
| 프로그래밍 | 명령형(Imperative) | 리액티브(Reactive) - Mono, Flux 반환 |
| 백프레셔 | 지원 안 함 | 지원함 - 생산자/소비자 속도 조절 가능 |
| 사용처 | 기존 동기식 애플리케이션, 비교적 낮은 동시성 요구 | 리액티브 스택 애플리케이션, 높은 동시성, 스트리밍 데이터, 마이크로서비스 간 통신 |
| 에러 처리 | try-catch및 예외 발생 | 리액티브 스트림 내에서 onErrorResume, onErrorMap등 활용 |
Mono<T>와 Flux<T>를 사용하여 비동기적인 단일 값 또는 다중 값 스트림을 처리한다.onErrorResume, onErrorMap 등)를 통해 섬세한 오류 처리가 가능하다.스프링 WebClient는 겉으로는 간단한 빌더 패턴과 리액티브 스트림 API를 제공하지만 내부에는 복잡한 논블로킹 HTTP 통신 메커니즘이 작동한다.
핵심적인 내부 동작 원리는 다음과 같은 요소들의 상호작용으로 이루어진다.
WebClient의 가장 중요한 내부 구성 요소 중 하나는 클라이언트 커넥터(ClientHttpConnector)이다.
WebClient 자체는 고수준의 API를 제공하며 실제 HTTP 통신(소켓 연결, 데이터 전송/수신)은 이 커넥터에게 위임한다.
ClientHttpConnector는 WebClient가 특정 HTTP 클라이언트 구현체에 종속되지 않고 유연하게 작동할 수 있도록 하는 추상화 계층이다.
public interface ClientHttpConnector {
Mono<ClientHttpResponse> connect(
HttpMethod method,
URI uri,
Function<? super ClientHttpRequest, Mono<Void>> requestCallback
);
}
ReactorClientHttpConnector): 스프링 WebFlux의 기본 커넥터이자 가장 일반적으로 사용되는 구현체이다. Netty는 이벤트 루프 기반의 고성능 논블로킹UndertowClientHttpConnector): 또 다른 논블로킹 웹 서버인 Undertow를 기반으로 한다.JdkClientHttpConnector): Java 11에 도입된 표준 HTTP 클라이언트이다.ApacheHttpClient5ClientHttpConnector): Apache HttpClient 5를 기반으로 하며 블로킹이지만 비동기 래터를 제공한다. 일반적으로WebClient에게 전달하는 저수준 작업을 담당한다.WebClient가 논블로킹 특성을 갖는 핵심적인 이유이다. 특히 Reactor Netty 커넥터의 경우 Netty의 이벤트 루프 모델을 그대로 활용한다.
ServerSocket을 직접 사용하는 경우)은 각 클라이언트 연결마다 별도의 스레드를 할당하여 I/O 작업이 완료될 때까지 해당 스레드를 블로킹한다.Mono/Flux의 구독자)으로 전달된다.WebClient는 스프링 WebFlux의 핵심인 Reactor 라이브러리의 Mono와 Flux를 사용하여 비동기 결과를 표현하고 처리한다.
WebClient의 모든 요청 메소드(retrieve(), exchangeToMono(), bodyToMono() 등)는 Mono또는 Flux를 반환한다.Mono<T>: 단일 응답 객체를 비동기적으로 받을 때 사용한다.Flux<T>: 여러 응답 객체를 비동기적으로 받을 때 사용한다.Mono나 Flux가 제공하는 연산자(map, filter, flatMap 등)을 사용하여 비동기 결과 스트림을 조합, 변환, 필터링, 에러 처리를 할 수 있다.Mono나 Flux는 콜 스트림이다. 즉, subscribe()메소드가 호출되기 전까지는 실제 HTTP 요청이 시작되지 않는다.subscribe()가 호출되면 비동기 요청이 트리거되고, 결과가 준비될 때 구독자에게 전달된다.Mono/Flux를 반환하면 프레임워크가 알아서 구독을 처리한다.WebClient는 HTTP 요청-응답 처리 파이프라인을 ExchangeFunction과 ExchangeFilterFunction으로 추상화한다.
@FunctionalInterface
public interface ExchangeFunction {
Mono<ClientResponse> exchange(ClientRequest request);
default ExchangeFunction filter(ExchangeFilterFunction filter) {
return filter.apply(this);
}
}
Mono<ClientResponse>를 반환하는 핵심 논리를 캡슐화한다.ClientHttpConnector를 사용하여 네트워크 통신을 수행한다.WebClient의 요청 파이프라인의 가장 마지막에 위치하며, 모든 필터가 적용된 후 최종적으로 호출된다.
@FunctionalInterface
public interface ExchangeFilterFunction {
Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next);
default ExchangeFilterFunction andThen(ExchangeFilterFunction afterFilter) {
Assert.notNull(afterFilter, "ExchangeFilterFunction must not be null");
return (request, next) -> {
return this.filter(request, (afterRequest) -> {
return afterFilter.filter(afterRequest, next);
});
};
}
default ExchangeFunction apply(ExchangeFunction exchange) {
Assert.notNull(exchange, "ExchangeFunction must not be null");
return (request) -> {
return this.filter(request, exchange);
};
}
static ExchangeFilterFunction ofRequestProcessor(Function<ClientRequest, Mono<ClientRequest>> processor) {
Assert.notNull(processor, "ClientRequest Function must not be null");
return (request, next) -> {
Mono var10000 = (Mono) processor.apply(request);
Objects.requireNonNull(next);
return var10000.flatMap(next::exchange);
};
}
static ExchangeFilterFunction ofResponseProcessor(Function<ClientResponse, Mono<ClientResponse>> processor) {
Assert.notNull(processor, "ClientResponse Function must not be null");
return (request, next) -> {
return next.exchange(request).flatMap(processor);
};
}
}
Authorization헤더 추가WebClient.builder()/create(): WebClient인스턴스 생성. 이때 사용할 ClientHttpConnector와 ExchangeFilterFunction체인이 구성된다.webClient.method(...).uri(...).header(...).body(...)개발자가 HTTP 요청을 빌드한다. 이 과정에서 요청 정보(메소드, URI, 헤더, 바디 등)가 내부적으로ClientRequest객체로 준비된다.retrieve()또는 exchangeToMono()호출: 요청 실행을 트리거한다.ExchangeFilterFunction체인 실행: ClientRequest객체가 필터 체인을 통과하면서 각 필터가 요청을 수정하거나 로직을 수행한다.ExchangeFunction호출: 필터 체인을 통과한 ClientRequest는 최종적으로 ExchangeFunction에 전달된다.ClientHttpConnector호출: ExchangeFunction은 구성된 ClientHttpConnector를 톨해 실제 네트워크 통신을 요청한다.ClientResponse객체로 변환하고 Mono<ClientResponse>형태로 ExchangeFunction에 반환하나.Mono/Flux발행: ClientResponse는 다시 필터 체인을 역방향으로 통과하며 필요에 따라 수정될 수 있다. 최종적으로 retrieve()나 exchangeToMono()호출Mono/Flux형태로 응답 데이터가 발행된다.subscribe()를 호출한 경우 또는 WebFlux 컨트롤러가 반환한 경우 Mono/Flux의 최종 결과가 구독자에게 전달된다.WebClient는 기본적으로 모든 I/O 작업을 논블로킹 방식으로 처리한다.Mono또는 Flux를 반환하여 결과가 비동기적으로 도달할 것임을 명시한다.WebClient인스턴스 자체와 이를 통해 구성되는 요청 객체(ClientRequest)는 불변(immutable)하다.WebClient.builder()를 통해 인스턴스를 생성하거나, .get(), .uri(), .header()등의 메소드를 호출할 때마다 내부적으로 새로운 중간 객체 또는 최종 객체가 생성되어WebClient인스턴스를 공유하거나 동시에 요청을 빌드하더라도 서로의 상태에 영향을 주지 않는다.WebClient.builder()를 통해 baseUrl, 기본 헤더/쿠키, 필터 등을 설정하여 다양한 시나리오에 맞는 WebClient인스턴스를 유연하게 생성할 수 있다.WebClient는 리액티브 스트림즈 표준을 준수하여 백프레셔를 지원한다.위에서 설명한 디자인 원칙들을 바탕으로 WebClient를 가장 효과적으로 사용하기 위한 몇 가지 권장 패턴이 있다.
WebClient인스턴스를 생성하는 것은 내부적으로 커넥션 풀 초기화, 필터 체인 구성 등 비용이 드는 작업이다.@Configuration클래스에서 @Bean으로 WebClient인스턴스를 정의한다.WebClient.Builder빈을 활용하면 더욱 편리하다.
@Configuration
public class WebClientConfig {
// WebClient.Builder는 Spring Boot가 자동으로 제공한다
@Bean
public WebClient userApiWebClient(WebClient.Builder builder) {
return builder
.baseUrl("http://localhost:8080/api/users")
.defaultHeader("Accept", "application/json")
// .filter(...) 추가 필터 설정 가능
.build();
}
@Bean
public WebClient productApiWebClient(WebClient.Builder builder) {
return builder
.baseUrl("http://localhost:8080/api/products")
// 각 API별로 다른 설정 가능
.build();
}
}
// 아래처럼 주입받아 사용하면 된다
@Service
public class WebClientUserService {
private final WebClient userApiWebClient;
public WebClientUserService(@Qualifier("userApiWebClient") WebClient userApiWebClient) {
this.userApiWebClient = userApiWebClient;
}
public Mono<SecurityProperties.User> getUserDetails(String id) {
return userApiWebClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(SecurityProperties.User.class);
}
}
WebClient가 가지므로 exchange()의 메모리 누수 위험이 없다.WebClient.Builder에 추가하는 것이
@Configuration
public class WebClientConfig {
// 인증 토큰을 모든 요청에 추가하는 필터
@Bean
public WebClient authenticatedWebClient(WebClient.Builder builder) {
return builder
.baseUrl("http://localhost:8080")
.filter((request, next) -> {
// 여기서는 간단히 하드코딩된 토큰 사용. 실제로는 보안 컨텍스트 등에서 가져옴
ClientRequest filteredRequest = ClientRequest.from(request)
.header("Authorization", "Bearer my_secret_token")
.build();
return next.exchange(filteredRequest);
})
.build();
}
}
Mono.block()이나 Flux.blockLast()같은 블로킹 연산자는 리액티브 스트림의 비동기 장점을 완전히 상쇄시키며, WebFlux 애플리케이션 내에서 사용될 경우 데드락, 스레드 고갈 등의flatMap, zip, combineLatest등 리액티브 연산자를 사용하여 비동기 결과를 조합하고 변환해야 한다.Mono나 Flux로 유지하여 프레임워크가 구독을 처리하도록 한다.try-catch블록과는 다른 방식의 오류 처리 메커니즘을 제공한다.onErrorResume, onErrorMap, retry, doOnError등의 연산자를 활용하여 스트림 내에서 발생하는 오류를 복구, 변환, 재시도, 로깅할 수 있다.