자바 백엔드 입문 40편 — Spring WebClient RestClient HTTP 클라이언트

2026-05-17자바 백엔드 입문

자바 백엔드 입문 40편. 외부 API 호출용 Spring HTTP 클라이언트 WebClient·RestClient 표준 패턴을 우편배달부 비유로 풀어쓴 학습 노트.

📚 자바 백엔드 입문 · 40편 — Spring WebClient RestClient HTTP 클라이언트

이 글은 자바 백엔드 입문 시리즈 59편 중 40편이에요. 백엔드가 다른 백엔드를 호출하는 일 — 결제 API·푸시 알림 API·외부 데이터 조회 — 이게 HTTP 클라이언트. 이번 40편은 Spring 6 표준이 된 WebClient · RestClient 를 풀어 가요.

HTTP 클라이언트의 세대 변천

자바 백엔드의 외부 API 호출은 세 세대를 거쳤어요.

세대 도구 시점 상태
1세대 RestTemplate Spring 3.0+ 유지보수 모드 (deprecated 아님, 신규 X)
2세대 WebClient Spring 5+ (WebFlux) 비동기·논블로킹 표준
3세대 RestClient Spring 6.1+ 동기 표준, WebClient 스타일

한국 회사 신규 프로젝트 룰: - 동기 호출 = RestClient (Spring 6.1+) - 비동기·스트리밍 = WebClient - 레거시 프로젝트 = RestTemplate 유지하다 단계적 마이그레이션

이전 22편 RestTemplate 같은 자리에서 다룬 옛 패턴은 신규 프로젝트엔 X.

우편배달부 비유

HTTP 클라이언트 = "우편배달부". 봉투(요청)·받을 사람 주소(URL)·내용물(body)·도착했을 때 무엇 받을 건지(응답 타입) 설정하면 — 클라이언트가 알아서 우체국 거쳐 전달·답신 받기.

  • RestTemplate = 오래된 우편 시스템 (느린 배달)
  • WebClient = 빠른 퀵서비스 (논블로킹)
  • RestClient = 모던 우편 (빠르고 깔끔)

RestClient — Spring 6.1+ 표준

의존성

spring-boot-starter-web 에 포함. 추가 의존성 X.

빌더 — 빈 등록 표준

@Configuration
public class HttpClientConfig {

    @Bean
    public RestClient paymentRestClient() {
        return RestClient.builder()
                .baseUrl("https://payment.example.com/api")
                .defaultHeader("Authorization", "Bearer " + apiKey)
                .defaultHeader("Content-Type", "application/json")
                .build();
    }
}

GET — 단건 조회

@Service
@RequiredArgsConstructor
public class PaymentClient {

    private final RestClient paymentRestClient;

    public Payment getPayment(String id) {
        return paymentRestClient.get()
                .uri("/payments/{id}", id)
                .retrieve()
                .body(Payment.class);
    }
}

retrieve() = "응답을 받겠다", body(...) = "이 타입으로 역직렬화". 끝.

POST — 생성

public Payment createPayment(PaymentRequest req) {
    return paymentRestClient.post()
            .uri("/payments")
            .body(req)
            .retrieve()
            .body(Payment.class);
}

List 응답

public List<Payment> listPayments(Long userId) {
    return paymentRestClient.get()
            .uri("/payments?userId={id}", userId)
            .retrieve()
            .body(new ParameterizedTypeReference<List<Payment>>() {});
}

6편 제네릭 의 타입 소거 영향 — List<Payment>.class 직접 못 써서 ParameterizedTypeReference 우회.

헤더·쿼리 파라미터

public Payment getPayment(String id, String traceId) {
    return paymentRestClient.get()
            .uri(uriBuilder -> uriBuilder
                    .path("/payments/{id}")
                    .queryParam("includeDetail", true)
                    .build(id))
            .header("X-Trace-Id", traceId)
            .retrieve()
            .body(Payment.class);
}

응답 상태 코드 처리

