Backend/Java

[Java] RestTemplate 대신 WebClient를 선택하는 이유

제이동 개발자 2025. 5. 25. 20:02
728x90

[Java] RestTemplate 대신 WebClient를 선택하는 이유

Spring Boot 기반 애플리케이션에서 외부 HTTP API 호출을 위해 오랫동안 RestTemplate을 사용해 왔습니다. 하지만 Spring 5부터 소개된 WebClient는 비동기·논블로킹 호출, 리액티브 스트림 기반 처리 등으로 마이크로서비스 아키텍처와 대용량 트래픽 환경에서 뛰어난 Spring Boot 성능 최적화를 제공합니다.
이 글에서는 RestTemplate의 한계를 짚어보고, WebClient로 전환해야 하는 이유에 대해서 알아보고자 합니다.

 

1. RestTemplate의 한계

1-1. 블로킹 I/O로 인한 스레드 낭비

  • RestTemplate은 내부적으로 HttpURLConnection·Apache HttpClient 등을 사용해 블로킹 I/O 방식으로 동작
  • 요청마다 스레드가 API 호출과 응답 대기 동안 점유 → 트래픽이 많아지면 스레드 풀 고갈
  • 결과적으로 CPU 사용률은 낮지만 블로킹 I/O로 인해 API 응답을 받을 때까지 대기하기 때문에 자원낭비가 심해짐

 

1-2 확장성(Scalability) 문제

  • 동시 호출 수가 많아지면 톰캣 톰캣 워커 스레드를 추가해야 하므로 메모리·CPU 리소스 소모 급증
  • 마이크로서비스 아키텍처에서 서비스 간 호출이 빈번할수록 확장 비용이 커짐

 

1-3. 유지보수 및 기능 확장 제약

  • RestTemplate은 필터(Filter)·Interceptor 등록이 제한적
  • 로깅·Retry·CircuitBreaker 같은 재시도 로직 구현 시 코드가 복잡해짐

 

1-4. RestTemplate은 “유지 보수 모드(Maintenance Mode)”

Spring 5.0부터 RestTemplate은 새 기능 추가 없이 버그 수정만 이루어지는 유지 보수 모드로 전환되었고. 반면에 WebClient는 계속해서 최적화·신기능이 추가되고 있습니다.

https://www.dhaval-shah.com/performant-and-optimal-spring-webclient/?utm_source=chatgpt.com

 

Performant and optimal Spring WebClient

Background In my previous post I tried demonstrating how to implement an optimal and performant REST client using RestTemplate In this article I will be …

www.dhaval-shah.com

 

 

2. WebClient 주요 특징

2-1. 논블로킹, 리액티브 스트림 기반

  • Spring WebFlux의 핵심 컴포넌트로, Reactor의 Flux·Mono를 지원
  • 호출 스레드가 I/O 대기 없이 즉시 반환 → 스레드 활용 효율 극대화

 

2-2. 풍부한 기능과 유연한 API

  • Function-style API 제공 (WebClient.create() 또는 DI 가능한 WebClient.Builder)
  • 호출 흐름 중간에 .filter(), .retryWhen(), .onStatus() 등을 체이닝해 쉽게 재시도·로깅·에러 처리

 

2-3. 커넥션 풀 관리

  • Netty 기반 기본 HTTP 클라이언트 지원 시 커넥션 풀 자동 관리
  • 타임아웃·Max Connections 설정으로 성능 최적화 가능

 

 

3. 그래서 왜 WebClient를 사용해야하는지 모르겠어

실제 업무시에는 공통 클래스로 만들어 놓은 RestTemplate 관련 Service를 만들어 사용해 왔었습니다. 그러다 개인 공부를 하던 중 WebClient를 알게 되었고,  최근에는 RestTemplate를 사용하는 것보다는 WebClient를 사용하는 것을 권장하는 것을 알게 되었습니다. 그래서 어떤 점이 좋길래 WebClient를 사용하는지 찾아보았는데 내용 대부분이 Spring WebFlux(리액티브-기반) 프레임워크에서의 이런저런 장점이 있다는 내용 위주라 Spring MVC 프레임워크를 사용하는 저에게는 크게 와닿았지 않았습니다.(리엑티브 프로그래밍 관련 공부를 해야겠다는 생각이 많이 들더군요. ㅜ)

 

따라서 직접 간단한 WebClient를 호출하는 로직을 구현하고 테스트해 보았습니다. 테스트 코드보단 직접 호출하여 그 결과를 보는 게 더 와닿기 때문에 아래와 같이 구현했습니다.

 

아래는 WebClient 사용 하여 외부 API를 호출하는 예시 코드입니다.

@RestController
@RequiredArgsConstructor
public class SampleController {

  private final SampleService sampleService;

  /**
   * WebClient를 이용한 외부 API 호출
   */
  @GetMapping("/api/foos/{id}")
  public Mono<ResponseEntity<FooResponse>> getFoo(@PathVariable Long id) {
    return sampleService.fetchFooSync(id)
        // 데이터가 있을 때 200 OK + 바디
        .map(ResponseEntity::ok)
        // Mono가 빈 값일 때 404 Not Found
        .defaultIfEmpty(ResponseEntity.notFound().build());
  }


  @GetMapping("/external/api/foos/{id}")
  public ResponseEntity<FooResponse> getExternalFoo(@PathVariable Long id) {

    // 3초 지연
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }

    return ResponseEntity.ok(new FooResponse(id, "푸우"));
  }
}

호출 결과

 

무언가 이상한 점을 느끼셨나요?

 

