Web on Reactive Stack

개요

  • 원본 문서 : web-reactive.html
  • 작업 기준 버전 : 5.3.5
  • 논블로킹 서버(Netty, Undertow, Servlet 3.1+) 위에서 리액티브 스트림 API 기반의 리액티브-스택 웹앱에 관한 사항을 다룬다

리액티브 스트림

java.util.concurrent.Flow Since 9 Goto - reactive-stream

  • Publisher : 발행자. subscribe​(Subscriber)
  • Subscriber : 구독자. onSubscribe(Subscription), onNext(T), onError(Throwable), onComplete(Void)
  • Subscription : 구독. request(long), cancel(Void)
  • Processor extends Subscriber, Publisher : 발행자 + 구독자

리액터의 발행자 Processor

  • reactor.core.publisher.Mono : 0..1개의 이벤트 발행
  • reactor.core.publisher.Flux : 0..n개의 이벤트 발행

예시 코드

Spring WebFlux

Overview

  • 개발 이유 1 : 더 적은 스레드와 하드웨어 리소스로 동시성을 처리하는 논-블로킹 웹 스택이 필요했기 때문
  • Servlet 3.1에서 논-블로킹 I/O를 제공했지만, 다른 여타 동기적 또는 블로킹하는 서블릿 API와 같이 사용하기는 어려움이 있다. 이에 여느 논-블로킹 런타임과도 동작할 수 있는 공통 API의 수요가 생겼다

  • 개발 이유 2 : Java 8에 도입된 람다를 활용할 수 있는 함수형 엔드포인트 개발을 위해

Define "Reactive"

  • 리액티브 ::= 변화에 반응하는 것을 중심으로 한 프로그래밍 모델 - I/O 이벤트, UI 이벤트 등
  • 스프링 팀은 여기에 "논-블로킹 back pressure"를 주요 매커니즘으로 더했다
  • 생산자가 소비자를 호출하는 경우, 빠른 생산으로 생산자가 소비자를 압도할 수 있다. WebFlux는 리액티브 스트림을 이용하여, subscriber가 publisher 속도를 제어하도록 한다

  • Publisher의 속도를 늦츨 수 없다면?
  • 리액티브 스트림은 잉여 생산물에 대한 규정은 하지 않는다. 버퍼에 담을 지, 버릴 지, 실패로 처리할 지는 개발자가 결정해야 한다

Reactive API

리액티브 스트림의 저수준 API를 사용자가 직접 이용하기에는 부적절하다. 이에 WebFlux는 리액터를 채택하였다

Programming Models

WebFlux는 HTTP 추상화, 지원 서버(Netty 등)를 위한 Goto - HttpHandler, Servlet API에 대응하는 Goto - WebHandler API 등을 포함하며, 이를 기초로 2가지 프로그래밍 모델을 지원한다

  • Goto - Annotated Controllers
  • Spring MVC와 같은 애너테이션을 사용한다. Spring MVC와 WebFlux 컨트롤러 모두 리액티브 반환 타입을 지원하므로, 둘을 떼어 놓고 이야기하기는 어렵다. 한 가지 큰 차이점으로, WebFlux는 리액티브 @RequestBody 인자를 지원한다

  • Goto - Functional Endpoints
  • 람다 기반, 경량, 함수형 프로그래밍 모델. 애너테이트된 컨트롤러와 다르게 앱이 요청의 시작부터 끝까지 제어하며, 요청을 라우팅하는 라이브러리나 유틸리티 집합이라고 생각해도 된다

Applicability

굳이 잘 동작하는 Spring MVC 앱을 바꿀 필요는 없다

Servers

WebFlux 자체에 서버를 시작/종료하는 기능은 없지만, Spring Boot WebFlux starter에서는 이 단계를 자동화해준다

Concurrency Model

