WebClient를 위한 Spring WebFlux 기초

최재혁·2022년 11월 26일
2

WebClient

목록 보기
1/3
post-thumbnail

들어가기 앞서

제가 이 포스팅을 작성하는 이유는 제 토이 프로젝트에서 WebClient를 사용한 외부 API 호출을 계획하고 있는데, 그 전에 기본적인 이해를 바탕에 두고 가기 위함입니다. 방대한 내용을 전부 다룰 수는 없고, WebClient를 사용하기 위한 기본적인 지식만 정리할 것입니다. 공식 문서에서도 Spring WebFlux는 러닝 커브가 높다고 언급하고 있습니다. 하지만 또한, Spring WebFlux로의 전환을 고려하기에 앞서 WebClient의 사용을 고려하는 것을 추천하고 있습니다. 진입 장벽이 낮은 API부터 사용해보면서, 더 깊은 이해를 추구하고자 합니다.

다음 내용은 스프링 리액티브 웹 스택 공식 레퍼런스를 직접 번역하신

토리맘의 한글라이즈 프로젝트를 기반으로 제가 이해한 바를 요약하고, 여러 자료를 통해 내용을 조금 보충한 것입니다.


Spring WebFlux란?

Spring 5.0 에 추가된, Non-Blocking으로 동작하는 "Reactive 스택 웹 프레임워크"이다.

보다 적은 쓰레드로 동시 처리를 제어하고, 적은 하드웨어 리소스로 확장하기 위해 Non-Blocking 방식의 웹 스택이 필요하였다. 기존 스프링 MVC의 서블릿으로 Non-Blocking을 구현하려면, 동기-Blocking 방식으로 동작하고 있는 API (Filter, Servlet, getParameter 등) 를 사용하기 힘들었다. 또한 Netty와 같은 비동기-논블로킹 방식의 서버를 위해서라도 새로운 공통 API의 필요성이 대두되었다.

그럼 Reactive(리액티브)란?

리액티브란 변화에 반응하는 것을 중점으로 만든 프로그래밍 모델이다. Non-Blocking 방식은 작업을 기다리는 것이 아닌, 완료될 때, 또는 데이터를 사용할 수 있을 때 반응하므로 하나의 리액티브라고할 수 있다.

스프링이 제공하는 리액티브 매커니즘에서 논블로킹 백프레셔(Non-Blocking back pressure)가 있다. Reactive Streams는 논블로킹 백프레셔를 통한 비동기 스트림 처리 표준을 제공하기 위한 명세이다. Reactive Streams를 쓰는 주 목적은 SubscriberPublisher의 데이터 생산 속도를 제어하는 것이다.

Blocking 방식에서 작업이 끝날 때까지 호출자를 강제로 기다리게 하는 것은 일종의 블로킹 백프레셔입니다.

Non-Blocking 방식에서는 강제로 기다리게 하는 것이 아닌, Producer의 속도가 Consumer의 속도를 압도하지 않도록 이벤트 속도를 제어합니다. 이것이 논블로킹 백프레셔의 쉬운 설명입니다.


Reactive Streams 구성

Reactive Streams에 대해 잘 설명해주신 LINE 블로그

위에 언급했다시피, Reactive Streams는 "명세"이다. Reactive Streams는 3개의 구성 요소로 정의된다.

public interface Publisher<T> {	 
   public void subscribe(Subscriber<? super T> s);
}
 
public interface Subscriber<T> {
   public void onSubscribe(Subscription s);
   public void onNext(T t);
   public void onError(Throwable t);
   public void onComplete();
}
 
public interface Subscription {
   public void request(long n);
   public void cancel();
}
  • Publisher에는 Subscriber의 구독을 받기 위한 subscribe API가 존재한다.
  • Subscriber는 다음과 같은 4개의 API로 구성된다.
    • 받은 데이터를 처리하기 위한 onNext
    • 에러를 처리하는 onError
    • 작업 완료 시 사용하는 onComplete
    • 매개 변수로 Subscription을 받는 onSubscribe
  • Subscription은 다음과 같은 2개의 API로 구성된다.
    • n개의 데이터를 요청하기 위한 request
    • 구독을 취소하기 위한 cancel