요청을 한 후 로그를 보면 "WebClient를 이용한 API 요청"가 출력이 되고 3초가 소모되는 외부 API를 호출하지만 바로 "WebClient를 이용한 API 요청 종료" 가 출력되는 것을 볼 수 있습니다. 그리고 그 이후 3초가 지나게 되면 Swagger에 응답 값이 오는 것을 확인할 수 있습니다.

 

어떤 원리로 요청 스레드(워커 스레드)가 종료가 되었는데 어떻게 응답 값을 줄 수 있을까요?

 

바로 Spring MVC 3.2부터 비동기 요청 처리(Async Servlet)를 지원하기 때문입니다. Servlet 3.0의 비동기 API를 사용하게 되면 처음 할당된 요청 스레드를 I/O 대기 중에 묶어두지 않고, 다른 스레드가 처리를 이어갈 수 있습니다. 이 메커니즘의 핵심은 AsyncContext와 AsyncListener입니다.

WebClient 사용 시 흐름도

 

 

  1. Client가 HTTP 요청을 보냅니다.
  2. Tomcat의 NIO IO 스레드(http-nio-exec-*) 중 하나가 요청을 수신해
    DispatcherServlet을 통해 Controller로 전달합니다.
    – 스레드: http-nio-exec-*
  3. ControllerService 메서드를 호출합니다.
    – 스레드: 여전히 http-nio-exec-*
  4. Service에서 WebClient(Reactor Netty)로 비동기 I/O 호출을 정의·구독하면,
    Spring MVC의 RequestMappingHandlerAdapter가 바로 request.startAsync()를 호출한 후
    이후 로직을 처리한 후 http-nio-exec-*는 해제됩니다.
  5. Reactor Netty 이벤트 루프 스레드(reactor-http-nio-*) 중 하나가 활성화되어
    External API로 논블로킹 HTTP 요청을 보냅니다.
    – 위치: Reactor Netty 커넥터
    – 스레드: reactor-http-nio-*
  6. External API가 (예: 3초 지연 후) 응답을 반환하면,
    Reactor Netty 이벤트 루프 스레드가 논블로킹으로 응답을 수신합니다.
    – 위치: Reactor Netty
    – 스레드: reactor-http-nio-*
  7. Reactor Netty 이벤트 루프 스레드는 받은 응답을 처리한 뒤 AsyncContext.dispatch()를 호출해
    Servlet Container의 Async 워커 스레드 풀을 깨웁니다.
    – 스레드: reactor-http-nio-*
  8. Tomcat의 Async 워커 스레드(http-nio-exec-*) 중 하나가 할당되어
    ResponseEntity 직렬화(JSON 변환) 및 HTTP 응답 전송을 최종 수행합니다.
    – 위치: Servlet Container 비동기 워커 풀
    – 스레드: http-nio-exec-*

 

즉, 요청 스레드(워커 스레드)는 WebClient가 비동기 호출이 되면 응답이 올 때까지 대기하지 않고, 다음 로직을 실행한 후 워커 스레드를 반납하게 됩니다. 이때 반납된 워커 스레드는 다른 요청들을 처리할 수 있기 때문에 스레드 자원을 효율적으로 사용할 수 있다는 이점이 있습니다. 이후 비동기 호출이 되었던 WebClient가 응답 값을 받게 되면 워커 스레드를 다시 할당받게 되고, WebClient의 응답 값을 직렬화하여 Client에게 전달하게 됩니다.

 

이처럼 RestTemplate의 경우 요청 스레드가 RestTemplate의 응답이 올 때까지 대기하게 되어 스레드를 효율적으로 쓸 수 없었지만 WebClient를 이용하면 이러한 문제들을 해결할 수 있다는 장점이 있습니다. 아래 이미지는 RestTemplate(Boot 1)을 사용했을 때와 WebClient(Boot 2)를 사용했을 때 Performance를 분석한 결과입니다.

성능 비교 Boo1 = RestTemplate, Boo2 = WebClient

 

하지만 위처럼 WebClient를 비동기 호출 한 후 별다른 로직없이 Client에게 응답 값을 전달하는 경우에는 워커 스레드들을 효율적으로 사용할 수 있게 되지만, 외부 API 호출 후 복잡한 작업(비즈니스 로직 처리와 DB에 저장 등)이 수행해야 하는 경우 동기 호출을 해야 하기 때문에 위와 같은 장점을 얻을 수 없습니다.(WebFlux는 아직 공부해 본 적이 없어서 어떨지 모르겠네요.) 

 

그렇지만 WebClient를 사용하게 된다면 Function-style API(WebClient.create()...), 재시도, 로깅, 에러처리와 같은 여러 풍부한 기능들을 사용하여 더 유연하고 편리하게 외부 API를 호출할 수 있습니다. 이와 관련한 내용을 다음에 글을 작성할 예정이기 때문에 자세히 다루지 않으려고 합니다.

 

 

 

마무리 글

 

이번 글을 작성하면서 왜 WebClient를 사용해야 하는지 알 수 있게 되었습니다. 도대체 왜 RestTemplate 대신 왜 WebClient를 사용해야 하는 거야?라는 의문으로 작성한 글입니다. 무턱대고 좋다고 사용하는 게 아니라 "그래서 왜? 무엇이? 어떤 게? 이점이 있어 사용하는 걸까?" 와 같이 제가 궁금한 부분 위주로 정리했기 때문에 모든 내용이 담겨 있지 않습니다. 더 추가적인 WebClient 내용들은 이미 정리를 잘 정리해 주신 분들이 많기 때문에 따로 정리하진 않았지만 다음에 작성할 예정 글은 WebClient 설정에 대해서 작성할 예정입니다. 어서 빨리 회사에서 사용하고 있는 RestTemplate 관련 로직을 모두 WebClient로 리팩토링을 하고 싶어 지네요..!

 

728x90