Skip to content

Latest commit

 

History

History
142 lines (109 loc) · 11.2 KB

File metadata and controls

142 lines (109 loc) · 11.2 KB

03. HTTP 클라이언트 — RestClient와 RestTemplate

무엇을 / 왜

Spring 애플리케이션은 다른 HTTP API를 호출하는 쪽이기도 하다. 이때 부딪히는 문제: JDK HttpClient, Apache HttpComponents, Jetty, Reactor Netty 등 클라이언트 라이브러리는 API가 제각각이다. 또 URL 빌드, 헤더 설정, 바디 직렬화(02장), 응답 역직렬화, 에러 처리, 인터셉터를 매번 손으로 엮으면 코드가 중복된다.

spring-web은 이를 두 층으로 푼다. 아래층은 http.clientClientHttpRequestFactory — 라이브러리 차이를 흡수하는 어댑터. 위층은 web.clientRestClient(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)

ClientHttpRequestFactory — 라이브러리 어댑터의 이음매

@FunctionalInterface
public interface ClientHttpRequestFactory {
    ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException;
}

이 한 메서드가 모든 차이를 흡수한다. JdkClientHttpRequestFactory는 JDK HttpClient를, HttpComponentsClientHttpRequestFactory는 Apache를, ReactorClientHttpRequestFactory는 Reactor Netty를 감싼다. 반환된 ClientHttpRequestHttpOutputMessage(01장)이기도 해서, 변환기가 바디를 그대로 써넣을 수 있다. execute()를 호출하면 ClientHttpResponse(이쪽은 HttpInputMessage)가 나온다. 요청은 OutputMessage, 응답은 InputMessage — 서버와 정확히 반대 방향이고, 그래서 같은 변환기가 양쪽에서 동작한다.

ClientHttpRequestInterceptor — 요청/응답 가로채기

@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 — fluent 동기 클라이언트 (권장)

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 — 전통적 템플릿

RestTemplateRestOperations를 구현하는 전통 API다. getForObject, postForEntity, exchange(RequestEntity, ...) 등 메서드별 호출이며, 내부적으로 RequestCallback(요청 준비)·ResponseExtractor(응답 추출) 콜백과 HttpMessageConverterExtractor로 동작한다. 새 코드는 RestClient가 권장되지만 둘은 같은 저수준 인프라(팩토리/인터셉터/변환기)를 공유하므로 RestClient.create(restTemplate)로 설정을 그대로 옮길 수 있다.

에러 처리 — ResponseErrorHandler / StatusHandler

기본적으로 4xx/5xx 응답은 예외로 변환된다. RestTemplateResponseErrorHandler(기본 DefaultResponseErrorHandler)가, RestClientonStatus(predicate, handler)로 등록하는 StatusHandler가 담당한다. 예외 계층은 RestClientExceptionRestClientResponseExceptionHttpStatusCodeExceptionHttpClientErrorException(4xx)/HttpServerErrorException(5xx)로 나뉜다. 응답 상태를 모를 때(UnknownHttpStatusCodeException), 본문 타입을 모를 때(UnknownContentTypeException)도 별도 예외다. 술어는 01장의 HttpStatusCode::is4xxClientError를 그대로 쓴다.

동작 흐름 — RestClient 한 번의 호출

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가 끼어든다.

URI 빌드 — web.util의 조력

요청 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 → 상태 평가 → 변환기로 응답 역직렬화"로 분해된다.