study

Spring WebFlux

개요(Overview)

Spring WebFlux는 왜 만들어졌을까?

그 이유 중 하나는 소수의 스레드로 동시성을 처리하고 하드웨어 자원을 적게 사용하면서 확장할 수 있는 논블로킹 웹 스택이 필요했기 때문입니다. Servlet 논블로킹 I/O는 나머지 Servlet API에서 벗어나게 됩니다. 나머지 Servlet API는 동기적(Filter, Servlet)이거나 블로킹(getParameter, getPart) 방식이기 때문입니다. 이것이 모든 논블로킹 런타임에서 기반이 될 수 있는 새로운 공통 API를 만들게 된 동기입니다. 이는 Netty와 같이 비동기 및 논블로킹 환경에서 잘 정립된 서버들 때문에 중요합니다.

다른 이유는 함수형 프로그래밍입니다. Java 5에서 어노테이션이 추가되면서 주석 기반 REST 컨트롤러나 단위 테스트와 같은 기회가 생긴 것처럼, Java 8에서 람다 표현식이 추가되면서 Java에서 함수형 API를 사용할 기회가 생겼습니다. 이는 논블로킹 애플리케이션과 CompletableFutureReactiveX에서 인기 있는 연속형(continuation-style) API에서 비동기 로직을 선언적으로 구성할 수 있게 해줍니다. 프로그래밍 모델 수준에서, Java 8은 Spring WebFlux가 어노테이션 기반 컨트롤러와 함께 함수형 웹 엔드포인트를 제공할 수 있게 해주었습니다.

“Reactive” 정의

“논블로킹(non-blocking)”과 “함수형(functional)”을 언급했지만, reactive가 정확히 무엇을 의미할까?

“Reactive”라는 용어는 변화에 반응하는 프로그래밍 모델을 의미합니다. 예를 들어, 네트워크 컴포넌트가 I/O 이벤트에 반응하거나, UI 컨트롤러가 마우스 이벤트에 반응하는 식입니다. 이런 의미에서 논블로킹은 reactive입니다. 블로킹되는 대신, 연산이 완료되거나 데이터가 사용 가능해질 때 알림에 반응하는 방식이기 때문입니다.

Spring 팀에서 reactive와 연관된 또 다른 중요한 메커니즘은 논블로킹 백프레셔(non-blocking back pressure)입니다. 동기적 명령형 코드에서는 블로킹 호출이 자연스럽게 백프레셔 역할을 하여 호출자가 기다리게 합니다. 하지만 논블로킹 코드에서는 빠른 생산자가 대상(destination)을 압도하지 않도록 이벤트 속도를 제어하는 것이 중요합니다.

Reactive Streams는 백프레셔가 있는 비동기 컴포넌트 간의 상호작용을 정의하는 작은 사양으로, Java 9에서도 채택되었습니다. 예를 들어 데이터 저장소(Publisher 역할)가 데이터를 생성하면 HTTP 서버(Subscriber 역할)가 이를 응답으로 작성할 수 있습니다. Reactive Streams의 주요 목적은 구독자가 퍼블리셔가 데이터를 얼마나 빠르게 혹은 느리게 생성할지 제어할 수 있도록 하는 것입니다.

자주 묻는 질문: 퍼블리셔가 속도를 늦출 수 없다면? Reactive Streams의 목적은 단지 메커니즘과 경계를 설정하는 것입니다. 퍼블리셔가 속도를 늦출 수 없다면, 데이터를 버퍼링할지, 버릴지, 실패할지 스스로 결정해야 합니다.

Reactive API

Reactive Streams는 상호 운용성(interoperability)에서 중요한 역할을 합니다. 라이브러리와 인프라 컴포넌트에는 유용하지만, 애플리케이션 API로서는 너무 저수준이어서 덜 유용합니다. 애플리케이션에서는 비동기 로직을 구성하기 위해 Java 8 Stream API와 유사하지만 컬렉션뿐만 아니라 다양한 데이터에 적용 가능한, 더 높은 수준의 풍부한 함수형 API가 필요합니다. 이것이 리액티브 라이브러리가 수행하는 역할입니다.