public Payment getPayment(String id) {
    return paymentRestClient.get()
            .uri("/payments/{id}", id)
            .retrieve()
            .onStatus(status -> status.value() == 404, (req, res) -> {
                throw new PaymentNotFoundException(id);
            })
            .onStatus(HttpStatusCode::is5xxServerError, (req, res) -> {
                throw new PaymentServerException();
            })
            .body(Payment.class);
}

.onStatus(predicate, handler) 패턴 — HTTP 코드별 예외 매핑.

WebClient — 비동기·논블로킹

비동기 호출·스트리밍이 필요할 때.

의존성

implementation 'org.springframework.boot:spring-boot-starter-webflux'

WebFlux 의존성. 기존 MVC 프로젝트에 WebClient만 쓰는 것도 OK — WebFlux를 "클라이언트로만" 사용.

사용

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient userWebClient() {
        return WebClient.builder()
                .baseUrl("https://user-api.example.com")
                .defaultHeader("Authorization", "Bearer " + apiKey)
                .build();
    }
}

@Service
@RequiredArgsConstructor
public class UserClient {

    private final WebClient userWebClient;

    // 비동기 — Mono<T> 반환
    public Mono<User> getUserAsync(Long id) {
        return userWebClient.get()
                .uri("/users/{id}", id)
                .retrieve()
                .bodyToMono(User.class);
    }

    // 동기 — block() 호출
    public User getUserSync(Long id) {
        return userWebClient.get()
                .uri("/users/{id}", id)
                .retrieve()
                .bodyToMono(User.class)
                .block();                    // ← 동기 변환 (블로킹)
    }
}

Mono<T> = "비동기 한 개", Flux<T> = "비동기 스트림" (WebFlux 의 핵심 타입). 39편 CompletableFuture 와 비슷한 비동기 결과 객체지만 — 더 풍부한 연산자.

RestClient vs WebClient — 어느 거?

RestClient WebClient
스타일 동기 비동기 (block 가능)
필요 의존성 spring-web spring-webflux
학습 곡선 낮음 (RestTemplate 사용자에게 친숙) Mono·Flux 학습 필요
추천 시점 일반적인 동기 호출 스트리밍·논블로킹 필수
Spring 버전 6.1+ 5+

: 한국 회사 신규 프로젝트 동기 호출 99% = RestClient. 비동기·스트리밍·고성능 필수만 WebClient.

외부 API 병렬 호출 — RestClient + CompletableFuture

RestClient는 동기지만 — 39편 CompletableFuture 와 조합하면 병렬 호출.

@Service
@RequiredArgsConstructor
public class DashboardService {

    private final UserClient userClient;
    private final OrderClient orderClient;
    private final PaymentClient paymentClient;

    public Dashboard fetch(Long userId) {
        CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() ->
                userClient.getUser(userId));
        CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() ->
                orderClient.listOrders(userId));
        CompletableFuture<List<Payment>> paymentFuture = CompletableFuture.supplyAsync(() ->
                paymentClient.listPayments(userId));

        CompletableFuture.allOf(userFuture, orderFuture, paymentFuture).join();

        return new Dashboard(userFuture.join(), orderFuture.join(), paymentFuture.join());
    }
}

세 외부 API를 병렬로 호출 — 응답 시간이 "가장 느린 API + α" 로 압축.

타임아웃 — 운영 필수

외부 API가 응답 안 하면 — 우리 백엔드 스레드가 영원히 대기. 타임아웃 반드시 설정.

@Bean
public RestClient paymentRestClient() {
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setConnectTimeout(Duration.ofSeconds(3));    // 연결 3초
    factory.setReadTimeout(Duration.ofSeconds(10));      // 응답 10초

    return RestClient.builder()
            .baseUrl("https://payment.example.com/api")
            .requestFactory(factory)
            .build();
}

WebClient 도 비슷 — Netty HttpClient 설정.

재시도 — Spring Retry

