
Spring Boot Application에서 외부 API 호출은 거의 모든 서비스에서 필수적으로 등장하는 요소다. 결제, 인증, 알림, 데이터 수집 등 다양한 비즈니스 요구 사항은 외부 시스템과의 통신을 전제로 한다. Spring Framework 6부터 제공되는 RestClient는 이러한 외부 HTTP 통신을 보다 단순하고 일관된 방식으로 처리할 수 있도록 설계된 동기식 HTTP 클라이언트다.
RestClient 개요
RestClient는 Spring Boot 3 이상 환경에서 별도의 의존성 추가 없이 바로 사용할 수 있는 동기식 HTTP 클라이언트다. 기존 RestTemplate의 단점을 보완하면서도 학습 비용을 최소화하는 방향으로 설계되었다. 요청 흐름이 메서드 체이닝 형태로 구성되어 있어 코드를 자연어처럼 읽을 수 있으며, 요청 구성과 실행 흐름이 명확하게 드러난다.
RestClient는 내부적으로 다양한 HTTP 라이브러리를 지원하며, 메시지 컨버터를 통해 JSON, XML 등의 응답을 자바 객체로 자동 변환할 수 있다. 또한 한 번 생성된 RestClient 인스턴스는 쓰레드 세이프(thread-safe)하게 설계되어 있어 싱글톤 빈으로 등록하여 여러 요청에서 재사용해도 안전하다.
기본 인스턴스는 아래와 같이 간단하게 생성할 수 있으며, builder를 활용하면 기본 URL, 헤더, 인터셉터 등을 미리 설정한 클라이언트를 구성할 수 있다.
RestClient defaultClient = RestClient.create();
RestClient customClient = RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer token")
.build();
이처럼 RestClient는 간단한 사용성과 확장 가능한 설정 구조를 동시에 제공하며, 동기식 API 호출이 필요한 대부분의 서버 환경에서 무리 없이 사용할 수 있다.
RestClient 요청 처리
RestClient의 핵심 사용 흐름은 요청 생성 → URI 지정 → 요청 실행 → 응답 변환의 구조를 따른다. HTTP 메서드는 get(), post(), put(), delete() 등으로 명확히 구분되며, retrieve() 메서드를 호출하는 시점에 실제 요청이 전송된다. 이후 body() 메서드를 통해 응답 본문을 원하는 타입으로 변환할 수 있다.
아래는 가장 기본적인 GET 요청 예시다.
String result = restClient.get()
.uri("https://example.com")
.retrieve()
.body(String.class);
URI에는 Path Variable을 사용할 수 있으며, header(), contentType() 등을 통해 요청 헤더와 콘텐츠 타입을 유연하게 지정할 수 있다. 메서드 체이닝 방식 덕분에 요청 구성이 한 눈에 들어온다는 점이 장점이다.
restClient.get()
.uri("https://example.com/orders/{id}", id)
.header("hello", "world")
.retrieve()
.body(String.class);
POST, PUT 요청 시에는 body() 메서드를 통해 요청 본문을 전달할 수 있으며, MediaType을 활용해 JSON 외의 다양한 포맷도 처리 가능하다.
RestClient 에러 핸들링
RestClient는 retrieve() 이후 HTTP 상태 코드 기반의 에러 핸들링을 제공한다. onStatus() 메서드를 사용하면 특정 상태 코드 범위에 대해 사용자 정의 예외를 발생시킬 수 있다. 이를 통해 비즈니스 로직과 에러 처리 로직을 명확히 분리할 수 있다.
String result = restClient.get()
.uri("https://example.com/this-url-does-not-exist")
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new MyCustomRuntimeException(
response.getStatusCode(),
response.getHeaders()
);
})
.body(String.class);
보다 복잡한 시나리오에서는 exchange() 메서드를 활용해 요청과 응답 객체에 직접 접근할 수 있다. 이를 통해 상태 코드, 헤더, 바디를 하나의 흐름에서 유연하게 제어할 수 있다.
Pet result = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.exchange((request, response) -> {
if (response.getStatusCode().is4xxClientError()) {
throw new MyCustomRuntimeException(
response.getStatusCode(),
response.getHeaders()
);
}
return convertResponse(response);
});
DELETE 요청의 경우 HTTP 사양상 본문을 금지하지는 않지만, 일반적으로 권장되지는 않는다. RestClient에서도 delete() 메서드는 body()를 지원하지 않으며, 필요한 경우 method(HttpMethod.DELETE)를 통해 우회적으로 본문을 포함할 수 있다.
RestClient.create()
.method(HttpMethod.DELETE)
.uri(uri)
.contentType(MediaType.APPLICATION_JSON)
.body(requestBody)
.retrieve();
이러한 특성을 이해하고 사용하는 것이 RestClient를 안정적으로 활용하는 핵심이다.