Spring Boot 4에서 외부 API 호출 무엇을 선택해야 할까?

2026. 3. 27. 22:13·Spring Boot

 

 

 

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
'Spring Boot' 카테고리의 다른 글
  • 왜 UUID v7을 선택했는가? (Snowflake와 비교까지)
  • Lettuce VS Redisson 분산 락
  • 🚚 허브 간 최단 경로 탐색 캐시 스탬피드 (Cache Stampede) 발생+해결
  • 🚚 허브 간 최단 경로 탐색 알고리즘 구현 + Kakao Map Api 연동 (Spring Boot)
kimfishes
kimfishes
kimfishes 님의 블로그 입니다.
  • kimfishes
    kimfishes 님의 블로그
    kimfishes
  • 전체
    오늘
    어제
    • 전체 (18) N
      • Infra (5)
        • AWS (0)
        • LogBack (4)
      • Spring Boot (13) N
        • LLM (4) N
      • 일상 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Discord 알림 연동
    Qdrant
    캐시 스탬피드
    실시간 알림 시스템
    loging
    ELK
    promtail
    Pre-Signed URL
    cache stampede
    UUID v7
    pgvector
    분산 락
    Spring boot
    traceId
    Redis
    ollama
    로깅
    spring ai
    LLM
    스프링 알림 시스템
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
kimfishes
Spring Boot 4에서 외부 API 호출 무엇을 선택해야 할까?
상단으로

티스토리툴바