Reactor는 Spring WebFlux에서 선택된 리액티브 라이브러리입니다. Reactor는 MonoFlux API 타입을 제공하여 0..1(Mono) 및 0..N(Flux)의 데이터 시퀀스를 다룰 수 있으며, ReactiveX 연산자와 일치하는 풍부한 연산자 세트를 제공합니다. Reactor는 Reactive Streams 라이브러리이므로, 모든 연산자가 논블로킹 백프레셔를 지원합니다. Reactor는 서버 사이드 Java에 초점을 맞추고 있으며, Spring과 긴밀하게 협력하여 개발됩니다.

WebFlux는 핵심 의존성으로 Reactor를 필요로 하지만, Reactive Streams를 통해 다른 리액티브 라이브러리와 상호 운용 가능합니다. 일반적으로 WebFlux API는 입력으로 일반 Publisher를 받아 내부적으로 Reactor 타입으로 변환하여 사용하고, 출력으로 Flux 또는 Mono를 반환합니다. 따라서 입력으로 어떤 Publisher든 전달할 수 있으며, 출력에 연산을 적용할 수 있지만, 다른 리액티브 라이브러리와 함께 사용하려면 출력을 적절히 변환해야 합니다. 가능할 경우(예: 어노테이션 기반 컨트롤러) WebFlux는 RxJava나 다른 리액티브 라이브러리 사용을 투명하게 지원합니다. 자세한 내용은 Reactive Libraries를 참조하세요.

Reactive API 외에도, WebFlux는 Kotlin의 Coroutines API와 함께 사용할 수 있으며, 이는 보다 명령형(imperative) 스타일의 프로그래밍을 제공합니다. 다음 Kotlin 코드 샘플은 Coroutines API와 함께 제공됩니다.

프로그래밍 모델(Programming Models)

spring-web 모듈에는 HTTP 추상화, 지원되는 서버용 Reactive Streams 어댑터, 코덱, 그리고 Servlet API와 유사하지만 논블로킹 계약을 가진 핵심 WebHandler API를 포함한 Spring WebFlux의 리액티브 기반이 들어 있습니다.

이 기반 위에서 Spring WebFlux는 두 가지 프로그래밍 모델을 제공합니다:

적용성(Applicability)

Spring MVC 아니면 WebFlux?

자연스럽게 떠올릴 수 있는 질문이지만, 잘못된 이분법을 설정하게 됩니다. 사실 두 프레임워크는 함께 사용되어 사용 가능한 옵션의 범위를 확장합니다. 두 프레임워크는 상호 연속성과 일관성을 위해 설계되었으며, 나란히 사용할 수 있고, 각 측의 피드백은 서로에게 도움이 됩니다. 다음 다이어그램은 두 프레임워크가 어떻게 관련되어 있는지, 공통점과 각자가 고유하게 지원하는 내용을 보여줍니다.

spring mvc와 webflux 벤 다이어그램

다음 구체적인 점들을 고려해보는 것이 좋습니다:

서버(Servers)

Spring WebFlux는 Tomcat, Jetty, Servlet 컨테이너뿐만 아니라 Netty, Undertow와 같은 비-Servlet 런타임에서도 지원됩니다. 모든 서버는 저수준의 공통 API에 맞춰 적응되어, 상위 수준의 프로그래밍 모델이 서버 전반에서 지원될 수 있도록 합니다.

Spring WebFlux 자체에는 서버를 시작하거나 중지하는 기능이 내장되어 있지 않습니다. 그러나 Spring 설정과 WebFlux 인프라를 조합하여 애플리케이션을 구성하고, 몇 줄의 코드로 실행하는 것은 쉽습니다.

Spring Boot에는 이러한 단계를 자동화하는 WebFlux 스타터가 있습니다. 기본적으로 스타터는 Netty를 사용하지만, Maven이나 Gradle 의존성을 변경하여 Tomcat, Jetty, Undertow로 쉽게 전환할 수 있습니다. Spring Boot가 기본으로 Netty를 사용하는 이유는 비동기 논블로킹 환경에서 더 널리 사용되며, 클라이언트와 서버가 자원을 공유할 수 있기 때문입니다.

Tomcat과 Jetty는 Spring MVC와 WebFlux 모두에서 사용할 수 있습니다. 다만, 사용 방식이 매우 다르다는 점을 유념해야 합니다. Spring MVC는 Servlet 블로킹 I/O에 의존하며, 필요할 경우 애플리케이션이 Servlet API를 직접 사용할 수 있게 합니다. 반면 Spring WebFlux는 Servlet 논블로킹 I/O에 의존하며, 저수준 어댑터를 통해 Servlet API를 사용하지만 직접 노출하지는 않습니다.