외부 API가 일시 실패할 때 — 자동 재시도.

implementation 'org.springframework.retry:spring-retry'
implementation 'org.springframework:spring-aspects'
@SpringBootApplication
@EnableRetry
public class Application { ... }

@Service
public class PaymentClient {

    @Retryable(
        retryFor = { ResourceAccessException.class },
        maxAttempts = 3,
        backoff = @Backoff(delay = 500))
    public Payment getPayment(String id) {
        return paymentRestClient.get()...;
    }

    @Recover
    public Payment recover(ResourceAccessException e, String id) {
        log.error("결제 조회 최종 실패: {}", id, e);
        return null;
    }
}

@Retryable + @Recover — 최대 3회 시도, 500ms 간격. 다 실패하면 @Recover 메서드 호출.

함정 5가지

(1) RestTemplate 신규 사용

// ❌ Spring 6+ 신규 프로젝트엔 X
RestTemplate rt = new RestTemplate();

// ✅ RestClient 사용
RestClient rc = RestClient.create();

(2) String 응답 타입에 매번 body(String.class)

JSON 응답을 자주 String으로 받아 직접 파싱 — 안티패턴. DTO로 정의해서 Jackson에 맡기기.

(3) 타임아웃 없음

기본 타임아웃 = 무한대. 외부 API 죽으면 우리 백엔드도 같이 죽음. 반드시 설정.

(4) 동기 호출을 직렬로

세 API 직렬 호출 = 합쳐서 5초. 병렬로 호출하면 2초. 39편 패턴 활용.

(5) 4xx·5xx 무시

.retrieve() 는 4xx·5xx에 기본 예외 던짐 (RestClientResponseException). 단 — 비즈니스 의미 있는 코드 (404 = 자원 없음) 는 명시적으로 매핑.

🎯 한국 회사 표준

Spring 6.1+ 신규 = RestClient. 비동기·스트리밍 = WebClient. RestTemplate은 신규 사용 X. 타임아웃·재시도·예외 매핑은 운영 필수.

한 줄 정리 — Spring 6.1+ HTTP 클라이언트 표준 = RestClient(동기) + WebClient(비동기). RestTemplate은 유지보수 모드. 타임아웃·재시도·외부 API 병렬 호출 패턴 필수.

시험 직전 한 번 더 — WebClient/RestClient 입문자가 매번 헷갈리는 것

  • 세 세대 = RestTemplate(1세대) → WebClient(2세대) → RestClient(3세대)
  • RestClient = Spring 6.1+ 동기 표준
  • WebClient = Spring 5+ 비동기 표준 (Mono·Flux)
  • RestTemplate = 유지보수 모드 (신규 X)
  • 의존성 = RestClient는 spring-web, WebClient는 spring-webflux
  • 빌더 패턴 = RestClient.builder().baseUrl(...).defaultHeader(...).build()
  • 빈으로 등록해서 재사용 (생성 비용 큼)
  • retrieve() = 응답 받겠다
  • body(T.class) = 역직렬화
  • body(new ParameterizedTypeReference<List<T>>(){}) = 제네릭 타입
  • .onStatus(predicate, handler) = 코드별 예외 매핑
  • .uri(builder -> ...) = 쿼리 파라미터
  • WebClient 동기 변환 = .block() (블로킹 — 운영 신중)
  • Mono = 비동기 한 개, Flux = 비동기 스트림
  • 타임아웃 설정 필수 — 무한 대기 방지 (3초·10초 표준)
  • 재시도 = Spring Retry @Retryable + @Recover
  • 외부 API 병렬 호출 = RestClient + CompletableFuture.supplyAsync (한국 회사 표준)
  • 4xx·5xx 자동 예외 (RestClientResponseException)
  • 404 등 비즈니스 의미 코드 = 명시적 매핑
  • DTO 응답 받기 — String 받아 직접 파싱 X
  • 자바 백엔드 = 외부 API 매일 호출

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!