제가 이 포스팅을 작성하는 이유는 제 토이 프로젝트에서 WebClient
를 사용한 외부 API 호출을 계획하고 있는데, 그 전에 기본적인 이해를 바탕에 두고 가기 위함입니다. 방대한 내용을 전부 다룰 수는 없고, WebClient를 사용하기 위한 기본적인 지식만 정리할 것입니다. 공식 문서에서도 Spring WebFlux
는 러닝 커브가 높다고 언급하고 있습니다. 하지만 또한, Spring WebFlux로의 전환을 고려하기에 앞서 WebClient의 사용을 고려하는 것을 추천하고 있습니다. 진입 장벽이 낮은 API부터 사용해보면서, 더 깊은 이해를 추구하고자 합니다.
다음 내용은 스프링 리액티브 웹 스택 공식 레퍼런스를 직접 번역하신
토리맘의 한글라이즈 프로젝트를 기반으로 제가 이해한 바를 요약하고, 여러 자료를 통해 내용을 조금 보충한 것입니다.
Spring 5.0 에 추가된, Non-Blocking으로 동작하는 "Reactive 스택 웹 프레임워크"이다.
보다 적은 쓰레드로 동시 처리를 제어하고, 적은 하드웨어 리소스로 확장하기 위해 Non-Blocking 방식의 웹 스택이 필요하였다. 기존 스프링 MVC의 서블릿으로 Non-Blocking을 구현하려면, 동기-Blocking 방식으로 동작하고 있는 API (Filter
, Servlet
, getParameter
등) 를 사용하기 힘들었다. 또한 Netty
와 같은 비동기-논블로킹 방식의 서버를 위해서라도 새로운 공통 API의 필요성이 대두되었다.
리액티브란 변화에 반응하는 것을 중점으로 만든 프로그래밍 모델이다. Non-Blocking 방식은 작업을 기다리는 것이 아닌, 완료될 때, 또는 데이터를 사용할 수 있을 때 반응하므로 하나의 리액티브라고할 수 있다.
스프링이 제공하는 리액티브 매커니즘에서 논블로킹 백프레셔(Non-Blocking back pressure)가 있다. Reactive Streams
는 논블로킹 백프레셔를 통한 비동기 스트림 처리 표준을 제공하기 위한 명세이다. Reactive Streams
를 쓰는 주 목적은 Subscriber가 Publisher의 데이터 생산 속도를 제어하는 것이다.
Blocking 방식에서 작업이 끝날 때까지 호출자를 강제로 기다리게 하는 것은 일종의 블로킹 백프레셔입니다.
Non-Blocking 방식에서는 강제로 기다리게 하는 것이 아닌, Producer의 속도가 Consumer의 속도를 압도하지 않도록 이벤트 속도를 제어합니다. 이것이 논블로킹 백프레셔의 쉬운 설명입니다.
위에 언급했다시피, 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();
}
subscribe
API가 존재한다.onNext
onError
onComplete
onSubscribe
request
cancel
Reactive Streams
에서 위 API 실행 흐름은 다음과 같다.
subscribe
함수를 사용하여 Publisher에게 구독을 요청onSubscribe
함수를 사용하여 Subscriber에게 Subscription을 전달request
함수를 통해 Publisher에게 전달onNext
에 데이터를 전달하고, 작업이 완료되면 onComplete
, 에러가 발생하면 onError
시그널을 전달subscribe
부터 onComplete
까지 연결되고, 이를 통해 백 프레셔가 완성하지만 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는 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의 주된 이점은 고정된 적은 쓰레드와 적은 메모리로도 확장할 수 있다는 것이다. 예측 가능한 방법으로 확장하기 때문에, 부하 속에서도 어플리케이션의 복원 능력은 더 좋아진다.