HTTP Client
기존에는 RestTemplate을 사용하여 동기 방식으로 HTTP 요청을 수행했지만, 요청량이 증가할수록 스레드가 차오르고 확장성이 떨어지는 문제가 발생했습니다.
이러한 요구를 해결하기 위해 Spring WebFlux에서는 완전한 논블로킹(Non-Blocking) 기반의 HTTP 클라이언트인 WebClient를 제공합니다. WebClient는 Reactor 기반의 함수형(Functional)·유창한(Fluent) API 스타일을 갖추고 있어, 개발자가 스레드와 동시성 처리에 직접 개입하지 않고도 비동기 로직을 선언적으로 구성할 수 있다는 장점이 있습니다.
또한 서버 측에서 요청과 응답을 인코딩/디코딩할 때 사용하는 것과 동일한 코덱(codec)을 활용하며, 스트리밍까지 지원하기 때문에 고성능 비동기 통신에 매우 적합합니다.
WebClient는 요청을 수행하기 위해 내부적으로 HTTP 클라이언트 라이브러리를 사용하며, Spring WebFlux는 이를 위한 여러 HTTP 엔진을 기본적으로 지원하고 있습니다.
RestTemplate
Spring에서 외부 API와 통신하거나 다른 서버와 데이터를 주고받기 위해 가장 많이 사용되던 도구는 바로 RestTemplate입니다. 이름에서 알 수 있듯이 템플릿 기반으로 HTTP 요청을 수행할 수 있도록 설계된 클래스이며, getForObject(), postForEntity() 같은 직관적인 API 덕분에 학습 난이도가 낮고 사용하기 편리하다는 장점이 있습니다.
과거에는 대부분의 Spring 애플리케이션에서 HTTP 요청을 처리할 때 RestTemplate이 사실상 표준처럼 사용되어 왔습니다. 그러나 오늘날의 애플리케이션은 외부 서비스 호출이 많고, 더 큰 트래픽, 더 높은 동시성, 비동기 처리가 요구되기 때문에 RestTemplate의 구조적 한계가 드러나기 시작했습니다. 이를 이해하기 위해 RestTemplate의 동작 방식과 특징부터 간단히 살펴보겠습니다.
RestTemplate은 다음과 같은 특징을 갖습니다.
- 동기/블로킹 방식 처리
→ 응답을 받을 때까지 해당 스레드가 대기 상태로 유지됩니다. - 템플릿 메서드 제공
→ getForObject, postForEntity, exchange, execute 등 상황별 편의 기능 제공 - 설정 변경에 취약
→ 공유 Bean 형태로 사용하는 경우가 대부분이며, 설정 변경은 애플리케이션 시작 시 고정됩니다.
RestTemplate은 다음과 같이 사용할 수 있습니다.
RestTemplate restTemplate = new RestTemplate();
// GET 요청
User user = restTemplate.getForObject("https://api.example.com/users/1", User.class);
// POST 요청
ResponseEntity<User> response = restTemplate.postForEntity(
"https://api.example.com/users",
new User("Jin", 25),
User.class
);
// 일반 요청
ResponseEntity<String> result = restTemplate.exchange(
"https://api.example.com/data",
HttpMethod.GET,
null,
String.class
);
RestTemplate의 한계
- Blocking I/O 기반
- 비동기 및 스트리밍 미지원
- Spring 공식 신규 기능 추가는 중단
- 환경별 동적 설정 변경 어려움
RestClient
RestTemplate이 오랫동안 Spring에서 HTTP 통신의 표준으로 사용되어 왔지만, 동기 기반 구조로 인해 고부하 환경에서는 확장성이 떨어지는 한계를 가진다는 점을 살펴보았습니다. 이러한 한계를 보완하고, 보다 개발자 친화적이며 현대적인 HTTP 클라이언트를 제공하기 위해 Spring 6.1부터 RestClient가 새롭게 도입되었습니다.
RestClient는 RestTemplate과 동일하게 동기(Blocking) HTTP 요청을 수행하는 클라이언트지만, API 사용성이 훨씬 더 직관적이고 유연하도록 개선된 것이 큰 특징입니다. 또한 기존 RestTemplate 인프라(메시지 컨버터, 인터셉터 등)를 함께 활용할 수 있기 때문에 자연스럽게 대체할 수 있습니다.
RestClient는 다음과 같은 특징이 있습니다.
- 동기(Blocking) HTTP 요청 처리
- create() / builder() 스타일로 인스턴스 구성 유연
- 기존 RestTemplate의 메시지 컨버터, 인터셉터 인프라 공유
- Spring 공식 가이드에서 새로운 동기 클라이언트로 RestClient 사용을 권장
→ 향후 확장이 RestClient 중심으로 이루어질 계획
RestClient는 다음과 같이 사용할 수 있습니다.
// create 기반 설정
RestClient client = RestClient.create("https://api.example.com");
// builder 기반 설정
RestClient client = RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer token")
.build();
// GET 요청
User user = client.get()
.uri("/users/1")
.retrieve()
.body(User.class);
// POST 요청
User created = client.post()
.uri("/users")
.body(new User("Jin", 25))
.retrieve()
.body(User.class);
RestClient의 한계
- Blocking I/O 기반
- 비동기 및 스트리밍 미지원
WebClient
WebClient는 Spring WebFlux가 제공하는 Reactor 기반 비동기·논블로킹 HTTP 클라이언트이며, Stream/SSE/WebSocket 등 고성능 통신에 적합한 현대적 HTTP Client입니다.
WebClient의 내부 구조를 살펴보면 다음과 같습니다.