Reactive Streams에서 위 API 실행 흐름은 다음과 같다.

  1. Subscribersubscribe 함수를 사용하여 Publisher에게 구독을 요청
  2. PublisheronSubscribe 함수를 사용하여 Subscriber에게 Subscription을 전달
  3. SubscriptionSubscriberPublisher 간 통신의 매개체로서 동작함. 즉, SubscriberPublisher에게 직접 데이터 요청을 하지 않음. Subscriptionrequest 함수를 통해 Publisher에게 전달
  4. PublisherSubscription을 통해 SubscriberonNext에 데이터를 전달하고, 작업이 완료되면 onComplete, 에러가 발생하면 onError 시그널을 전달
  5. SubscriberPublisher, Subscription이 서로 유기적으로 연결되어 통신을 주고받으면서 subscribe부터 onComplete까지 연결되고, 이를 통해 백 프레셔가 완성

Reactor

하지만 Reactive Streams는 실제 어플리케이션에서 사용하기에는 너무 저수준(Low-level)이다. Reative Library는 비동기 로직을 어플리케이션에서 구현하기 위한 고수준의 함수형 API를 제공한다.

Spring WebFlux에서는 Reactor를 Reactive Library로 사용한다. Reactor는 ReactiveX 연산자 소개에 정리된 연산자들을 통해서, 데이터 시퀀스가 0~1개일 때는 Mono, 0~N개일 때는 Flux로 표현한다. Reactor는 Reactive Streams Library이기 때문에, 논블로킹 백프레셔를 지원한다.

WebFlux는 Reactor 이외의 다른 Reactive Library를 사용해도, Reative Streams로 상호 작용이 가능하다. 순수한 Publisher를 입력으로 받아 내부적으로 Reactor 타입으로 변환하고, 이를 사용하여 Mono 또는 Flux로 반환하면 된다.

  • 즉, 입력으로는 어떤 Publisher도 가능하지만, 출력 형식은 맞춰줘야 한다.

Spring WebFlux의 서버

Spring WebFlux는 Tomcat, Jetty와 같은 서블릿 컨테이너부터, 서블릿 기반이 아닌 Netty, Undertow에서도 잘 동작한다. 추상화된 공통 API를 통해 모든 서버에 고수준의 프로그래밍 모델을 적용할 수 있다.

스프링 부트의 WebFlux starter를 통해 WebFlux로 어플리케이션을 실행할 수 있다(원래 WebFlux엔 서버 기동이나 중단을 위한 내장 기능은 없다). 기본 서버로 Netty를 사용하지만, dependency만 수정하면 톰캣, Jetty 등으로의 교체도 가능하다. 이 때, 서버를 서블릿 컨테이너 기반으로 교체한다면, 스프링MVC와 WebFlux 사이의 동작 방식이 다름에 주의해야 한다.


동시성 처리와 쓰레드 풀

Spring MVC의 경우에는 어플리케이션이 처리 중인 쓰레드가 잠시 중단될 수 있다(외부 서비스, 네트워크 I/O 등). 즉,Blocking이 발생한다. 따라서 서블릿 컨테이너는 Blocking에 대비해 큰 쓰레드 풀로 요청을 처리한다.

Spring WebFlux는 실행 중인 쓰레드가 중단되지 않는다는 전제가 있다. 따라서 Non-Blocking 서버는 작은 쓰레드 풀을 고정해놓고 요청을 처리한다.


성능에 대한 오해

Reactive와 Non-Blocking을 사용한다고 해서 어플리케이션이 바로 빨라지는 것은 아니다. 물론 WebClient를 사용하여 외부 서비스 호출을 병렬적으로 처리한다면 빨라질 수도 있다. 전반적으로 보면 Non-Blocking 방식이 처리할 일이 더 많다 보니, 처리 시간이 약간 더 길어질 수도 있다.

Reactive와 Non-Blocking의 주된 이점은 고정된 적은 쓰레드와 적은 메모리로도 확장할 수 있다는 것이다. 예측 가능한 방법으로 확장하기 때문에, 부하 속에서도 어플리케이션의 복원 능력은 더 좋아진다.

profile
잘못된 고민은 없습니다

0개의 댓글