Spring WebFlux

김소희·2024년 2월 8일

프로젝트 리액터

  • 프로젝트 리액터(project Reactor)는 리액티브 스트림의 구현체 중 하나로 스프링의 에코시스템 범주에 포함된 프레임워크이다.
  • 리액티브 스트림 사양을 구현하고 있으므로 리액티브 스트림에서 사용하는 용어와 규칙을 그대로 사용한다.
  • 리액터를 사용하면 애플리케이션에 리액티브 프로그래밍을 적용할 수 있고 비동기-논블로킹을 적용할 수 있다.
  • 함수형 프로그래밍의 접근 방식을 사용해서 비동기-논블로킹 코드의 난해함을 어느정도 해결한다.
  • 백프레셔(Back pressure)를 사용해 시스템의 부하를 효율적으로 조정할 수 있다

모노와 플럭스

리액터는 리액티브 스트림의 Publisher 인터페이스를 구현하는 모노(Mono)와 플럭스(Flux)라는 두가지 핵심 타입을 제공한다.

  • 모노는 0~1개의 단일 요소 스트림을 통지하는 발행자이다
  • 플럭스는 0~N개로 이뤄진 다수 요소 스트림을 통지하는 발행자이다.
  • 두 타입 모두 리액티브 스트림 데이터 처리 프로토콜대로 onComplete 또는 onError 시그널이 발생할 때 까지 onNext를 사용해 구독자에게 데이터를 통지한다.
  • Mono.just(data: T)는 객체를 인자로 받은 뒤 모노로 래핑하는 팩토리 함수이다.
import reactor.core.publisher.Mono

fun main() {
	val mono: Mono<String> = Mono.just("Hello")
    mono.subscribe(::println) //결과 Hello
    
    //println(mono) 결과 MonoJust
}
  • 모노와 플럭스 연산자는 모두 Lazy로 동작하는 형태라서 최종연산자인 subscribe를 호출하지 않으면 리액티브 스트림 사양대로 코드가 동작하지 않는다.
  • 자바의 스트림도 이와 유사하게 map, flatMap, filter 등은 중간연산자, collect, findFirst, count 등이 최종 연산자이다.

스프링 WebFlux와 스프링 MVC 비교

스프링 MVC

  • 스프링으로 개발된 대부분의 웹 애플리케이션은 서블릿 기반의 스프링 MVC이다.
  • 스프링 MVC는 동시성 처리를 전통적 웹 방식인 하나의 스레드가 하나의 요청을 처리하는 Thread per Request Model 을 사용한다.
  • Thread per Request Model은 DB, Network IO 등이 발생할 경우 결과를 받기까지 스레드가 블로킹 되므로 이를 해결하기 위해 스레드 풀을 사용해 동시성을 제어한다.
  • 스레드도 메모리 자원을 소모하기 때문에 요청마다 새로운 스레드를 만들 수 없으므로 스레드 풀을 활용한다.
  • 명령형 코드 작성은 코드 흐름을 쉽게 이해할 수 있고 디버깅하기 쉽다.
  • 대부분의 스프링 웹 애플리케이션이 MVC 기반이므로 안정선과 풍부한 라이브러리를 지원한다.
  • JPA, JDBC와 같은 블로킹 API를 사용하는 경우에는 MVC를 사용하는 것이 낫다.
  • MVC에서도 리액터와 WebFlux 의존성을 추가하여 리액티브 코드와 논블로킹 라이브러리를 사용할 수 있다.

스프링 WebFlux

  • 스프링 WebFlux는 전통적 웹 프레임워크인 스프링MVC와 대비되는 리액티브 기반의 웹 스택 프레임워크이다.
  • 기본적으로 프로젝트 리액터 기반이며 리액티브 스트림의 다른 구현체인 RxJav, 코틀린 코루틴으로도 개발이 가능하다.
  • 비동기-논블로킹으로 동작하므로 적은 수의 스레드로도 대량의 동시성을 제어할 수 있다.
  • 함수형 엔드포인트와 애노테이션 컨트롤러 방식을 모두 지원한다.
  • 내부적으로 이벤트 루프 동시성 모델을 사용한다.
  • 스프링 MVC에 비해서 러닝커브가 많이 높은 편이다.(배우기 어렵다)
  • 전 구간 비동기-논블로킹인 경우에 최적의 성능을 보여주므로 어쩔 수 없이 블로킹 API를 사용할 때는 별도의 스케줄러로 동작시키는 게 좋다.

