WebClient

김기현·2025년 8월 3일

Spring WebFlux

목록 보기
14/28

스프링 WebFlux의 WebClient는 비동기적이고 논블로킹 방식으로 HTTP 요청을 보내기 위한 리액티브 클라이언트이다.
기존 스프링 MVC에서 주로 사용되던 RestTemplate의 리액티브 버전이라고 할 수 있다.

WebClient는 논블로킹 I/O를 기반으로 하여 대규모 동시 요청 처리 및 스트리밍 데이터 처리에 강점을 가진다. Reactor(Mono, Flux)를 사용하여 비동기 결과를 처리하며 백프레셔를 지원한다.

WebClient를 사용하는 이유

특징RestTemplateWebClient
스레드 모델블로킹논블로킹
I/O 모델요청당 스레드 모델. 동시 요청 증가 시 스레드 오버헤드 증가이벤트 루프 기반. 적은 스레드로도 높은 동시성 처리 가능
프로그래밍명령형(Imperative)리액티브(Reactive) - Mono, Flux 반환
백프레셔지원 안 함지원함 - 생산자/소비자 속도 조절 가능
사용처기존 동기식 애플리케이션, 비교적 낮은 동시성 요구리액티브 스택 애플리케이션, 높은 동시성, 스트리밍 데이터, 마이크로서비스 간 통신
에러 처리try-catch및 예외 발생리액티브 스트림 내에서 onErrorResume, onErrorMap등 활용

WebClient의 주요 기능 및 특징

  1. 리액티브 스트림즈 지원: Mono<T>Flux<T>를 사용하여 비동기적인 단일 값 또는 다중 값 스트림을 처리한다.
  2. 논블로킹 I/O: Netty, Reactor-Netty, Undertow 등 다양한 논블로킹 HTTP 클라이언트 라이브러리 위에서 동작한다.
  3. 유연한 API: 빌더 패턴을 사용하여 HTTP 요청을 구성하고 전송하는 매우 유연하고 읽기 쉬운 API를 제공한다.
  4. 필터 지원: 요청/응답을 가로채서 로깅, 인증 토큰 추가, 에러 처리 등을 할 수 있는 필터 메커니즘을 제공한다.
  5. 오류 처리: 리액티브 스트림의 오류 연산자(onErrorResume, onErrorMap 등)를 통해 섬세한 오류 처리가 가능하다.
  6. 폼 데이터, 멀티 파트 데이터 지원: 다양한 형태의 요청 바디 전송을 지원한다.
  7. HTTP/2 지원: 최신 HTTP/2 프로토콜을 지원하여 성능 향상에 기여할 수 있다.
    • 이는 기반 클라이언트 라이브러리에 따라 다르다.

WebClient의 내부 동작 원리

스프링 WebClient는 겉으로는 간단한 빌더 패턴과 리액티브 스트림 API를 제공하지만 내부에는 복잡한 논블로킹 HTTP 통신 메커니즘이 작동한다.
핵심적인 내부 동작 원리는 다음과 같은 요소들의 상호작용으로 이루어진다.

1. 클라이언트 커넥터 (Client Connector)

WebClient의 가장 중요한 내부 구성 요소 중 하나는 클라이언트 커넥터(ClientHttpConnector)이다.
WebClient 자체는 고수준의 API를 제공하며 실제 HTTP 통신(소켓 연결, 데이터 전송/수신)은 이 커넥터에게 위임한다.

추상화 계층

ClientHttpConnectorWebClient가 특정 HTTP 클라이언트 구현체에 종속되지 않고 유연하게 작동할 수 있도록 하는 추상화 계층이다.

public interface ClientHttpConnector {
    Mono<ClientHttpResponse> connect(
            HttpMethod method,
            URI uri,
            Function<? super ClientHttpRequest, Mono<Void>> requestCallback
    );
}

주요 구현체

  • Reactor Netty(ReactorClientHttpConnector): 스프링 WebFlux의 기본 커넥터이자 가장 일반적으로 사용되는 구현체이다. Netty는 이벤트 루프 기반의 고성능 논블로킹
    네트워크 프레임워크이다.
  • Undertow(UndertowClientHttpConnector): 또 다른 논블로킹 웹 서버인 Undertow를 기반으로 한다.
  • JDK 11 HTTP Client(JdkClientHttpConnector): Java 11에 도입된 표준 HTTP 클라이언트이다.
  • Apache HttpComponents(ApacheHttpClient5ClientHttpConnector): Apache HttpClient 5를 기반으로 하며 블로킹이지만 비동기 래터를 제공한다. 일반적으로
    선호되지 않는다.

