Spring 애플리케이션은 다른 HTTP API를 호출하는 쪽이기도 하다. 이때 부딪히는 문제: JDK HttpClient, Apache HttpComponents, Jetty, Reactor Netty 등 클라이언트 라이브러리는 API가 제각각이다. 또 URL 빌드, 헤더 설정, 바디 직렬화(02장), 응답 역직렬화, 에러 처리, 인터셉터를 매번 손으로 엮으면 코드가 중복된다.
spring-web은 이를 두 층으로 푼다. 아래층은 http.client의 ClientHttpRequestFactory — 라이브러리 차이를 흡수하는 어댑터. 위층은 web.client의 RestClient(fluent, 6.1+ 권장)와 RestTemplate(전통)이며, 02장 변환기와 URI 빌더(web.util)를 엮어 고수준 API를 제공한다. 이 장은 이 두 층의 구조와 한 번의 호출이 어떻게 흘러가는지 본다.
┌──────────────────────────────────────────────────────────┐
│ 고수준 API (web.client) │
│ RestClient (fluent, 동기, 6.1+) │
│ RestTemplate (template-method, 전통) │
│ ResponseErrorHandler / StatusHandler │
└───────────────────────────┬──────────────────────────────┘
│ 사용
▼
┌──────────────────────────────────────────────────────────┐
│ 저수준 추상 (http.client) │
│ │
│ ClientHttpRequestFactory ──createRequest(uri,method)──► │
│ ClientHttpRequest ──execute()──► ClientHttpResponse │
│ │
│ ClientHttpRequestInterceptor (체인) ─ InterceptingFactory │
│ BufferingClientHttpRequestFactory (응답 버퍼링 래퍼) │
└───────────────────────────┬──────────────────────────────┘
│ 구현 (라이브러리별 어댑터)
┌────────────┬────────┴────────┬─────────────┐
▼ ▼ ▼ ▼
JdkClientHttp HttpComponents JettyClient ReactorClient
RequestFactory ...Factory ...Factory ...Factory
(JDK HttpClient)(Apache) (Jetty) (Reactor Netty)
+Simple(JDK URLConnection)
@FunctionalInterface
public interface ClientHttpRequestFactory {
ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException;
}이 한 메서드가 모든 차이를 흡수한다. JdkClientHttpRequestFactory는 JDK HttpClient를, HttpComponentsClientHttpRequestFactory는 Apache를, ReactorClientHttpRequestFactory는 Reactor Netty를 감싼다. 반환된 ClientHttpRequest는 HttpOutputMessage(01장)이기도 해서, 변환기가 바디를 그대로 써넣을 수 있다. execute()를 호출하면 ClientHttpResponse(이쪽은 HttpInputMessage)가 나온다. 요청은 OutputMessage, 응답은 InputMessage — 서버와 정확히 반대 방향이고, 그래서 같은 변환기가 양쪽에서 동작한다.
@FunctionalInterface
public interface ClientHttpRequestInterceptor {
ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException;
}인터셉터는 요청을 검사·수정하고 execution.execute(request, body)로 다음 단계에 위임한다(또는 위임을 막아 호출 자체를 차단). 7.0에서 andThen/apply 기본 메서드가 추가돼 인터셉터를 함수형으로 합성할 수 있다. 실제 체인은 InterceptingClientHttpRequestFactory가 만든다 — 인터셉터들을 ClientHttpRequestExecution 체인으로 엮어, 마지막 단계가 실제 팩토리를 호출한다. 인증 토큰 주입, 로깅, 재시도 등이 여기 들어간다.
부가 래퍼도 있다. BufferingClientHttpRequestFactory는 응답 바디를 메모리에 버퍼링해 여러 번 읽을 수 있게 한다(로깅 인터셉터가 바디를 소비해도 본 처리에 영향 없음). ClientHttpRequestInitializer는 모든 요청에 공통 헤더를 적용하는 초기화 훅이다.
RestClient(6.1+, 소스: web/client/RestClient.java)는 메서드 체인으로 요청을 조립한다.
RestClient client = RestClient.builder()
.baseUrl("https://api.example.com")
.requestInterceptor(authInterceptor)
.build();
User user = client.get()
.uri("/users/{id}", 42)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(User.class);진입점은 get()/post()/put() … 이고, 각각 RequestHeadersUriSpec/RequestBodyUriSpec을 반환한다. URI 지정 → 헤더/바디 → retrieve()(고수준) 또는 exchange()(저수준, 응답을 직접 다룸)로 끝난다. 내부 구현은 DefaultRestClient이며, 결국 위에서 본 ClientHttpRequestFactory를 사용한다. 변환기 목록은 HttpMessageConverters(02장)로 주입된다.
RestTemplate은 RestOperations를 구현하는 전통 API다. getForObject, postForEntity, exchange(RequestEntity, ...) 등 메서드별 호출이며, 내부적으로 RequestCallback(요청 준비)·ResponseExtractor(응답 추출) 콜백과 HttpMessageConverterExtractor로 동작한다. 새 코드는 RestClient가 권장되지만 둘은 같은 저수준 인프라(팩토리/인터셉터/변환기)를 공유하므로 RestClient.create(restTemplate)로 설정을 그대로 옮길 수 있다.
기본적으로 4xx/5xx 응답은 예외로 변환된다. RestTemplate은 ResponseErrorHandler(기본 DefaultResponseErrorHandler)가, RestClient는 onStatus(predicate, handler)로 등록하는 StatusHandler가 담당한다. 예외 계층은 RestClientException ← RestClientResponseException ← HttpStatusCodeException ← HttpClientErrorException(4xx)/HttpServerErrorException(5xx)로 나뉜다. 응답 상태를 모를 때(UnknownHttpStatusCodeException), 본문 타입을 모를 때(UnknownContentTypeException)도 별도 예외다. 술어는 01장의 HttpStatusCode::is4xxClientError를 그대로 쓴다.
DefaultRestClient.exchangeInternal(소스 라인 582~)을 따라가 보면 한 호출이 이렇게 흐른다.
client.get().uri("/users/{id}", 42).retrieve().body(User.class)
│
▼ exchangeInternal()
1. initUri() : baseUrl + "/users/{id}" → URI 확장(web.util UriBuilderFactory)
2. initHeaders() : Accept/쿠키/기본 헤더 구성
3. createRequest(uri): ClientHttpRequestFactory.createRequest(uri, GET)
│
▼
4. body.writeTo(req): (POST 등) 변환기가 객체 → 요청 바디 직렬화
※ writeWithMessageConverters: canWrite 변환기 선택 후 write (02장)
│
▼ Observation(micrometer) 스코프 안에서
5. clientRequest.execute() → ClientHttpResponse
│
▼ ConvertibleClientHttpResponse 래핑
6. exchangeFunction.exchange(req, response)
├ 상태 핸들러(onStatus) 평가 → 4xx/5xx면 예외
└ readWithMessageConverters: canRead 변환기로 응답 바디 → User 역직렬화
│
▼ finally
7. 스트리밍 결과가 아니면 response.close() (관측 stop)
핵심은 고수준 호출이 결국 저수준 팩토리 + 변환기 목록으로 분해된다는 점이다. URL 확장은 web.util(UriBuilderFactory), 바디/응답 변환은 02장, 실제 전송은 라이브러리 어댑터가 맡는다. 인터셉터가 등록돼 있으면 3~5단계 사이에 InterceptingClientHttpRequest가 끼어든다.
요청 URL은 web.util.UriComponentsBuilder/DefaultUriBuilderFactory가 만든다. "/users/{id}" 같은 템플릿에 변수를 안전하게 채우고(인코딩 모드 선택), 쿼리 파라미터를 붙인다. UriComponents는 파싱·인코딩·확장이 분리된 불변 표현이다. 클라이언트뿐 아니라 서버 측 리다이렉트 URL 생성에도 쓰인다.
ClientHttpRequestFactory.createRequest(uri, method)— 라이브러리 차이를 흡수하는 단일 진입점. 어떤 HTTP 라이브러리를 쓸지는 이 팩토리 교체로 결정된다.ClientHttpRequest.execute()— 요청을 전송하고ClientHttpResponse를 얻는 동기 경계.RestClient ... retrieve().body(Class)— 고수준 happy-path. 내부에서exchangeInternal이 위 7단계를 수행.RestClient ... exchange((req, resp) -> ...)— 응답을 직접 다루는 저수준 탈출구. 스트리밍/조건부 처리에 사용.onStatus(predicate, handler)/ResponseErrorHandler.handleError— 상태 기반 예외 변환 지점.
- 2계층 분리: 라이브러리 어댑터(
ClientHttpRequestFactory)와 고수준 API(RestClient)를 분리해, 전송 구현 교체가 호출 코드에 영향을 주지 않는다. - 변환기 재사용: 요청=
HttpOutputMessage, 응답=HttpInputMessage로 모델링해 02장 변환기를 서버와 그대로 공유한다. - 인터셉터 체인:
ClientHttpRequestInterceptor가 횡단 관심사(인증·로깅·재시도)를 합성 가능한 단위로 분리. 7.0의andThen으로 함수형 조립. - 관측 내장: 호출이 micrometer
Observation스코프로 감싸여 메트릭/트레이싱이 자동 연동된다. - 확장점: 커스텀
ClientHttpRequestFactory(새 라이브러리), 인터셉터,HttpMessageConverters(변환),StatusHandler(에러),UriBuilderFactory(URL 정책)를 각각 갈아끼울 수 있다.
Spring HTTP 클라이언트는 두 층으로 설계된다. 아래층 ClientHttpRequestFactory는 JDK/Apache/Jetty/Reactor 등 라이브러리 차이를 createRequest → execute 한 줄로 흡수하고, 요청·응답을 HttpOutputMessage/HttpInputMessage로 표현해 02장 변환기를 재사용한다. 위층 RestClient(fluent, 권장)와 RestTemplate(전통)은 URI 빌드·변환·에러 처리·인터셉터·관측을 엮어 고수준 API를 제공한다. 한 번의 retrieve().body(...) 호출은 결국 "URI 확장 → 팩토리로 요청 생성 → 변환기로 바디 직렬화 → execute → 상태 평가 → 변환기로 응답 역직렬화"로 분해된다.