어제 오늘 내일

[WebClient] "RestTemplate은 이제 Deprecated!" 비동기 HTTP 요청의 정석, WebClient 사용법 본문

IT/SpringBoot

[WebClient] "RestTemplate은 이제 Deprecated!" 비동기 HTTP 요청의 정석, WebClient 사용법

hi.anna 2026. 4. 2. 08:21

 

오랫동안 사랑받아온 RestTemplate이 유지보수 모드(Maintenance Mode)로 들어갔다는 사실, 알고 계셨나요?
스프링 팀은 대놓고 "앞으로는 WebClient를 쓰세요"라고 권장합니다.

이유는 단순합니다. RestTemplate동기(Blocking) 방식이라서, 외부 API가 3초 걸리면 내 서버도 3초 동안 멈춰 있어야 하기 때문입니다.
반면 WebClient는 요청만 보내고 딴 일을 하러 갈 수 있습니다. (Non-Blocking)


1. WebClient 만들기 (Builder 패턴)

WebClient는 불변(Immutable) 객체라서 쓰레드 안전(Thread-safe)합니다. 싱글톤 빈으로 등록해서 재사용하는 것이 좋습니다.

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .baseUrl("http://api.external-server.com") // 기본 주소
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }
}

2. GET 요청 보내기 ( vs )

가장 기본적인 조회 요청입니다.
응답을 Mono<String>이나 Flux<UserDto>로 받을 수 있습니다.

@Service
@RequiredArgsConstructor
public class ExternalApiService {

    private final WebClient webClient;

    public Mono<UserDto> getUser(String id) {
        return webClient.get()
                .uri("/users/{id}", id)
                .retrieve() // 응답 본문(Body)을 바로 가져옴
                .bodyToMono(UserDto.class); // JSON -> Java 객체 변환
    }
}

참고: exchange() 메서드는 메모리 누수 위험 때문에 Deprecated 되었습니다. retrieve()를 쓰세요.


3. POST 요청 보내기

데이터를 보낼 때도 똑같습니다. bodyValue()만 추가하면 됩니다.

public Mono<String> createUser(UserDto userDto) {
    return webClient.post()
            .uri("/users")
            .bodyValue(userDto) // 요청 본문(Body) 설정
            .retrieve()
            .bodyToMono(String.class); // "생성 성공" 같은 응답 받기
}

4. WebClient의 필살기: 병렬 호출 ()

이게 진짜 핵심입니다.
"유저 정보(User)"와 "주문 내역(Order)"을 각각 다른 서버에서 가져와야 한다고 칩시다.

  • 유저 서버: 2초 소요
  • 주문 서버: 3초 소요

❌ RestTemplate (순차 실행: 2 + 3 = 5초)

User user = restTemplate.getForObject(...); // 2초 대기
Order order = restTemplate.getForObject(...); // 3초 대기
// 총 5초 걸림

✅ WebClient (병렬 실행: max(2, 3) = 3초)

Mono<User> userMono = webClient.get().uri("/users/1").retrieve().bodyToMono(User.class);
Mono<Order> orderMono = webClient.get().uri("/orders/1").retrieve().bodyToMono(Order.class);

// 두 요청을 동시에 쏘고(zip), 둘 다 올 때까지 기다림
return Mono.zip(userMono, orderMono)
        .map(tuple -> {
            User user = tuple.getT1();
            Order order = tuple.getT2();
            return new UserOrderDto(user, order);
        });
// 총 3초 걸림 (가장 느린 놈 기준)

API 개수가 많아질수록 성능 차이는 더 벌어집니다. MSA 환경에서는 선택이 아닌 필수입니다.


5. 에러 처리 ()

외부 서버가 400이나 500 에러를 뱉으면 어떻게 할까요?
retrieve() 뒤에 onStatus를 붙여서 예외 처리를 할 수 있습니다.

webClient.get()
        .uri("/users/1")
        .retrieve()
        .onStatus(
            status -> status.is4xxClientError(), // 400번대 에러면
            response -> Mono.error(new RuntimeException("잘못된 요청입니다."))
        )
        .onStatus(
            status -> status.is5xxServerError(), // 500번대 에러면
            response -> Mono.error(new RuntimeException("외부 서버가 죽었습니다."))
        )
        .bodyToMono(UserDto.class);

마치며

오늘의 결론입니다.

  1. WebClient는 Non-Blocking 방식이라, 외부 서버가 느려도 내 서버는 멈추지 않는다.
  2. Mono.zip을 사용하면 여러 API를 병렬(Parallel)로 호출해서 응답 시간을 획기적으로 줄일 수 있다.
  3. RestTemplate은 이제 잊고, WebClient로 갈아타자. (심지어 동기 방식인 MVC에서도 WebClient를 쓸 수 있습니다!)

이제 외부 API 연동까지 완벽하게 비동기로 처리할 수 있게 되었습니다.
하지만 아직 가장 큰 관문이 남았습니다. 바로 데이터베이스(DB)입니다.
WebClient로 기껏 비동기 처리를 해놓고, JPA(Blocking)를 쓰면 말짱 도루묵이거든요.
다음 포스팅에서는 "WebFlux에서 JPA 쓰면 망합니다!" JDBC의 대안인 R2DBC와 Reactive DB 연결에 대해 알아보겠습니다.
도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊

반응형
Comments