역할

  • 커넥터는 네트워크 구준에서 소켓 연결을 관리하고, HTTP 요청을 인코딩하여 전송하며, HTTP 응답을 디코딩하여 WebClient에게 전달하는 저수준 작업을 담당한다.
  • 이 과정에서 커넥터는 논블로킹 I/O이벤트 루프 모델을 활용한다.

2. 이벤트 루프 (Event Loop) 기반 논블로킹 I/O

WebClient가 논블로킹 특성을 갖는 핵심적인 이유이다. 특히 Reactor Netty 커넥터의 경우 Netty의 이벤트 루프 모델을 그대로 활용한다.

적은 수의 스레드

  • 전톡적인 블로킹 I/O 방식(ex: ServerSocket을 직접 사용하는 경우)은 각 클라이언트 연결마다 별도의 스레드를 할당하여 I/O 작업이 완료될 때까지 해당 스레드를 블로킹한다.
  • 반면 Netty는 적은 수의 이벤트 루프 스레드(일반적으로 CPU 코어 수와 비슷하다)를 사용하여 모든 I/O 이벤트를 처리한다.

I/O 다중화 (Multiplexing)

  • 이벤트루프 스레드는 여러 네트워크 소켓을 동시에 모니터링한다.
  • 특정 소켓에서 읽을 데이터가 있거나 쓸 수 있는 상태가 되면 (I/O 이벤트 발생), 이벤트 루프는 해당 이벤트를 감지하고 관련 작업을 처리한다.

논블로킹

  • 데이터 I/O 작업 시 데이터가 아직 준비되지 않았거나 버퍼가 가득 찬 경우 스레드를 블로킹하지 않고 다른 소켓의 이벤트를 처리하러 간다.
  • I/O 작업이 완료되면 운영체제로부터 알림(셀렉터 기반)을 받고 다시 해당 작업을 이어서 처리한다.

비동기 콜백

  • I/O 작업 결과는 즉시 반환되지 않고, 작업이 완료되었을 때 등록된 콜백(Future, Promise, Mono/Flux의 구독자)으로 전달된다.

3. Reactor(Mono/Flux)와의 통합

WebClient는 스프링 WebFlux의 핵심인 Reactor 라이브러리의 MonoFlux를 사용하여 비동기 결과를 표현하고 처리한다.

비동기 결과 캡슐화

  • WebClient의 모든 요청 메소드(retrieve(), exchangeToMono(), bodyToMono() 등)는 Mono또는 Flux를 반환한다.
  • 이는 HTTP 요청이 즉시 완료되지 않고, 미래의 어느 시점에 결과가 나올 것임을 나타낸다.

스트림 처리

  • Mono<T>: 단일 응답 객체를 비동기적으로 받을 때 사용한다.
  • Flux<T>: 여러 응답 객체를 비동기적으로 받을 때 사용한다.

연산자 체인

  • 개발자는 MonoFlux가 제공하는 연산자(map, filter, flatMap 등)을 사용하여 비동기 결과 스트림을 조합, 변환, 필터링, 에러 처리를 할 수 있다.

구독 (Subscription)

  • MonoFlux는 콜 스트림이다. 즉, subscribe()메소드가 호출되기 전까지는 실제 HTTP 요청이 시작되지 않는다.
  • subscribe()가 호출되면 비동기 요청이 트리거되고, 결과가 준비될 때 구독자에게 전달된다.
    • WebFlux 컨트롤러에서 Mono/Flux를 반환하면 프레임워크가 알아서 구독을 처리한다.

4. Exchange Functions 및 Filters

WebClient는 HTTP 요청-응답 처리 파이프라인을 ExchangeFunctionExchangeFilterFunction으로 추상화한다.

ExchangeFunction


@FunctionalInterface
public interface ExchangeFunction {
    Mono<ClientResponse> exchange(ClientRequest request);

    default ExchangeFunction filter(ExchangeFilterFunction filter) {
        return filter.apply(this);
    }
}
  • 실제 HTTP 요청을 실행하고 Mono<ClientResponse>를 반환하는 핵심 논리를 캡슐화한다.
  • 내부적으로는 ClientHttpConnector를 사용하여 네트워크 통신을 수행한다.
  • WebClient의 요청 파이프라인의 가장 마지막에 위치하며, 모든 필터가 적용된 후 최종적으로 호출된다.