Invoking a Blocking API

  • 리액터의 publishOn, subscribeOn을 이용해 호출을 다른 스레드를 이용해 처리할 수 있다
  • 지원하는 Schedulers.html
  • method실행 적합
    parallel()병렬 non-blocking Runnable
    single()저지연 Runnable 단일 실행. 반환 Scheduler는 재사용된다.
    boundedElastic()실행 시간이 긴 여러 (블로킹) 작업 실행. 동시 실행 제한(bound) 존재
    • 유저 스레드는 idle 60초를 넘으면 동적으로 삭제된다
    • 기본 유저 스레드지만, 오버라이딩 메서드로 데몬 스레드 지정 가능

    • threadCap : 최대 스레드 수. 기본값은 (코어 수) * 10
    • queuedTaskCap : 최대 큐잉 작업 수. 초과하면 RejectedExecutionException 발생. Integer.MAX_VALUE으로 무제한 설정 가능
    • name : 스레드 이름 prefix
    immediate()호출자 스레드로 즉시 실행
    fromExecutorService()Backing ExecutorService 지정

Reactive Core

HttpHandler

지원하는 HTTP 서버들에 대한 최소한의 추상화

↓ interface HttpHandler

Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response)

↓ Reactor Netty

HttpHandler handler = ... ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); HttpServer.create().host(host).port(port).handle(adapter).bind().block();

WebHandler API

org.springframework.web.server 패키지는 HttpHandler 규약 위에서, 컴포넌트 체인(N개 WebExceptionHandler, N개 WebFilter, 1개 WebHandler)을 이용해 요청을 처리한다

WebHttpHandlerBuilder를 이용해 체인을 구성할 수 있는데, 직접 builder에 등록하거나, 컴포넌트를 자동 감지한 ApplicationContext를 지정해주면 된다

WebHandler API는 웹앱 공통 기능들을 지원한다 : 유저 세션 with 속성, 요청 속성, 요청 Locale/Principal 결정, form data, multipart data 등

WebHttpHandlerBuilder 자동 감지 빈

Bean nameBean typeCountDescription
<any>WebExceptionHandler0..N예외 처리
<any>WebFilter0..N전후 처리
webHandlerWebHandler1요청 처리
webSessionManagerWebSessionManager0..1WebSession 인스턴스 관리. ServerWebExchange로 획득 가능. 기본값 DefaultWebSessionManager
실행 스크린샷
최초 get 헤더
<이미지 - 최초 get 헤더>
최초 get body
<이미지 - 최초 get body>
1번째 set
<이미지 - 1번째 set>
2번째 set
<이미지 - 2번째 set>
2번째 get 헤더
<이미지 - 2번째 get 헤더>
2번째 get body
<이미지 - 2번째 get body>

Redis를 이용해 세션을 관리하고 싶은 경우 : spring-session-data-redis 사용 고려samplesspring-session-reactive

serverCodecConfigurerServerCodecConfigurer0..1form, multipart 데이터를 파싱하는 HttpMessageReader 인스턴스에 접근. ServerWebExchange로 획득 가능. 기본값 ServerCodecConfigurer.create()
localeContextResolverLocaleContextResolver0..1ServerWebExchange로 획득 가능. 기본값 AcceptHeaderLocaleContextResolver
forwardedHeaderTransformerForwardedHeaderTransformer0..1프록시가 추가한 forwarded 헤더 조작. 기본적으로 사용되지 않는다.

Form Data(application/x-www-form-urlencoded)

↓ ServerWebExchange

Mono<MultiValueMap<String,String>> getFormData()

DefaultServerWebExchange는 설정된 HttpMessageReader 인스턴스를 이용해 form을 MultiValueMap으로 파싱한다. 기본적으로 ServerCodecConfigurer 빈이 FormHttpMessageReader 빈을 사용 설정한다

Multipart Data(multipart/form-data)

↓ ServerWebExchange

Mono<MultiValueMap<String,Part>> getMultipartData()

DefaultServerWebExchange는 설정된 HttpMessageReader<MultiValueMap<String, Part>> 인스턴스를 이용해 multipart를 MultiValueMap으로 파싱한다. 기본적으로 DefaultPartHttpMessageReader 빈이 이용된다

