Spring Framework에서 지원하는 리액티브 웹서버 프레임워크인 Spring WebFlux에 대하여 알아보자.
Spring WebFlux
기존 SpringFramework의 웹 프레임워크인 Spring MVC는 서블릿 API와 서블릿 컨테이너용으로 만들어졌다. 그런데 점차 Reactive stack이 발전하면서 SpringFramework 5.0 버전 이상 부터는 Reactive stack의 웹 프레임워크인 Spring WebFlux를 개발하였다. Spring WebFlux는 완전한 non-blocking을 지원하며 Reactive stream의 back pressure를 지원하고, Netty, Undertow, Servlet 3.1+ 컨테이너와 같은 서버에서 실행된다.
Spring MVC vs Spring WebFlux
Spring MVC와 WebFlux는 완전히 다른 개념이며, 어느 것이 더 좋다라고 말할 수는 없을 것이다. 그래서 각각의 특성을 잘 파악한 뒤 프로젝트 특성에 맞게 잘 선택하는 것이 좋을 것 같다.
아래 다이어그램은 Spring 공식 문서에 나와있는 다이어그램인데, 이 다이어그램은 두 프레임워크의 관계, 공통점 및 고유 특성을 나타낸다.
특징 | Spring MVC | Spring WebFlux |
프로그래밍 방식 | 명령형 프로그래밍 | 함수형 프로그래밍 |
라이브러리 / API | Blocking | Non-blocking |
러닝커브 | 비교적 낮다 | 비교적 높다 |
서버 | Tomcat, Jetty | Tomcat, Jetty, Netty, Undertow |
쓰레드 모델 | Request per Thread | Event Loop |
물론 Spring MVC, Spring WebFlux를 같이 사용할 수도 있다.
Spring MVC
Spring MVC는 명령형 프로그래밍 형식으로 코드를 작성한다. 그리고 Blocking 라이브러리를 사용한다. 그리고 Request per Thread, 즉, 하나의 요청 당 하나의 쓰레드를 사용한다.
그런데 요청이 들어올 때 마다 쓰레드를 생성하는 것은 상당한 비용이 들기 때문에, 쓰레드 풀을 만들어 쓰레드를 재사용한다. 요청이 들어오면 요청을 큐에 쌓은 다음, 쓰레드 풀에서 사용 가능한 쓰레드가 요청을 처리한다.
대신 이러한 경우, 쓰레드 풀 갯수보다 많은 요청이 자꾸 들어오면 요청이 큐에 계속 쌓여서 처리를 못하고 있는 요청이 늘어난다. 그래서 성능이 저하되는데 Spring MVC를 사용하는 경우에는 쓰레드 풀 개수를 잘 조절해야 한다.
Spring WebFlux
Spring WebFlux는 함수형 리액티브 프로그래밍 형식으로 코드를 작성한다. 그리고 Non-blocking Asynchronous 방식을 사용하며 동시성 모델로 이벤트 루프 방식을 사용한다. 논블로킹 I/O를 이용하기 때문에 Spring WebFlux는 적은 쓰레드로도 많은 양의 동시성 처리를 감당할 수 있다.
하지만 러닝커브가 높으며, 잘못 사용하면 Spring MVC보다 성능이 안나올 수도 있다. 대표적인 예시로는 Non Blocking API를 사용해야 하는데 RestTemplate와 같은 Blocking API를 사용하는 경우에는 성능저하가 일어날 수 밖에 없다.
Spring WebFlux 맛보기
일단 Spring WebFlux 애플리케이션을 띄워보자. @Controller 애노테이션을 사용하는 Spring MVC와 다르게 WebFlux는 Router & Handler 형식으로 요청을 받는다.
Gradle
gradle에 Spring WebFlux를 구성하는 데 필요한 디펜던시를 추가한다.
bulid.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.7.10"
kotlin("plugin.spring") version "1.7.10"
id("org.springframework.boot") version "2.7.3"
id("io.spring.dependency-management") version "1.0.13.RELEASE"
}
group = "org.example"
version = "1.0-SNAPSHOT"
buildscript {
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
}
}
repositories {
mavenCentral()
}
tasks {
withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = JavaVersion.VERSION_17.majorVersion
}
}
}
dependencies {
apply(plugin = "kotlin-spring")
apply(plugin = "kotlin")
apply(plugin = "io.spring.dependency-management")
apply(plugin = "org.springframework.boot")
implementation("org.springframework.boot:spring-boot-starter-webflux")
// test
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
}
Router & Handler
Spring WebFlux는 요청을 라우팅하고 처리하는 데 함수가 사용되는 경량 함수형 프로그래밍 모델을 채택한다. Spring WebFlux는 Controller가 아닌 함수형 프로그래밍 모델인 Router & Handler로 구성한다.
간단한 예제로 /hello 로 요청을 하면 hello, world를 반환하는 웹 서버를 만들어보자.
Handler
WebFlux에서는 HTTP 요청은 HandlerFunction에 의해 핸들링된다. HandlerFunction은 ServerRequest를 받고 Mono<ServerResponse>를 반환하는 함수이다. 이 두 객체는 immutable이다.
HandlerFunction은 Spring MVC에서 @RequestMapping의 바디에 해당된다. 즉, 요청을 처리하고 반환하는 로직을 담당한다.
예제 설명과 같이 요청을 받으면 Hello, world를 리턴하도록 코드를 구성하였다.
TestHandler.kt
package com.beer1.example.presentation
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.ServerResponse.ok
import reactor.core.publisher.Mono
@Component
class TestHandler {
fun helloWorld(request: ServerRequest): Mono<ServerResponse> {
return ok().bodyValue(TestData("Hello, world"))
}
}
data class TestData(val result: String)
- 리턴값에서 ok()는 200 리턴을 의미하며, created() (201), accepted() (202) 등 여러가지로 나타낼 수도 있다.
- bodyValue() 메서드 파라미터에 반환하고자 하는 body 객체를 전달하면 된다.
Router
Spring WebFlux로 들어오는 모든 요청은 RouterFunction이 있는 Handler function으로 라우팅된다. RouterFunction은 ServerRequest를 받고 Mono<HandlerFunction> 을 반환한다. 요청 중 router function에 일치한다면 그에 대응하는 handler function이 반환되며 일치하는 router function이 없다면 empty mono를 리턴한다.
RouterFunction은 Spring MVC에서 @RequestMapping 어노테이션에 해당된다.
예제 설명과 같이 우리는 path가 /hello 인 요청을 받아야 하므로 RouterFunction은 아래와 같이 구성해야 한다.
TestRouter.kt
package com.beer1.example.presentation
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.router
@Configuration
class TestRouter(
private val handler: TestHandler
) {
@Bean
fun testRouterFunction(): RouterFunction<ServerResponse> {
return router {
"/hello".nest {
accept(MediaType.APPLICATION_JSON).nest {
GET("", handler::helloWorld)
}
}
}
}
}
- path가 /hello 이면서 Accept: application/json 인 요청을 TestHandler.helloWorld 메서드가 처리하도록 RouterFunction을 구성하였다.
요청 날려보기
이 상태에서 BootRun을 하고 실행한 후 curl으로 테스트를 하면 출력이 된다.
$ curl localhost:8080/test/hello
{"result":"hello"}%
댓글