서버 개발을 하다 보면 외부 API를 호출하는 경우가 늘 있습니다. 그런데 Spring 생태계에는 통신을 위한 도구들이 너무나도 많아, 도대체 뭘 써야 할지, 코드는 어떻게 짜야 할지 고민되는 경우가 많습니다.
오늘은 자바 백엔드 개발의 역사와 함께해 온 5가지 핵심 HTTP 클라이언트의 특징을 짚어보도록 하겠습니다.
1. 아키텍처 기반: I/O 모델과 스레딩의 이해
우선 HTTP 클라이언트에 대해 설명하기 전에 다음 글을 보고 I/O 모델 & 멀티 스레딩에 대해 이해하시면 글을 읽는데 도움이 될 것 같습니다.
2025.12.15 - [IT/SpringBoot] - [Java 21] 서버 성능을 10배 높이는 비밀: 블로킹 vs 논블로킹, 그리고 가상 스레드의 혁명
2. RestTemplate: 구관이 명관? 이제는 놓아주자
오랫동안 자바 개발자들의 친구였던 RestTemplate입니다. 가장 익숙하고 직관적이지만, 요청마다 스레드를 하나씩 차지하는 '블로킹(Blocking)' 방식이라 트래픽이 몰리면 서버가 힘들어할 수 있어요. 현재는 유지보수 모드로, 새로운 프로젝트에서는 권장하지 않습니다.
2-1) 내부 아키텍처 및 확장성
RestTemplate 자체는 HTTP 통신을 직접 수행하지 않습니다. 대신 ClientHttpRequestFactory 인터페이스를 통해 실제 통신을 담당하는 라이브러리에 작업을 위임하는 어댑터 역할을 수행합니다.
표 1. 주요 ClientHttpRequestFactory 구현체 비교
| 구현체 클래스 | 기반 라이브러리 | 특징 및 아키텍처적 고려사항 |
| SimpleClientHttpRequestFactory | JDK HttpURLConnection | 기본값. 별도 의존성 불필요. 커넥션 풀링(Connection Pooling) 미지원으로 성능 저하 발생 가능. |
| HttpComponentsClientHttpRequestFactory | Apache HttpClient | 가장 널리 사용됨. 커넥션 풀링, 타임아웃 정밀 제어, SSL 컨텍스트 설정 등 엔터프라이즈급 기능 제공. |
| OkHttp3ClientHttpRequestFactory | OkHttp | 안드로이드 및 모바일 친화적. 성능 우수. |
| ReactorNettyClientRequestFactory | Reactor Netty | Spring Boot 3.4부터 지원 강화. 비동기 기반이지만 동기 어댑터로 동작. |
| JdkClientHttpRequestFactory | Java java.net.http.HttpClient | Java 11+ 클라이언트 사용. Spring Boot 3.4부터 자동 설정 우선순위 상향. |
2-2) 사용 패턴 및 한계점
RestTemplate은 오버로딩된 수많은 메서드(getForObject, postForEntity, exchange 등)를 제공합니다. 이는 초기에는 편리했으나, API가 비대해지고 유연성이 떨어진다는 비판을 받았습니다.
2-3) 유지보수 모드와 미래
Spring 5.0부터 RestTemplate은 '유지보수 모드'로 지정되었습니다. 이는 버그 수정 외에 새로운 기능 추가는 없다는 것을 의미합니다. 문서상으로는 Spring 6.1부터 RestClient 사용을 권장하고 있으며, Spring Framework 7.0(2025년 말 예정)에서 공식적으로 Deprecated 처리될 예정입니다. 그러나 오픈소스 지원은 최소 2029년까지 지속될 것으로 보이며, 이는 기존 시스템을 급하게 마이그레이션할 필요는 없으나, 신규 개발에는 사용을 지양해야 함을 시사합니다
사용 예시 (JAVA Code)
@Service
public class UserService {
private final RestTemplate restTemplate;
public UserService(RestTemplateBuilder builder) {
// 타임아웃 설정은 필수!
this.restTemplate = builder
.setConnectTimeout(Duration.ofSeconds(3))
.setReadTimeout(Duration.ofSeconds(3))
.build();
}
public UserDto getUserInfo(String userId) {
String url = "https://api.example.com/users/" + userId;
// 가장 기본적인 GET 요청
// 응답을 객체로 바로 매핑합니다.
return restTemplate.getForObject(url, UserDto.class);
}
public void createUser(UserDto userDto) {
String url = "https://api.example.com/users";
// POST 요청 및 헤더 설정이 필요할 때
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<UserDto> request = new HttpEntity<>(userDto, headers);
restTemplate.postForObject(url, request, UserDto.class);
}
}
3. WebClient: 비동기의 강력함, 하지만 높은 난이도
Spring 5와 함께 등장한 WebClient는 적은 스레드로 엄청난 트래픽을 처리할 수 있는 '논블로킹(Non-blocking)' 방식의 강자입니다. 성능은 최고지만, 리액티브 프로그래밍(Mono, Flux)을 알아야 해서 코드를 짜기가 까다롭고 디버깅이 어렵다는 단점이 있습니다.
3-1) 이벤트 루프 아키텍처 심층 분석
WebClient의 핵심은 Netty와 같은 비동기 런타임입니다. 요청이 발생하면 작업을 이벤트 큐에 넣고 즉시 리턴합니다. 실제 네트워크 I/O가 완료되면, 이벤트 루프 스레드가 이를 감지하여 등록된 연산자 체인(Operator Chain)을 실행합니다.
- 스레드 모델: 적은 수의 스레드로도 높은 처리량을 냅니다. 하지만 CPU 연산이 많은 작업(CPU-bound task)을 이벤트 루프 스레드에서 수행할 경우, 전체 시스템의 반응성이 급격히 저하될 수 있습니다. 따라서 암호화나 복잡한 계산은 별도의 스레드 풀(Schedulers)로 격리해야 합니다.
- 메모리 효율성: 스택 메모리를 적게 사용하므로, 대규모 동시 접속 환경(예: 10,000 TPS 이상의 API 게이트웨이)에서 메모리 사용량이 예측 가능하고 안정적입니다.
3-2)1 블로킹 환경에서의 오용과 위험성
많은 개발자들이 Spring MVC(서블릿) 애플리케이션에서 WebClient를 사용하면서 .block() 메서드를 호출하여 결과를 동기적으로 받아옵니다. 이는 WebClient의 장점을 모두 버리는 행위일 뿐만 아니라, 특정 상황에서는 데드락(Deadlock)을 유발할 수 있습니다. 또한 spring-boot-starter-webflux 의존성을 추가함으로써 불필요한 Netty 라이브러리와 리액티브 관련 클래스들이 클래스패스에 로드되어 애플리케이션의 시작 시간과 메모리 사용량을 증가시킵니다.
핵심 인사이트: 전체 스택이 리액티브하지 않다면(즉, DB 드라이버부터 컨트롤러까지 모두 비동기가 아니라면), 단순히 HTTP 클라이언트만 WebClient를 사용하는 것은 아키텍처적으로 권장되지 않습니다.
사용 예시 (JAVA Code)
@Service
public class ProductService {
private final WebClient webClient;
public ProductService(WebClient.Builder builder) {
this.webClient = builder.baseUrl("https://api.store.com").build();
}
// 비동기 결과(Mono)를 반환합니다.
public Mono<ProductDto> getProduct(String productId) {
return webClient.get()
.uri("/products/{id}", productId)
.retrieve()
// 에러 처리도 스트림 흐름 안에서!
.onStatus(HttpStatusCode::is4xxClientError, response ->
Mono.error(new RuntimeException("상품이 없습니다.")))
.bodyToMono(ProductDto.class);
}
// 여러 API를 동시에 호출하고 합칠 때 강력합니다. (Zip)
public Mono<FullInfo> getFullInfo(String id) {
Mono<ProductDto> product = getProduct(id);
Mono<ReviewDto> review = webClient.get().uri("/reviews/" + id).retrieve().bodyToMono(ReviewDto.class);
return Mono.zip(product, review, FullInfo::new);
}
}
4. OpenFeign: 인터페이스만 만들면 끝!
Spring Cloud 프로젝트에서 가장 사랑받는 도구입니다. 구현 코드를 짤 필요 없이 인터페이스에 어노테이션만 붙이면 런타임에 알아서 구현체를 만들어줍니다. 코드가 정말 깔끔해지지만, 기본적으로 블로킹 방식이고 Spring Cloud 의존성이 무겁다는 점을 고려해야 합니다.
4-1) 동적 프록시(Dynamic Proxy) 메커니즘
Feign은 자바의 리플렉션(Reflection)과 동적 프록시 기술을 사용하여 런타임에 인터페이스의 구현체를 생성합니다. 개발자가 @FeignClient 애노테이션이 붙은 인터페이스의 메서드를 호출하면, 프록시가 이를 가로채어(Intercept) HTTP 요청으로 변환합니다
4-2) 구조적 한계: 블로킹과 비동기 지원의 부재
Feign은 태생적으로 동기/블로킹 클라이언트입니다. 인터페이스의 리턴 타입으로 CompletableFuture 등을 사용할 수는 있지만, 내부적으로는 여전히 블로킹 방식으로 동작합니다. 커뮤니티에서 비동기 I/O 지원을 위한 논의가 오랫동안 있었으나, Feign의 내부 구조(인터셉터, 에러 디코더 등)가 동기식 실행을 전제로 설계되어 있어 완전한 논블로킹 지원은 요원한 상태입니다. 또한, 런타임에 프록시를 생성하고 요청을 해석하는 과정에서 미세한 성능 오버헤드가 발생하며, 디버깅 시 프록시 내부를 들여다보기 어렵다는 단점이 있습니다
사용 예시 (JAVA Code)
// 1. 인터페이스 정의
@FeignClient(name = "order-client", url = "https://api.orders.com")
public interface OrderClient {
@GetMapping("/orders/{orderId}")
OrderDto getOrder(@PathVariable("orderId") String orderId);
@PostMapping("/orders")
void createOrder(@RequestBody OrderRequest request);
}
// 2. 서비스에서 사용
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderClient orderClient; // 자동으로 주입됩니다.
public void processOrder(String orderId) {
// 일반 메소드 호출하듯이 사용!
OrderDto order = orderClient.getOrder(orderId);
System.out.println("주문 정보: " + order);
}
}
5. RestClient: 모던 자바의 새로운 표준 (강력 추천)
Spring Boot 3.2부터 도입된 동기 방식의 최신 표준입니다. WebClient의 예쁜 API 디자인을 그대로 가져오면서, 내부는 RestTemplate처럼 단순한 구조를 가집니다. 특히 Java 21 가상 스레드(Virtual Threads)와 함께 쓰면, 복잡한 비동기 코드 없이도 엄청난 성능을 낼 수 있는 '치트키'입니다.
5-1) 디자인 철학: 함수형 API와 인프라의 재사용
RestClient는 WebClient와 거의 동일한 함수형(Fluent) API를 제공합니다. 그러나 내부적으로는 Mono/Flux와 같은 리액티브 타입 대신 일반적인 자바 객체를 반환합니다. 가장 중요한 점은 RestTemplate이 사용하던 ClientHttpRequestFactory, HttpMessageConverter, ClientHttpRequestInterceptor 등의 인프라를 그대로 재사용한다는 것입니다. 이는 기존 RestTemplate 사용자들이 러닝 커브 없이, 그리고 기존 설정을 유지한 채 쉽게 마이그레이션할 수 있음을 의미합니다.
5-2) Spring Boot 3.4의 자동 설정과 기능 강화
최신 Spring Boot 3.4 릴리스에서는 RestClient에 대한 지원이 대폭 강화되었습니다.
- 다양한 HTTP 클라이언트 자동 감지: 클래스패스에 있는 라이브러리에 따라 최적의 ClientHttpRequestFactory를 자동으로 설정합니다. 우선순위는 다음과 같습니다:
- Apache HttpComponents
- Jetty Client
- Reactor Netty HttpClient
- JDK HttpClient (java.net.http.HttpClient)
- JDK HttpURLConnection (Simple)
- SSL 번들(SSL Bundles) 통합: RestClient.Builder는 Spring Boot의 SSL 번들 기능을 기본적으로 지원하여, mTLS(상호 인증)나 커스텀 트러스트 스토어 설정을 프로퍼티 파일만으로 간편하게 적용할 수 있습니다.
사용 예시 (JAVA Code)
기본 GET 요청 및 에러 처리 (onStatus) RestClient는 상태 코드에 따른 에러 처리를 람다식으로 깔끔하게 정의할 수 있습니다. RestTemplate의 전역적인 ResponseErrorHandler보다 훨씬 지역적이고 유연합니다.
@Service
public class PaymentService {
private final RestClient restClient;
public PaymentService(RestClient.Builder builder) {
this.restClient = builder.baseUrl("https://api.payment.com").build();
}
public PaymentStatus pay(PaymentRequest request) {
return restClient.post()
.uri("/payments")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
// 상태 코드별 예외 처리가 매우 직관적입니다.
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
throw new IllegalArgumentException("결제 정보 오류");
})
.body(PaymentStatus.class);
}
}
사용 예시2 (JAVA Code)
글로벌 설정 및 타임아웃 구성 (Boot 3.4 스타일) ClientHttpRequestFactoryBuilder를 사용하여 전역적인 타임아웃 설정을 적용한 빈(Bean)을 생성할 수 있습니다.
@Configuration
public class RestClientConfig {
@Bean
public RestClient customRestClient(RestClient.Builder builder) {
// Spring Boot 3.4의 새로운 팩토리 빌더 사용
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(Duration.ofSeconds(2))
.withReadTimeout(Duration.ofSeconds(5));
// JDK HttpClient 기반의 팩토리 생성
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder.jdk()
.build(settings);
return builder
.requestFactory(requestFactory)
.defaultHeader("X-Service-ID", "my-service")
.build();
}
}
6. @HttpExchange: Feign을 대체할 미래 (HTTP Interfaces)
Spring 6.0에 내장된 '선언적 HTTP 클라이언트'입니다. OpenFeign처럼 인터페이스 기반으로 동작하지만, 별도의 무거운 의존성 없이 Spring 프레임워크 자체 기능만으로 동작합니다. 뒷단의 통신 엔진을 RestClient나 WebClient 중에 골라 끼울 수 있어 유연함 끝판왕입니다.
- WebClient 사용 시: WebClientAdapter를 사용하면 리액티브 논블로킹 클라이언트가 생성됩니다.
- RestClient 사용 시 (Spring 6.1+): RestClientAdapter를 사용하면 동기 블로킹 클라이언트가 생성됩니다.
사용 예시 (JAVA Code)
// 1. 인터페이스 정의 (어노테이션이 조금 다릅니다!)
@HttpExchange("/notifications")
public interface NotificationClient {
@PostExchange("/email")
void sendEmail(@RequestBody EmailRequest request);
}
// 2. 설정(Configuration) - 프록시 팩토리 설정 필요
@Configuration
public class ClientConfig {
@Bean
public NotificationClient notificationClient(RestClient.Builder builder) {
// RestClient를 기반으로 인터페이스 구현체를 생성
RestClient restClient = builder.baseUrl("https://api.noti.com").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
return factory.createClient(NotificationClient.class);
}
}
// 3. 서비스에서는 주입받아 사용하면 끝!
7. 종합 비교 분석 및 의사결정 매트릭스
네 가지 클라이언트의 특성을 아키텍처 관점에서 종합적으로 비교하면 아래와 같습니다.
표 2. Spring HTTP 클라이언트 종합 비교
| 특성 | RestTemplate | WebClient | OpenFeign | RestClient |
| I/O 모델 | 블로킹 (동기) | 논블로킹 (비동기) | 블로킹 (동기) | 블로킹 (동기) |
| API 스타일 | 템플릿 메서드 (오버로딩) | 함수형 (Fluent API) | 선언적 (인터페이스) | 함수형 (Fluent API) |
| 기반 기술 | Servlet/HTTP Client | Netty/Reactor | Dynamic Proxy | Servlet/HTTP Client |
| 의존성 | spring-web | spring-webflux | spring-cloud-starter-openfeign | spring-web |
| 가상 스레드 호환 | 우수함 | 효과 미미함 | 우수함 | 최적 |
| 주 사용 사례 | 레거시 유지보수 | 고성능/스트리밍/게이트웨이 | 마이크로서비스 통신 (Cloud) | 일반적인 비즈니스 로직 |
| Spring 상태 | 유지보수 모드 (Deprecated 예정) | 권장 (리액티브 스택) | 표준 (Spring Cloud) | 권장 (서블릿 스택) |
8.그래서 무엇을 선택해야 할까요?
다양한 선택지가 있지만, 2025년 현재 시점에서 가장 추천하는 조합은 다음과 같습니다.
- 일반적인 신규 프로젝트: 무조건 RestClient + Java 21 가상 스레드 조합을 추천합니다. 코드는 쉽고 성능은 강력합니다.
- 인터페이스 방식을 선호한다면: 무거운 Feign 대신 @HttpExchange를 사용하여 RestClient를 연결해 보세요. 깔끔함과 성능을 모두 잡을 수 있습니다.
- 대용량 스트리밍/완전 비동기: 이때만 WebClient를 사용하세요.
기술은 계속 변하지만, 결국 목적은 '안정적이고 효율적인 통신'입니다. 오늘 보여드린 코드 예시들을 바탕으로 여러분의 프로젝트를 한 단계 업그레이드해보세요!

📚 참조 링크
'IT > JAVA&SpringBoot' 카테고리의 다른 글
| Spring Boot 3 PageHelper 완벽 가이드: 페이징 처리의 정석 (MyBatis 필수 설정) (0) | 2025.12.17 |
|---|---|
| [Java 21] 서버 성능을 10배 높이는 비밀: 블로킹 vs 논블로킹, 그리고 가상 스레드의 혁명 (0) | 2025.12.15 |
| Java 21(JDK 21) 개발자가 꼭 알아야 할 핵심 변경점 및 마이그레이션 가이드 (0) | 2025.12.15 |
| 자바 개발자의 연봉을 결정짓는 운명의 시간! JDK 25 & Spring Boot 4 완벽 해부 (0) | 2025.12.10 |
| ☕ Java 개발자의 영원한 숙제, "도대체 어떤 JDK를 써야 할까?" 완벽 정리 (0) | 2025.12.10 |
댓글