우리가 코드에서 사용하는 WebClient는 가장 바깥에 있는 인터페이스 역할을 하며, 실제 동작은 내부의 DefaultWebClient 구현체가 담당합니다. 요청을 보내면 ExchangeFunction이 이를 받아 HTTP 호출 흐름을 처리하고, 마지막 단계에서는 ClientHttpConnector가 실제 네트워크 통신을 수행합니다.
ClientHttpConnector는 말 그대로 WebClient와 실제 HTTP 클라이언트 엔진 사이를 연결해주는 역할을 하며, 기본적으로는 Reactor Netty가 사용되지만 Jetty나 Apache HttpComponents로도 손쉽게 교체할 수 있습니다.
WebClient
WebClient의 HTTP Method 메서드는 요청 본문이 필요한 경우 RequestBodyUriSpec,
본문이 없는 경우 RequestHeadersUriSpec을 반환하여 잘못된 API 사용을 컴파일 단계에서 방지합니다.
또한 WebClient는 create()와 builder() 모두 내부적으로 DefaultWebClientBuilder를 사용해 생성되며, 기본적으로 Reactor Netty HttpClient를 기반으로 동작합니다.
public interface WebClient {
RequestHeadersUriSpec<?> get();
RequestHeadersUriSpec<?> head();
RequestBodyUriSpec post();
RequestBodyUriSpec put();
RequestBodyUriSpec patch();
RequestHeadersUriSpec<?> delete();
RequestHeadersUriSpec<?> options();
RequestBodyUriSpec method(HttpMethod method);
Builder mutate();
// Static, factory methods
static WebClient create() {
return new DefaultWebClientBuilder().build();
}
static WebClient create(String baseUrl) {
return new DefaultWebClientBuilder().baseUrl(baseUrl).build();
}
static WebClient.Builder builder() {
return new DefaultWebClientBuilder();
}
}
Builder
WebClient를 직접 create()로 바로 써도 되지만, 일반적으로 Builder를 통해 공통 설정을 미리 구성합니다.
interface Builder {
/**
* Configure a base URL for requests.
*/
Builder baseUrl(String baseUrl);
/**
* Headers for every request.
*/
Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer);
/**
* Headers for every request.
*/
Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
/**
* Consumer to customize every request.
*/
Builder defaultRequest(Consumer<RequestHeadersSpec<?>> defaultRequest);
/**
* Client filter for every request.
*/
Builder filters(Consumer<List<ExchangeFilterFunction>> filtersConsumer);
/**
* HTTP client library settings.
*/
Builder clientConnector(ClientHttpConnector connector);
/**
* CodecConfigurer settings.
*/
Builder codecs(Consumer<ClientCodecConfigurer> configurer);
/**
* exchangeFunction customizations.
*/
Builder exchangeFunction(ExchangeFunction exchangeFunction);
/**
* Build the instance.
*/
WebClient build();
}
WebClient의 builder() 메서드는 기본적으로 DefaultWebClientBuilder를 사용합니다.
아래의 DefaultWebClientBuilder.build() 메서드를 보면 initConnector()를 호출해 ReactorClientHttpConnector를 생성합니다.
final class DefaultWebClientBuilder implements WebClient.Builder {
...
private ClientHttpConnector initConnector() {
if (reactorNettyClientPresent) {
return new ReactorClientHttpConnector();
}
else if (reactorNetty2ClientPresent) {
return new ReactorNetty2ClientHttpConnector();
}
else if (jettyClientPresent) {
return new JettyClientHttpConnector();
}
else if (httpComponentsClientPresent) {
return new HttpComponentsClientHttpConnector();
}
else {
return new JdkClientHttpConnector();
}
}
@Override
public WebClient build() {
ClientHttpConnector connectorToUse =
(this.connector != null ? this.connector : initConnector());
ExchangeFunction exchange = (this.exchangeFunction == null ?
ExchangeFunctions.create(connectorToUse, initExchangeStrategies()) :
this.exchangeFunction);
ExchangeFilterFunction filterFunctions = (this.filters != null ? this.filters.stream()
.reduce(ExchangeFilterFunction::andThen)
.orElse(null) : null);
HttpHeaders defaultHeaders = copyDefaultHeaders();
MultiValueMap<String, String> defaultCookies = copyDefaultCookies();
return new DefaultWebClient(
exchange, filterFunctions,
initUriBuilderFactory(), defaultHeaders, defaultCookies,
this.defaultRequest,
this.statusHandlers,
this.observationRegistry, this.observationConvention,
new DefaultWebClientBuilder(this));
}
}
ReactorClientHttpConnector는 내부적으로 HttpClient.create() 를 호출하여 기본 HttpClient 인스턴스를 생성하고, 이를 기반으로 네트워크 연결을 수행합니다.
public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLifecycle {
public ReactorClientHttpConnector() {
this.httpClient = defaultInitializer.apply(HttpClient.create());
this.resourceFactory = null;
this.mapper = null;
}
...
}
따라서 WebClient에서 네트워크 연결과 관련된 세부 설정을 적용하려면, HttpClient를 직접 생성해 ReactorClientHttpConnector로 감싸고 이를 WebClient 빌더에 전달하면 됩니다.
UriSpec
HTTP 메서드를 선택(get/post/put/delete 등)하고 요청 URI를 지정하는 단계를 담당하는 인터페이스입니다.
interface UriSpec<S extends RequestHeadersSpec<?>> {
S uri(URI uri);
S uri(String uri, Object... uriVariables);
S uri(String uri, Map<String, ?> uriVariables);
S uri(String uri, Function<UriBuilder, URI> uriFunction);
S uri(Function<UriBuilder, URI> uriFunction);
}
RequestHeadersUriSpec & RequestHeadersSpec
URI가 정해진 뒤, 헤더·쿠키·쿼리 파라미터 등 요청 메타데이터를 설정하는 단계를 담당합니다.
interface RequestHeadersUriSpec<S extends RequestHeadersSpec<S>>
extends UriSpec<S>, RequestHeadersSpec<S> {}
interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
S accept(MediaType... acceptableMediaTypes);
S acceptCharset(Charset... acceptableCharsets);
S cookie(String name, String value);
S cookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
S header(String headerName, String... headerValues);
S headers(Consumer<HttpHeaders> headersConsumer);
S attribute(String name, Object value);
S attributes(Consumer<Map<String, Object>> attributesConsumer);
S httpRequest(Consumer<ClientHttpRequest> requestConsumer);
ResponseSpec retrieve();
<V> Mono<V> exchangeToMono(Function<ClientResponse, ? extends Mono<V>> responseHandler);
<V> Flux<V> exchangeToFlux(Function<ClientResponse, ? extends Flux<V>> responseHandler);
}
RequestBodyUriSpec & RequestBodySpec
POST, PUT 등 Body가 필요한 요청에서 요청 본문을 설정할 수 있는 단계를 담당합니다.
interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec<RequestBodySpec> {}
interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
RequestBodySpec contentLength(long contentLength);
RequestBodySpec contentType(MediaType contentType);
RequestHeadersSpec<?> bodyValue(Object body);
<T, P extends Publisher<T>> RequestHeadersSpec<?> body(P publisher,
Class<T> elementClass);
<T, P extends Publisher<T>> RequestHeadersSpec<?> body(P publisher,
ParameterizedTypeReference<T> elementTypeRef);
RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter);
}
ResponseSpec
요청이 전송된 후 응답 상태·에러 처리·body 디코딩을 담당하는 단계로, retrieve() 이후 반환되는 인터페이스입니다.
interface ResponseSpec {
ResponseSpec onStatus(Predicate<HttpStatusCode> statusPredicate,
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction);
<T> Mono<T> bodyToMono(Class<T> elementClass);
<T> Flux<T> bodyToFlux(Class<T> elementClass);
<T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyClass);
<T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> elementClass);
<T> Mono<ResponseEntity<Flux<T>>> toEntityFlux(Class<T> elementType);
Mono<ResponseEntity<Void>> toBodilessEntity();
}
WebClient는 다음과 같이 사용할 수 있습니다.
HttpClient httpClient = HttpClient.create()
.compress(true)
.responseTimeout(Duration.ofSeconds(3));
WebClient.Builder builder = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient));
WebClient client = builder
.baseUrl("http://localhost:8080")
.build();
Mono<Person> result = client.get()
.uri("/persons/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, response -> ...)
.onStatus(HttpStatusCode::is5xxServerError, response -> ...)
.bodyToMono(Person.class);'Spring > Webflux' 카테고리의 다른 글
| DispatcherHandler (1) | 2025.12.10 |
|---|---|
| Spring WebFlux (1) | 2025.12.09 |
| Reactor operators (0) | 2025.12.08 |
| Netty - ByteBuf (2) | 2025.12.03 |
| Netty Server (0) | 2025.12.03 |