[개발] Spring Framework/Spring WebFlux

[1] Spring WebFlux 시작하기

비어원 2022. 10. 6. 23:14
728x90

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"}%

 

 

728x90