함수형 엔드포인트

스프링 WebFlux는 클라이언트의 요청을 라우팅 하고 처리할 수 있는 람다(Lambda) 기반 프로그래밍 모델인 함수형 엔드포인트를 제공한다.
요청을 분석해 핸들러롤 라우팅하는 라우터 함수와 요청 객체를 전달받아 응답을 제공하는 핸들러 함수로 이루어져 있다.

라우터 함수

  • 클라이언트로부터 전달받은 요청을 해석하고 그에 맞는 핸들러로 전달하는 역할을 한다.
  • @Configuration 을 통해서 설정으로 등록해주어야 하고, RouterFunction을 반환하는 @Bean으로 등록해주어야 한다.
  • URI 패턴 매칭에 따른 분배 역할을 하기 위해서 라우터 함수에 있는 route()와 같은 builder를 사용하여 HTTP 요청 (post, put, get, delete)에 대한 매핑을 작성할 수 있다.
  • 중첩 라우터를 통해서 중복되는 URI에 대한 요청을 그룹핑하면 코드 중복을 줄일 수 있다.
@Configuration
class Router {
	
    @Bean
    fun helloRouter(handler: HelloHandler) : RouterFunction<ServerResponse> =
    	route()
        	.GET("/", handler::sayHello)
        	.build()
        
    @Bean
    fun userRouter(handler: UserHandler) : RouterFunction<ServerResponse> =
    	router {
    		"/users".nest {
        		GET("/{id}", handler::getUser)
        		GET("", handler::getAll)
        	}
    	}
        
}

핸들러 함수

  • 핸들러 함수는 라우터 함수로 부터 전달받은 ServerRequest를 인자로 받아서 로직을 처리한 다음에 ServerResponse를 생성하여 반환한다.
  • ServerRequest, ServerResponse 가 기본적으로 불변객체이기 때문에 내부에서 직접적으로 객체의 프로퍼티를 변경할 수 없다. (setter로는 변경할 수 있겠지만)
  • 핸들러 함수를 작성할 때에는 리액터의 퍼블리셔인 Mono나 Flux로 응답 본문을 작성한다.
@Component
class HelloHandler {

	fun sayHello(req: ServerRequest) : Mono<ServerResponse> {
    	return ServerResponse.ok().bodyValue("Hello WebFlux")
    }
    
}

data class User(val id: Long, val email: String)

@Component
class UserHandler {
	val users = listOf(
    	User(id = 1, email = "user1@gmail.com")
        User(id = 2, email = "user2@gmail.com")
    )
    
    fun getUser(req: ServerRequest) : Mono<ServerResponse> =
    	user.find { req.pathVariable("id").toLong() == it.id }
        	?.let {
            	ServerResponse.ok().bodyValue(it)
            } ?: ServerResponse.notFound().build()
            
    fun getAll(req: ServerRequest) : Mono<ServerResponse> =
    ServerResponse.ok().bodyValue(users)
}

애노테이션 컨트롤러

WebFlux에서도 Spring MVC와 동일하게 @RestController, @GetMapping과 같은 애노테이션을 사용하여 컨트롤러를 작성한다.

@RestController
class BookController(
	private val bookService: BookService,) {
    
    @PostMapping("/books")
    fun add(@Requestbody request: Map<String, Any>) : Mono<Book> {
    	return bookService.add(request)
    }
    
    @GetMapping("/books")
    fun getAll() : Flux<Book> {
    	return bookService.getAll()
    }
    
    @GetMapping("/books/{id}")
    fun get(@PathVariable id: Int) : Mono<Book> {
    	return bookService.get(id)
    }
    
    @DeleteMapping("/books/{id}")
    fun delete(@PathVariable id: Int) : Mono<Void> {
    	return bookService.delete(id)
    }
}

참고자료

Thread per Request Model에 대해서
토리맘의 스프링 웹플럭스

profile
백엔드 개발자의 노트

0개의 댓글