ExchangeFilterFunction


@FunctionalInterface
public interface ExchangeFilterFunction {
    Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next);

    default ExchangeFilterFunction andThen(ExchangeFilterFunction afterFilter) {
        Assert.notNull(afterFilter, "ExchangeFilterFunction must not be null");
        return (request, next) -> {
            return this.filter(request, (afterRequest) -> {
                return afterFilter.filter(afterRequest, next);
            });
        };
    }

    default ExchangeFunction apply(ExchangeFunction exchange) {
        Assert.notNull(exchange, "ExchangeFunction must not be null");
        return (request) -> {
            return this.filter(request, exchange);
        };
    }

    static ExchangeFilterFunction ofRequestProcessor(Function<ClientRequest, Mono<ClientRequest>> processor) {
        Assert.notNull(processor, "ClientRequest Function must not be null");
        return (request, next) -> {
            Mono var10000 = (Mono) processor.apply(request);
            Objects.requireNonNull(next);
            return var10000.flatMap(next::exchange);
        };
    }

    static ExchangeFilterFunction ofResponseProcessor(Function<ClientResponse, Mono<ClientResponse>> processor) {
        Assert.notNull(processor, "ClientResponse Function must not be null");
        return (request, next) -> {
            return next.exchange(request).flatMap(processor);
        };
    }
}
  • HTTP 요청을 보내기 전이나 응답을 받자마자 가로채서 추가적인 작업을 수행하는 필터이다.
  • 여러 필터를 체인으로 연결하여 요청-응답 파이프라인을 구성할 수 있다.
  • 주요 활용
    • 인증: Authorization헤더 추가
    • 로깅: 요청/응답 정보 로깅
    • 재시도: 특정 오류 발생 시 요청 재시도
    • 메트릭 수집: 요청 처리 시간, 성공/실패율 등 메트릭 수집
    • 공통 헤더/파라미터 추가: 모든 요청에 특정 헤더나 쿼리 파라미터 자동 추가
  • 필터는 요청을 변경하거나, 응답을 변경하거나, 심지어 요청을 아예 보내지 않고 응답을 증시 반환할 수도 있다.

WebClient 요청 흐름 요약

  1. WebClient.builder()/create(): WebClient인스턴스 생성. 이때 사용할 ClientHttpConnectorExchangeFilterFunction체인이 구성된다.
  2. webClient.method(...).uri(...).header(...).body(...)개발자가 HTTP 요청을 빌드한다. 이 과정에서 요청 정보(메소드, URI, 헤더, 바디 등)가 내부적으로
    ClientRequest객체로 준비된다.
  3. retrieve()또는 exchangeToMono()호출: 요청 실행을 트리거한다.
  4. ExchangeFilterFunction체인 실행: ClientRequest객체가 필터 체인을 통과하면서 각 필터가 요청을 수정하거나 로직을 수행한다.
  5. ExchangeFunction호출: 필터 체인을 통과한 ClientRequest는 최종적으로 ExchangeFunction에 전달된다.
  6. ClientHttpConnector호출: ExchangeFunction은 구성된 ClientHttpConnector를 톨해 실제 네트워크 통신을 요청한다.
  7. 논블로킹 I/O 및 이벤트 루프: 커넥터는 Netty 등의 논블로킹 라이브러리를 사용하여 HTTP 요청을 네트워크로 전송하고, 응답이 올 때까지 스레드를 블로킹하지 않고 이벤트를 기다린다.
  8. 비동기 응답 처리: 응답이 도착하면 커넥터는 이를 ClientResponse객체로 변환하고 Mono<ClientResponse>형태로 ExchangeFunction에 반환하나.
  9. Mono/Flux발행: ClientResponse는 다시 필터 체인을 역방향으로 통과하며 필요에 따라 수정될 수 있다. 최종적으로 retrieve()exchangeToMono()호출
    지점으로Mono/Flux형태로 응답 데이터가 발행된다.
  10. 구독자에게 결과 전달: 개발자가 subscribe()를 호출한 경우 또는 WebFlux 컨트롤러가 반환한 경우 Mono/Flux의 최종 결과가 구독자에게 전달된다.

WebClient의 주요 디자인 원칙

1. 논블로킹 및 비동기 우선주의

