Spring Boot에서 외부 API를 호출해야 하는 경우 RestClient vs WebClient vs Feign Client vs WebFlux 중 무엇을 선택해야 좋을지 궁금해서 끄적여 보았습니다.
1. RestTemplate은 왜 고려하지 않는지?
과거에는 RestTemplate이 기본 선택이었습니다. 하지만 현재는 유지보수 모드이며, Spring 공식 문서에서도 신규 프로젝트에서는 사용을 권장하지 않음
따라서 Boot 4 기준에서는:
- RestTemplate → 구버전
- RestClient / WebClient / Feign Client / WebFlux → 사용 권장
2. RestClient란?
RestClient는 Spring Framework 6.1부터 도입된 최신 동기 HTTP 클라이언트
특징
- 동기 방식 (Blocking)
- 코드가 간결하고 직관적
- Spring MVC와 궁합이 좋음
- Boot 4 공식 권장 방식
- 빌더 패턴으로 가독성 우수
예시 코드
RestClient restClient = RestClient.create();
KakaoUserInfo response = restClient.get()
.uri("https://api.kakao.com/v2/user/me")
.header("Authorization", "Bearer " + token)
.retrieve()
.body(KakaoUserInfo.class);
기존 RestTemplate보다 훨씬 가독성이 좋고 체이닝 방식이라 깔끔함
3. WebClient란?
WebClient는 Spring의 리액티브(비동기, 논블로킹) HTTP 클라이언트
특징
- 비동기(Non-blocking)
- 대규모 트래픽에 유리
- 병렬 API 호출 가능
- 리액티브 아키텍처에 적합
- Reactor 기반 Mono/Flux 반환
예시 코드
WebClient webClient = WebClient.create();
KakaoUserInfo response = webClient.get()
.uri("https://api.kakao.com/v2/user/me")
.header("Authorization", "Bearer " + token)
.retrieve()
.bodyToMono(KakaoUserInfo.class)
.block(); // MVC에서는 block() 호출 필요
주의: MVC 환경에서는 결국 .block()을 호출해야 하므로 비동기의 장점이 줄어드는 경우가 많음
4. WebFlux란?
WebFlux는 Spring이 제공하는 완전한 리액티브 웹 프레임워크입니다. 단순한 HTTP 클라이언트가 아니라 서버 프레임워크
특징
- 완전한 논블로킹/비동기 아키텍처
- Netty를 기반으로 한 리액티브 서버
- Mono/Flux로 스트림 처리
- MVC를 완전히 대체하는 선택지
- 고성능, 저리소스 사용
- 함수형 프로그래밍 지원
MVC vs WebFlux 아키텍처 비교
| 서버 | Tomcat (Servlet 기반) | Netty (리액티브 기반) |
| 스레드 모델 | 스레드 풀 (요청당 1 스레드) | 이벤트 루프 (스레드 수 최소화) |
| 동작 방식 | 블로킹 | 논블로킹 |
| 메모리 사용 | 높음 (스레드당 메모리) | 낮음 (스레드 수 적음) |
| 동시 연결 | 제한적 | 매우 많음 |
| 학습곡선 | 낮음 | 높음 |
WebFlux 예시 코드 (서버)
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
// Mono: 단일 값 반환
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userService.findById(id);
}
// Flux: 여러 값 스트림 반환
@GetMapping("/users")
public Flux<User> getAllUsers() {
return userService.findAll();
}
}
WebFlux 예시 코드 (서비스)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 단일 값
public Mono<User> findById(Long id) {
return Mono.fromCallable(() -> userRepository.findById(id))
.filterWhen(opt -> Mono.just(opt.isPresent()))
.map(opt -> opt.get());
}
// 스트림 값
public Flux<User> findAll() {
return Flux.fromIterable(userRepository.findAll());
}
}
WebFlux + WebClient 조합 (병렬 처리)
@Service
public class OrderService {
private final WebClient webClient = WebClient.create();
// Mono 반환 - 리액티브 완성
public Mono<Order> createOrder(OrderRequest req) {
Mono<Payment> paymentMono = webClient.post()
.uri("http://payment-service/pay")
.bodyValue(req.getPayment())
.retrieve()
.bodyToMono(Payment.class);
Mono<Stock> stockMono = webClient.get()
.uri("http://stock-service/check/{id}", req.getProductId())
.retrieve()
.bodyToMono(Stock.class);
Mono<Shipping> shippingMono = webClient.get()
.uri("http://shipping-service/quote")
.retrieve()
.bodyToMono(Shipping.class);
// 세 개 API를 병렬로 호출 - 완전한 비동기 (block() 없음!)
return Mono.zip(paymentMono, stockMono, shippingMono)
.map(tuple -> new Order(tuple.getT1(), tuple.getT2(), tuple.getT3()));
}
}
주목: WebFlux에서는 .block()을 호출하지 않습니다. 완전한 논블로킹 구조를 유지
5. Feign Client란?
Feign Client는 Netflix가 개발한 선언적 HTTP 클라이언트로, Spring Cloud에서 제공
특징
- 인터페이스 기반 선언적 방식
- 어노테이션으로 HTTP 요청 정의
- 동기 방식 (Blocking)
- MSA 환경에서 표준처럼 사용
- 자동 로드밸런싱 지원 (Eureka 연동 시)
- 에러 처리 및 재시도 로직 자동화 가능
의존성
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
기본 예시 코드
@FeignClient(name = "kakao-api", url = "https://api.kakao.com")
public interface KakaoFeignClient {
@GetMapping("/v2/user/me")
KakaoUserInfo getUserInfo(
@RequestHeader("Authorization") String token
);
}
// 사용
@Service
public class OAuthService {
@Autowired
private KakaoFeignClient kakaoClient;
public KakaoUserInfo getKakaoUser(String token) {
return kakaoClient.getUserInfo("Bearer " + token);
}
}
Feign의 강력한 기능
1. 자동 로드밸런싱 (Eureka 연동)
@FeignClient(name = "user-service") // Eureka에서 자동 조회
public interface UserFeignClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable Long id);
}
2. 재시도 로직 자동화
@FeignClient(name = "payment-service",
configuration = FeignConfig.class)
public interface PaymentFeignClient {
@GetMapping("/payment/{id}")
Payment getPayment(@PathVariable Long id);
}
@Configuration
public class FeignConfig {
@Bean
public Retryer retryer() {
return new Retryer.Default(100, 1000, 3); // 3회 재시도
}
}
3. 타임아웃 설정
@Configuration
public class FeignConfig {
@Bean
public Request.Options options() {
return new Request.Options(
5, TimeUnit.SECONDS, // 연결 타임아웃
5, TimeUnit.SECONDS, // 읽기 타임아웃
true // 리다이렉트 허용
);
}
}
4. 에러 처리
@Component
public class FeignErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 404) {
return new UserNotFoundException("User not found");
}
if (response.status() == 500) {
return new ServerException("Server error");
}
return new FeignException("API call failed");
}
}
5. 완전한 설정 예시
@FeignClient(
name = "payment-service",
url = "http://payment-service:8080",
configuration = PaymentFeignConfig.class
)
public interface PaymentFeignClient {
@PostMapping("/payments")
PaymentResponse processPayment(@RequestBody PaymentRequest request);
@GetMapping("/payments/{id}")
PaymentResponse getPayment(@PathVariable Long id);
}
@Configuration
public class PaymentFeignConfig {
// 재시도 로직
@Bean
public Retryer retryer() {
return new Retryer.Default(100, 1000, 3);
}
// 타임아웃
@Bean
public Request.Options options() {
return new Request.Options(5, TimeUnit.SECONDS, 5, TimeUnit.SECONDS, true);
}
// 에러 처리
@Bean
public ErrorDecoder errorDecoder() {
return new PaymentErrorDecoder();
}
// 로깅
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
6. 무엇을 선택해야 할까?
RestClient를 선택해야 하는 경우
- Spring MVC 기반 프로젝트
- OAuth 토큰 발급 / 유저 조회 같은 단순 API 호출
- 외부 API 연동 (카카오, 구글, 네이버 등)
- 복잡한 병렬 처리 필요 없음
- 코드 단순성이 중요
- 대부분의 일반적인 백엔드 서비스
WebClient를 선택해야 하는 경우
- WebFlux 기반 리액티브 서버
- 동시에 여러 외부 API 호출해야 함
- 고트래픽 마이크로서비스
- 논블로킹 구조가 필요한 경우
- 대규모 배치 작업
- MSA + 고성능 환경
WebFlux를 선택해야 하는 경우
- 완전한 리액티브 아키텍처 필요
- 실시간 스트리밍 데이터 처리
- 매우 높은 동시 연결 수 (10,000+)
- 마이크로서비스 환경에서 응답 시간이 매우 중요
- 함수형 프로그래밍 스타일 선호
- 고성능, 저리소스 마이크로서비스
- 학습곡선이 높을 수 있을 수도?
Feign Client를 선택해야 하는 경우
- 마이크로서비스 아키텍처 (MSA)
- 서비스 간 통신이 주 목적
- Eureka 같은 서비스 디스커버리 사용
- 선언적인 코드 스타일 선호
- 재시도 로직이나 로드밸런싱 자동화 필요
- 여러 개의 내부 API 호출
- 모놀리식 구조보다는 분산 환경
7. 실전 예시: 상황별 선택
시나리오 1: 카카오 로그인 구현
// RestClient 사용
@Service
public class KakaoOAuthService {
private final RestClient restClient = RestClient.create();
public KakaoUserInfo getUserInfo(String token) {
return restClient.get()
.uri("https://api.kakao.com/v2/user/me")
.header("Authorization", "Bearer " + token)
.retrieve()
.body(KakaoUserInfo.class);
}
}
이유: 단순 외부 API 호출, MVC 환경, 코드 간결성 필요
시나리오 2: 주문 시 동시에 결제 + 재고 확인 + 배송 조회
// WebClient 사용
@Service
public class OrderService {
private final WebClient webClient = WebClient.create();
public Order createOrder(OrderRequest req) {
Mono<Payment> paymentMono = webClient.post()
.uri("http://payment-service/pay")
.bodyValue(req.getPayment())
.retrieve()
.bodyToMono(Payment.class);
Mono<Stock> stockMono = webClient.get()
.uri("http://stock-service/check/{id}", req.getProductId())
.retrieve()
.bodyToMono(Stock.class);
Mono<Shipping> shippingMono = webClient.get()
.uri("http://shipping-service/quote")
.retrieve()
.bodyToMono(Shipping.class);
// 세 개 API를 병렬로 호출하고 모두 완료 대기
return Mono.zip(paymentMono, stockMono, shippingMono)
.map(tuple -> new Order(tuple.getT1(), tuple.getT2(), tuple.getT3()))
.block(); // MVC에서 동기 반환
}
}
이유: 병렬 API 호출, 대기 시간 최소화, 높은 동시성
시나리오 3: 실시간 주문 알림 시스템 (고동시성)
// WebFlux + WebClient 사용
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
// Server-Sent Events (SSE) - 실시간 스트림
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Order> getOrderStream() {
return orderService.getOrderStream();
}
// WebSocket도 가능
@GetMapping("/subscribe")
public Mono<Order> subscribeOrder(@PathVariable Long id) {
return orderService.findById(id);
}
}
@Service
public class OrderService {
private final WebClient webClient = WebClient.create();
// Flux로 계속 데이터 스트림
public Flux<Order> getOrderStream() {
return Flux.interval(Duration.ofSeconds(1)) // 1초마다 실행
.flatMap(i -> webClient.get()
.uri("http://order-service/latest")
.retrieve()
.bodyToMono(Order.class)
)
.retry(3); // 실패 시 3회 재시도
}
}
이유: 완전한 논블로킹, 실시간 스트리밍, 매우 높은 동시성 (10,000+ 연결)
시나리오 4: MSA 환경에서 주문 서비스가 결제 서비스 호출
// Feign Client 사용
@FeignClient(
name = "payment-service",
url = "http://payment-service:8080",
configuration = PaymentFeignConfig.class
)
public interface PaymentFeignClient {
@PostMapping("/payments")
PaymentResponse processPayment(@RequestBody PaymentRequest request);
@GetMapping("/payments/{id}")
PaymentResponse getPayment(@PathVariable Long id);
}
@Configuration
public class PaymentFeignConfig {
// 재시도 로직: 3회 시도, 100ms ~ 1000ms 간격
@Bean
public Retryer retryer() {
return new Retryer.Default(100, 1000, 3);
}
// 타임아웃
@Bean
public Request.Options options() {
return new Request.Options(5, TimeUnit.SECONDS, 5, TimeUnit.SECONDS, true);
}
// 에러 디코딩
@Bean
public ErrorDecoder errorDecoder() {
return new PaymentErrorDecoder();
}
}
// 사용
@Service
public class OrderService {
@Autowired
private PaymentFeignClient paymentClient;
public Order createOrder(OrderRequest req) {
PaymentResponse payment = paymentClient.processPayment(
new PaymentRequest(req.getAmount(), req.getOrderId())
);
if (payment.isSuccess()) {
return new Order(req, payment);
}
throw new PaymentFailedException("결제 실패");
}
}
이유:
- 서비스 간 통신
- Eureka와 함께 사용하면 로드밸런싱 자동화
- 재시도/타임아웃/에러 처리 자동화
- 선언적이고 깔끔한 코드
8. 선택 플로우차트
HTTP 클라이언트가 필요한가?
│
├─ 웹 서버 자체를 리액티브하게 구성하고 싶은가?
│ └─ YES → WebFlux
│
├─ 외부 API 호출인가? (카카오, 구글, 네이버 등)
│ └─ YES → RestClient
│
├─ 마이크로서비스 간 통신인가?
│ └─ YES → Feign Client
│
└─ 동시에 여러 API를 병렬로 호출해야 하는가?
└─ YES → WebClient
└─ NO → RestClient 또는 Feign Client 중 선택
10. 성능 비교
시나리오처리량 (req/s)응답시간 (ms)메모리 (MB)
| MVC + RestClient (1,000 동시) | 5,000 | 200 | 500 |
| MVC + WebClient (1,000 동시) | 8,000 | 125 | 300 |
| WebFlux + WebClient (10,000 동시) | 50,000 | 50 | 200 |
| MSA + Feign (1,000 동시) | 6,000 | 167 | 400 |
주목: 동시 연결이 1,000을 넘어가면 WebFlux의 장점이 극대화
결론
| 일반 웹 애플리케이션 + 외부 API 호출 | RestClient | 간단하고 Spring MVC와 완벽 호환 |
| 고성능 필요 + 병렬 처리 많음 | WebClient | 논블로킹으로 동시성 극대화 |
| 완전한 리액티브 아키텍처 + 실시간 스트림 | WebFlux | 고동시성, 저리소스, 스트리밍 처리 |
| MSA 환경 + 서비스 간 통신 | Feign Client | 선언적이고 자동화 기능 우수 |
Spring Boot 4 시대에는 더 이상 RestTemplate을 선택할 이유가 없음
'Spring Boot' 카테고리의 다른 글
| 왜 UUID v7을 선택했는가? (Snowflake와 비교까지) (0) | 2026.02.26 |
|---|---|
| Lettuce VS Redisson 분산 락 (0) | 2026.02.13 |
| 🚚 허브 간 최단 경로 탐색 캐시 스탬피드 (Cache Stampede) 발생+해결 (1) | 2026.01.19 |
| 🚚 허브 간 최단 경로 탐색 알고리즘 구현 + Kakao Map Api 연동 (Spring Boot) (0) | 2025.12.06 |
| DDD의 페이징 로직 (Spring Boot) (0) | 2025.12.02 |