우리 API가 응답을 만들려면, 이제 다른 회사의 API부터 물어봐야 해요.지난 글에서는 WebSocket과 STOMP를 보면서 서버와 브라우저가 오래 열린 연결 위에서 메시지를 주고받는 흐름을 봤어요. 오늘은 방향을 바꿔볼게요. 이번에는 우리 Spring Boot 앱이 클라이언트가 되어서 다른 HTTP API를 호출하는 장면이에요. 처음에는 이런 코드가 자연스러워 보여요.
“RestClient와 WebClient 중 뭘 써야 하죠?"오늘은 문법보다 경계를 먼저 볼 거예요. 다른 서버를 호출하는 코드는 단순한 유틸 함수가 아니라, 우리 서비스 바깥으로 나가는 outbound API 경계예요. Spring Boot는 이 경계에서
"HTTP Interface는 Feign 같은 건가요?"
"timeout은 어디서 정해야 하나요?"
"외부 API가 500을 주면 우리 API도 바로 500인가요?"
"retry를 붙이면 더 안전해지는 거 아닌가요?"
"controller에서 바로 호출해도 되나요?”
RestClient, WebClient, HTTP Service Interface를 쓸 수 있게 도와주지만, 무엇을 고를지와 실패를 어떻게 다룰지는 우리가 설계해야 해요.
이 글은 Spring Boot 4.1.0과 Spring Framework 7.0.x 공식 문서의 REST clients,
RestClient, WebClient, HTTP Interface, HTTP Service client group 설명을 기준으로 작성했어요. Spring Boot 3.x 프로젝트에서도 큰 선택 기준은 비슷하지만, starter 이름과 HTTP Service client group 설정은 사용 중인 버전 문서를 함께 확인하세요.서버도 다른 서버 앞에서는 클라이언트예요
주문 API가 있다고 해볼게요. 사용자가 주문 상세 화면을 열면 우리 서버는 주문 정보만 갖고 있지 않을 수 있어요.- 결제 상태는 결제 서버에서 가져와요.
- 배송 상태는 배송 서버에서 가져와요.
- 적립 예정 포인트는 포인트 서버에서 가져와요.
GET /orders/42 하나예요. 하지만 서버 안쪽에서는 여러 HTTP 호출이 이어질 수 있어요.
이 그림에서 중요한 건 Spring Boot 앱이 두 역할을 한다는 점이에요. 브라우저에게는 서버지만, 결제 API와 배송 API 앞에서는 클라이언트예요.
그래서 outbound 호출 코드는 이런 책임을 가져요.
처음에는
GET 한 줄처럼 보여도, 실제로는 외부 시스템과 맺는 작은 계약이에요. 이 계약을 controller 안에 흩뿌리면 나중에 장애가 났을 때 “어느 외부 API가 느린지”, “어느 status를 우리가 잘못 해석했는지” 찾기 어려워져요.
RestClient는 blocking 앱의 기본 선택지예요
Spring MVC, JDBC, JPA처럼 blocking 흐름으로 짜인 앱이라면 먼저 RestClient를 떠올리면 돼요. RestClient는 Spring Framework의 동기 HTTP client예요. 요청을 보내고 응답이 올 때까지 현재 thread가 기다리는 모델이에요.
Spring Boot는 미리 설정된 RestClient.Builder bean을 제공해요. 그래서 보통은 직접 RestClient.create()를 흩뿌리기보다 builder를 주입받아 외부 API별 client를 만들어둬요.
PaymentClient는 “결제 서버를 어떻게 부를지”를 감싸는 adapter예요. controller나 service가 외부 API path를 직접 알 필요가 없게 해줘요.
RestClient는 이런 경우에 잘 맞아요.
처음에는 여기까지만 잡아도 충분해요. MVC 기반 서버에서 다른 REST API를 호출한다면
RestClient가 가장 읽기 쉬운 출발점이에요.
WebClient는 non-blocking 흐름에 맞는 client예요
WebClient는 이름 때문에 “웹 API 호출용 최신 client”처럼 보일 수 있어요. 하지만 핵심은 최신이 아니라 non-blocking, reactive HTTP client라는 점이에요.
지난 글에서 봤듯이 WebFlux는 기다리는 동안 thread를 붙잡지 않는 실행 모델이에요. WebClient도 그 흐름에 맞춰 Mono, Flux를 반환해요.
PaymentResponse가 아니라 Mono<PaymentResponse>예요. “결제 응답이 지금 있다”가 아니라 “결제 응답이 나중에 하나 올 수 있다”는 흐름을 표현해요.
WebFlux handler나 reactive service에서는 이 흐름이 자연스러워요.
WebClient를 가져와서 마지막에 .block()을 붙이면 이야기가 달라져요.
RestClient와 WebClient의 선택은 “외부 API 호출 문법”보다 애플리케이션의 실행 모델과 더 가까워요.
HTTP Interface는 호출 코드를 계약처럼 보이게 해줘요
이제 세 번째 선택지가 나와요. HTTP Service Interface예요.RestClient나 WebClient를 직접 쓰면 “어떻게 호출하는지”가 코드에 드러나요.
Spring Framework 쪽에서는
HttpServiceProxyFactory가 이 interface의 proxy를 만들어요. Spring Boot 4.1에서는 HTTP Service client group을 통해 base URL, timeout, SSL 같은 공통 설정을 묶는 흐름도 제공해요.
예를 들어 결제 API group을 둔다면 설정은 이런 감각으로 읽을 수 있어요.
PaymentHttpService를 주입받아 외부 API를 호출할 수 있어요.
method 모양이 예뻐져도 외부 API가 느리거나, 응답 schema가 바뀌거나, 인증 header가 빠지면 똑같이 실패해요. HTTP Interface는 호출 코드를 계약처럼 정리해주는 도구이지, 장애와 호환성 문제를 없애주는 도구는 아니에요.
timeout은 옵션이 아니라 계약이에요
외부 API 호출에서 가장 위험한 기본값은 “언젠가 응답하겠지”예요. 사용자가 주문 상세를 열었는데 결제 API가 30초 동안 응답하지 않는다고 해볼게요. 우리 서버가 아무 제한 없이 기다리면 사용자는 화면을 못 보고, 요청 thread는 묶이고, 동시 요청이 쌓이면서 우리 앱까지 느려질 수 있어요. 그래서 외부 호출에는 최소한 두 시간을 구분해서 생각해야 해요.
Spring Boot 4.1의 HTTP client 설정은 공통값과 service client group별 값을 나눠 잡을 수 있어요.
retry는 아무 실패에나 붙이면 위험해요
timeout 다음으로 많이 붙이는 것이 retry예요.“실패하면 한 번 더 해보면 더 안정적이지 않나요?”항상 그렇지는 않아요. retry는 상대 서버가 잠깐 흔들렸을 때 도움이 될 수 있지만, 잘못 붙이면 장애를 키워요. 예를 들어 상품 목록 조회가 실패했을 때 한 번 더 시도하는 건 비교적 안전할 수 있어요.
retry는 “실패를 숨기는 기능”이 아니에요. 실패를 조금 더 견디게 만들 수 있지만, 상대 서버가 이미 과부하라면 retry traffic이 더 큰 압박이 될 수 있어요.
에러 응답은 우리 서비스 언어로 번역해야 해요
외부 API가 실패했을 때 가장 쉬운 코드는 예외를 그대로 올려보내는 거예요. 하지만 그러면 우리 API의 에러 계약이 외부 서버의 상태와 body에 흔들려요. 결제 API가404 Not Found를 줬다고 해볼게요. 우리 API도 무조건 404일까요?
상황에 따라 달라요.
그래서 외부 client는 보통 외부 실패를 우리 도메인의 실패로 번역해요.
5xx를 우리 서비스가 이해하는 “결제 gateway를 사용할 수 없음”으로 바꿨다는 점이에요.
실무에서는 여기에 log, metric, trace tag도 같이 붙여야 해요.
- 어느 외부 service를 호출했나요?
- method와 path template은 무엇인가요?
- status code는 무엇이었나요?
- timeout인지, DNS인지, connection refused인지 구분되나요?
- 사용자의 주문 ID나 request ID로 추적할 수 있나요?
client 코드는 어디에 두는 게 좋을까요?
작은 예제에서는 controller에서 바로 호출해도 동작해요.
보통은 외부 시스템별 client adapter를 둬요.
이 구조에서 service는 애플리케이션의 결정을 담고, client adapter는 외부 API 호출과 번역을 맡아요. 나중에 HTTP Interface를 쓰든
RestClient를 직접 쓰든 service 쪽의 언어는 크게 흔들리지 않게 만들 수 있어요.
처음 프로젝트라면 너무 거창하게 나누지 않아도 돼요. 다만 “외부 API별로 client class 하나” 정도는 초반부터 두는 편이 유지보수에 유리해요.
그래서 무엇을 고르면 될까요?
세 도구를 한 번에 정리해볼게요.
그리고 어떤 도구를 쓰든 공통 원칙은 같아요.
- base URL은 설정으로 빼요.
- timeout은 외부 API별 기대 시간에 맞춰 잡아요.
- error response는 우리 서비스 언어로 번역해요.
- retry는 멱등성과 중복 방지 계약을 확인한 뒤 붙여요.
- 외부 service 이름, path template, status, duration을 관측 가능하게 남겨요.
- controller보다 외부 시스템별 client adapter에 호출 세부사항을 모아요.
참고한 링크
- Spring Boot Reference: REST Clients
- Spring Framework Reference: REST Clients
- Spring Framework Reference: WebClient
- Spring Framework Reference: HTTP Interface
자, 정리해볼까요?
- Spring Boot 앱도 다른 API를 호출할 때는 HTTP client가 돼요.
- Spring MVC, JDBC, JPA 중심의 blocking 앱에서는
RestClient가 가장 자연스러운 출발점이에요. - WebFlux와 reactive library로 끝까지 이어지는 흐름에서는
WebClient가 잘 맞아요. - HTTP Interface는 외부 API 호출을 Java interface 계약처럼 모아 읽게 해줘요.
- timeout, retry, error mapping, 관측은 도구 선택보다 더 중요한 outbound API 설계예요.