핵심 원칙

  • WebClient는 기본적으로 모든 I/O 작업을 논블로킹 방식으로 처리한다.
  • 이는 호출 스레드가 네트워크 I/O 작업(ex: 데이터를 보내거나 받는 중)을 기다리며 블로킹되지 않고 다른 작업을 수행할 수 있도록 해준다.

이점

  • 적은 수의 스레드로도 높은 동시성을 처리할 수 있어 서버 자원(CPU 및 메모리)을 효율적으로 사용하고 확장성을 극대화할 수 있다.

Reactor 통합

  • 모든 API는 Mono또는 Flux를 반환하여 결과가 비동기적으로 도달할 것임을 명시한다.
  • 개발자는 리액티브 스트림의 연산자를 활용하여 데이터 흐름을 제어한다.

2. 불변성 (Immutability)

설계

  • WebClient인스턴스 자체와 이를 통해 구성되는 요청 객체(ClientRequest)는 불변(immutable)하다.
  • WebClient.builder()를 통해 인스턴스를 생성하거나, .get(), .uri(), .header()등의 메소드를 호출할 때마다 내부적으로 새로운 중간 객체 또는 최종 객체가 생성되어
    반환된다.

이점

  • 스레드 안정성(Thread Safety)을 보장한다.
  • 여러 스레드가 동일한 WebClient인스턴스를 공유하거나 동시에 요청을 빌드하더라도 서로의 상태에 영향을 주지 않는다.
  • 이는 복잡한 동시성 문제를 방지하는 데 도움이 된다.

3. 확장성(Extensibility) 및 커스터마이징 용이성

빌더 패턴

  • WebClient.builder()를 통해 baseUrl, 기본 헤더/쿠키, 필터 등을 설정하여 다양한 시나리오에 맞는 WebClient인스턴스를 유연하게 생성할 수 있다.

ExchangeFilterFunction

  • 요청-응답 처리 파이프라인에 사용자 정의 로직(인증, 로깅, 재시도, 메트릭 수집 등)을 쉽게 삽입할 수 있는 확장 메커니즘을 제공한다.
  • 이는 Aspect-Oriented Programming (AOP)의 개념과 유사하게 동작하며 핵심 비즈니스 로직과 횡단 관심사(cross-cutting concerns)를 분리할 수 있게 한다.

ClientHttpConnector

  • 저수준 HTTP 클라이언트 (Netty, Undertow 등)를 선택적으로 사용할 수 있도록 추상화하여, 필요에 따라 기반 기술을 변경하거나 특정 네트워크 환경에 최적화할 수 있다.

4. 백프레셔(Backpressure) 지원

개념

  • WebClient는 리액티브 스트림즈 표준을 준수하여 백프레셔를 지원한다.
  • 이는 데이터 생산자가 소비자의 처리 속도에 맞춰 데이터를 전송하도록 조절하는 메커니즘이다.

이점

  • 소비자가 데이터를 처리할 준비가 되지 않았을 때 생산자가 데이터를 과도하게 푸쉬하여 발생하는 시스템 과부하를 방지한다.
  • 스트리밍 API, SSE(Server-Sent Events) 등에서 중요하다.

WebClient의 권장 사용 패턴

위에서 설명한 디자인 원칙들을 바탕으로 WebClient를 가장 효과적으로 사용하기 위한 몇 가지 권장 패턴이 있다.

1. WebClient 인스턴스는 한 번 생성하여 재사용 (싱글톤 또는 빈으로 등록)

이유

  • WebClient인스턴스를 생성하는 것은 내부적으로 커넥션 풀 초기화, 필터 체인 구성 등 비용이 드는 작업이다.
  • 매 요청마다 새로운 인스턴스를 생성하면 이러한 오버헤드가 반복되어 성능 저하와 자원 낭비를 유발한다.
  • 또한 내부적으로 관리되는 커넥션 풀을 효율적으로 사용할 수 있다.

방법

  • 스프링 부트 애플리케이션에서 @Configuration클래스에서 @Bean으로 WebClient인스턴스를 정의한다.
  • 그리고 다른 컴포넌트에서 이를 주입받아 사용하는 것이 일반적이다.
  • WebClient.Builder빈을 활용하면 더욱 편리하다.

@Configuration
public class WebClientConfig {