Filters

  • WebFilter 사이의 실행 순서는 빈 정의에 @Order를 이용하거나, Ordered 인터페이스를 구현함으로써 조절할 수 있다
  • Controller 클래스에 애너테이션을 붙여 CORS 설정을 할 수 있지만, Spring Security를 이용하는 경우 CorsFilter를 이용해 Security보다 먼저 실행되도록 설정할 것이 권장된다Goto - CORS

Exceptions

  • ResponseStatusExceptionHandler : Throwable -> int(HttpStatus) 결정
  • WebFluxResponseStatusExceptionHandler : @ResponseStatus로부터 HttpStatus 자동 획득
  • REST API의 경우, ResponseEntity를 이용함이 권장된다

Codecs

  • Encoder, Decoder는 HTTP 스펙과 무관하게, 임의 타입과 DataBuffer 사이의 변환을 정의한다
  • HttpMessageReader, HttpMessageWriter는 HTTP 메시지의 읽기/쓰기를 담당한다
  • Encoder는 EncoderHttpMessageWriter로, Decoder는 DecoderHttpMessageReader로 래핑 가능하다
  • DataBuffer는 기존 byte buffer들(Netty의 ByteBuf, java.nio.ByteBuffer, ...)에 대한 추상화다

spring-core 모듈은 byte[], ByteBuffer, DataBuffer, Resource, String에 대한 encoder/decoder를 제공하고, spring-web 모듈은 Jackson JSON, Jackson Smile(binary JSON), JAXB2 encoder/decoder 및 HTTP(form, multipart, ...) reader/writer를 제공한다

전형적으로 ClientCodecConfigurer, ServerCodecConfigurer를 이용해 코덱을 커스터마이징한다. Goto - HTTP message codecs

Jackson JSON

  • Jackson 라이브러리가 존재하는 경우, JSON 및 binary JSON(Smile)이 지원된다
  • Jackson2Decoder의 동작
    1. Jackson의 비동기, 논블로킹 파서가 하나의 JSON 객체를 표현하는 TokenBuffer로 바이트 스트림을 모은다
    2. 각 TokenBuffer는 Jackson의 ObjectMapper로 넘겨져 고수준 객체로 변환된다
    3. single-value publisher(e.g. Mono)의 디코딩의 경우, TokenBuffer가 하나만 필요하다
    4. multi-value publisher(e.g. Flux)의 디코딩의 경우, 각 TokenBuffer가 객체를 구성할 수 있게 되면 ObjectMapper로 전달된다. 입력 컨텐츠로 JSON 배열, line-delimited JSON(application/stream+json)이 가능하다
  • Jackson2Encoder의 동작
    1. single-value publisher(e.g. Mono)는 바로 ObjectMapper에서 직렬화한다
    2. multi-value publisher를 application/json로 직렬화하는 경우, Flux#collectToList()의 반환 컬렉션을 직렬화한다
    3. application/x-ndjson, application/stream+x-jackson-smile 같은 스트리밍 타입으로 multi-value publisher를 직렬화하는 경우, line-delimited JSON 포맷으로 각 값을 따로 write, flush한다
    4. SSE의 경우, 이벤트가 발생할 때마다 Jackson2Encoder를 1회 호출하고 flush하여 저지연을 보장한다
  • 커스텀 JsonSerializer 등록
  • ↓ java

    @JsonComponent public class EntityResponseSerializer<T> extends JsonSerializer<EntityResponse<T>> { @Override public void serialize(EntityResponse<T> value, JsonGenerator gen, SerializerProvider serializers) throws IOException { var v = value.entity(); if (v == null) gen.writeNull(); else if (v instanceof String) gen.writeString((String) v); else gen.writeObject(v); } }

Logging

DispatcherHandler

Special Bean Types

WebFlux Config

Processing

Result Handling

Exceptions

View Resolution

Annotated Controllers

@Controller

Request Mapping

Handler Methods

Model

DataBinder

Managing Exceptions

Controller Advice

Functional Endpoints

Overview

HandlerFunction

RouterFunction

Running a Server

Filtering Handler Functions

URI Links

CORSGoto - CORS

Web Security

View Technologies

HTTP Caching

WebFlux Config

HTTP message codecs