WebFlux 애플리케이션에서는 Servlet 필터를 매핑하거나 Servlet API를 직접 조작하는 것은 강력히 권장되지 않습니다. 앞서 언급한 이유 때문에, 동일한 컨텍스트에서 블로킹 I/O와 논블로킹 I/O를 혼합하면 런타임 문제가 발생할 수 있습니다.

Undertow의 경우, Spring WebFlux는 Servlet API 없이 Undertow API를 직접 사용합니다.

성능(Performance)

성능에는 여러 가지 특성과 의미가 있습니다. 일반적으로 리액티브 및 논블로킹 방식이 애플리케이션을 더 빠르게 실행시키는 것은 아닙니다. 특정 경우에는 속도를 높일 수 있습니다. 예를 들어 WebClient를 사용해 원격 호출을 병렬로 실행하는 경우가 그렇습니다. 그러나 논블로킹 방식으로 처리하려면 더 많은 작업이 필요하며, 이로 인해 처리 시간이 약간 늘어날 수 있습니다.

리액티브와 논블로킹의 핵심 기대 이점은 소수의 고정 스레드와 적은 메모리로 확장할 수 있다는 점입니다. 이로 인해 애플리케이션이 부하 상황에서도 보다 예측 가능한 방식으로 확장되어 내결함성이 높아집니다. 다만 이러한 이점을 관찰하려면 일정한 지연(latency)이 필요하며, 여기에는 느리거나 예측 불가능한 네트워크 I/O가 혼합된 경우가 포함됩니다. 이러한 상황에서 리액티브 스택의 강점이 나타나며, 차이는 매우 뚜렷할 수 있습니다.

동시성 모델(Concurrency Model)

Spring MVC와 Spring WebFlux는 모두 어노테이션 기반 컨트롤러를 지원하지만, 동시성 모델과 블로킹 및 스레드에 대한 기본 가정에는 중요한 차이가 있습니다.

Spring MVC(및 일반적인 서블릿 애플리케이션)에서는 애플리케이션이 현재 스레드를 블로킹할 수 있다고 가정합니다(예: 원격 호출). 이러한 이유로 서블릿 컨테이너는 요청 처리 중 발생할 수 있는 블로킹을 흡수하기 위해 큰 스레드 풀을 사용합니다.

Spring WebFlux(및 일반적인 논블로킹 서버)에서는 애플리케이션이 블로킹하지 않는다고 가정합니다. 따라서 논블로킹 서버는 요청을 처리하기 위해 소수의 고정 크기 스레드 풀(이벤트 루프 워커)을 사용합니다.

“확장(scale)”과 “소수의 스레드(small number of threads)”는 모순처럼 들릴 수 있지만, 현재 스레드를 절대 블로킹하지 않고 대신 콜백에 의존한다는 것은 추가 스레드가 필요 없음을 의미합니다. 블로킹 호출이 없기 때문입니다.

블로킹 API 호출(Invoking a Blocking API)

블로킹 라이브러리를 반드시 사용해야 한다면 어떻게 해야 할까요? Reactor와 RxJava 모두 publishOn 연산자를 제공하여 다른 스레드에서 처리를 계속할 수 있습니다. 즉, 쉽게 빠져나올 수 있는 방법이 있다는 뜻입니다. 다만 블로킹 API는 이 동시성 모델에는 적합하지 않다는 점을 염두에 두어야 합니다.

변경 가능한 상태(Mutable State)

Reactor와 RxJava에서는 연산자를 통해 로직을 선언합니다. 런타임에서는 데이터가 구분된 단계로 순차적으로 처리되는 리액티브 파이프라인이 형성됩니다. 이로 인한 주요 이점은 파이프라인 내의 애플리케이션 코드가 동시에 호출되지 않기 때문에 애플리케이션이 변경 가능한 상태를 보호할 필요가 없다는 것입니다.

스레딩 모델(Threading Model)

Spring WebFlux로 실행되는 서버에서 어떤 스레드를 볼 수 있을까요?

설정(Configuring)

Spring Framework 자체에는 서버 시작과 중지를 지원하는 기능이 없습니다. 서버의 스레딩 모델을 구성하려면 서버별 설정 API를 사용하거나, Spring Boot를 사용하는 경우 각 서버에 대한 Spring Boot 설정 옵션을 확인해야 합니다. WebClient는 직접 구성할 수 있습니다. 그 외 모든 라이브러리는 해당 문서를 참조하세요.