    // WebClient.Builder는 Spring Boot가 자동으로 제공한다
    @Bean
    public WebClient userApiWebClient(WebClient.Builder builder) {
        return builder
                .baseUrl("http://localhost:8080/api/users")
                .defaultHeader("Accept", "application/json")
                // .filter(...) 추가 필터 설정 가능
                .build();
    }

    @Bean
    public WebClient productApiWebClient(WebClient.Builder builder) {
        return builder
                .baseUrl("http://localhost:8080/api/products")
                // 각 API별로 다른 설정 가능
                .build();
    }
}

// 아래처럼 주입받아 사용하면 된다
@Service
public class WebClientUserService {
    private final WebClient userApiWebClient;

    public WebClientUserService(@Qualifier("userApiWebClient") WebClient userApiWebClient) {
        this.userApiWebClient = userApiWebClient;
    }

    public Mono<SecurityProperties.User> getUserDetails(String id) {
        return userApiWebClient.get()
                .uri("/users/{id}", id)
                .retrieve()
                .bodyToMono(SecurityProperties.User.class);
    }
}

2. retrieve()와 exchangeToMono()/exchangeToFlux() 사용

retrieve()

  • 2xx 성공 응답만 관심 있고 4xx/5xx 응답은 예외로 처리하려는 경우에 가장 간단하고 선호되는 방법이다.
  • 대부분의 REST API 호출에 적합하다.

exchangeToMono()/exchangeToFlux()

  • HTTP 응답 상태 코드에 따라 직접 세밀한 제어(ex: 404에 대한 특정 대체 로직, 500에 대한 재시도 로직 등)가 필요한 경우에 사용한다.
  • 이 방법을 사용하면 응답을 완전히 소비하는 책임을 WebClient가 가지므로 exchange()의 메모리 누수 위험이 없다.

3. 필터(ExchangeFilterFunction)를 활용한 횡단 관심사 처리

  • 인증 토큰 추가, 요청/응답 로깅, 에러 발생 시 재시도 로직, 메트릭 수집 등 애플리케이션 전반에 걸쳐 공통적으로 적용해야 하는 기능들을 필터로 구현하여 WebClient.Builder에 추가하는 것이
    좋다.
  • 장점: 핵심 비즈니스 로직과 분리되어 코드가 깔끔해지고, 재사용성이 높아지며, 테스트하기 용이하다.

@Configuration
public class WebClientConfig {
    // 인증 토큰을 모든 요청에 추가하는 필터
    @Bean
    public WebClient authenticatedWebClient(WebClient.Builder builder) {
        return builder
                .baseUrl("http://localhost:8080")
                .filter((request, next) -> {
                    // 여기서는 간단히 하드코딩된 토큰 사용. 실제로는 보안 컨텍스트 등에서 가져옴
                    ClientRequest filteredRequest = ClientRequest.from(request)
                            .header("Authorization", "Bearer my_secret_token")
                            .build();
                    return next.exchange(filteredRequest);
                })
                .build();
    }
}

4. 절대 block()을 사용하지 않도록 하기

블로킹 피하기

  • Mono.block()이나 Flux.blockLast()같은 블로킹 연산자는 리액티브 스트림의 비동기 장점을 완전히 상쇄시키며, WebFlux 애플리케이션 내에서 사용될 경우 데드락, 스레드 고갈 등의
    심각한 성능 및 안정성 문제를 발생시킬 수 있다.

예외

  • 극히 드물게 레거시 코드와의 통합 또는 애플리케이션 종료 시점에 리소스 정리와 같은 정말 블로킹이 필요한 특정 상황에서만 신중하게 사용해야 한다.

대안

  • flatMap, zip, combineLatest등 리액티브 연산자를 사용하여 비동기 결과를 조합하고 변환해야 한다.
  • 컨트롤러 메소드의 반환 타입도 MonoFlux로 유지하여 프레임워크가 구독을 처리하도록 한다.

5. 오류 처리의 명시적 정의

  • 리액티브 스트림은 try-catch블록과는 다른 방식의 오류 처리 메커니즘을 제공한다.
    • onErrorResume, onErrorMap, retry, doOnError등의 연산자를 활용하여 스트림 내에서 발생하는 오류를 복구, 변환, 재시도, 로깅할 수 있다.
  • API 호출의 성공/실패 시나리오를 명확히 정의하고, 각 경우에 대한 오류 처리 전략을 수립하는 것이 중요하다.
profile
백엔드 개발자를 목표로 공부하는 대학생

0개의 댓글