IO

- NIO가 나오면서 자바의 기존 IO 방식은 OIO(Old I/O)라고도 불림.

- *InputStream, *OutputStream, *Reader, *Writer...

- 전통적인 blocking call 방식.

- 때문에 FD(소켓, 파일...) 당 하나의 쓰레드가 필요함.

- 때문에, 동시 요청이 자주 발생하는 어플리케이션의 경우 필연적으로 쓰레드 풀을 사용하게 됨.

 

 

NIO

- New I/O 로, 자바 1.4에서 등장.

- 주요 요소: Buffer, Charset, Channel, Selector.

- Buffer: NIO의 데이터 저장 컨테이너로, 다이렉트 버퍼, 논 다이렉트 버퍼(힙 버퍼)로 나뉨. 다이렉트 버퍼는 네이티브 메모리를 사용하고, 논 다이렉트 버퍼는 JVM의 힙 메모리를 사용함.

- Channel: 데이터가 오고 가는 양방향 통로.

- Selector: non-blocking, 이벤트 방식으로, 지정한 채널에 발생한 이벤트를 수신하여 이벤트 유형에 맞는 동작을 처리할 수 있음. 

- 결정적으로, OIO에서는 다수의 요청을 받아들이기 위해 많은 수의 쓰레드를 필요로 했지만, NIO에서는 Selector를 조회하는 하나의 쓰레드만으로 다수의 요청을 커버할 수 있음. 물론 요청의 성격에 따라 별도의 처리 쓰레드가 필요할 수 는 있다.

- 자바 6 부터는 epoll을 지원하여, 런타임 os가 리눅스 커널 2.6 이상이라면 epoll을 디폴트로 한다. poll을 사용하려면 아래와 같이 시스템 프로퍼티로 제어 가능함.

System.setProperty("java.nio.channels.spi.SelectorProvider", "sun.nio.ch.PollSelectorProvider");

- 이렇게, NIO는 os specific하다. os의 기능을 차용하여, 아래와 같이 런타임 os에 따라 동작을 달리한다.

Windows: select

Mac OS: Kqueue

리눅스 커널 2.6+: epoll

솔라리스: poll

 

 

NIO2

- 그냥 NIO로 통틀어 말하기도 함. 자바 1.7에서 등장. 네트워킹 I/O에는 마이너한, 파일 I/O에는 메이저한 변화가 있었음.

- Async* 시리즈의 등장으로, 비동기 콜백 방식의 I/O를 지원함.

'Java > Core' 카테고리의 다른 글

Java 13 간략히 정리  (0) 2020.07.16
Java 12 간략히 정리  (0) 2020.07.15
Java 11 간략히 정리  (0) 2020.07.15
Java 10 간략히 정리  (0) 2020.07.15
람다  (0) 2020.01.05

Web on Reactive Stack


이 문서는 Netty(이하 네티), Undertow(이하 언더토), 서블릿 3.1+ 컨테이너와 같은 논 블로킹 서버 위에 구동되는, Reactive Streams(이하 리액티브 스트림) API 기반의 리액티브 스택 웹 어플리케이션 지원에 대해 다룬다. 각 챕터는 Spring WebFlux(이하 스프링 웹플럭스) 프레임워크, 리액티브 WebClient(이하 웹클라이언트), 테스팅 그리고 리액티브 라이브러리를 주제로 한다. 서블릿-스택 웹 어플리케이션에 대해서는 Web on Servlet Stack을 보라.

1. 스프링 웹플럭스(Spring WebFlux)

스프링 프레임워크의 오리지널 웹 프레임워크인 스프링 웹 MVC는 서블릿 API와 서블릿 컨테이너를 위한 것이었다. 리액티브 스택 웹 프레임워크인 스프링 웹플럭스는 스프링 버전 5.0 이후로 추가되었다. 스프링 웹플럭스는 완전한 논 블로킹으로, 리액티브 스트림 back pressure(이하 백프레셔)를 지원하며, 네티, 언더토, 서블릿 3.1+ 컨테이너 등등의 서버에서 구동된다.

스프링 웹 MVC와 스프링 웹플럭스 각각은 스프링 프레임워크 안에서 대칭적으로 존재한다(spring-webmvc, spring-webflux 모듈). 각 모듈은 선택적이며, 어플리케이션은 하나, 또는 다른 모듈, 또는 경우에 따라 둘 모두를 동시에 사용할 수 있다 - 예를 들어 스프링 MVC 컨트롤러와 리액티브 웹클라이언트를 함께 사용할 수 있다.

1.1. 개요

스프링 웹플럭스는 왜 탄생했는가?

이 물음에 대한 답 일부는, 논 블로킹 웹 스택이 적은 수의 쓰레드와 보다 적은 하드웨어 자원으로 동시성을 처리하기 위함이다. 서블릿 3.1 에서도 이미 논 블로킹 I/O를 다루기 위한 API를 제공하지만, 이 API를 사용하면 다른 나머지 서블릿 API와는 멀어지게 된다(필터, 서블릿과 같은 동기 방식 처리나 getParameter, getPart 등 블로킹 API). 이런 점이 어떠한 논 블로킹 런타임에서든 기반 역할로 지원하는 새로운 공통 API의 탄생 동기가 되었다. 네티와 같이 비동기, 논 블로킹 영역이 잘 구현된 서버로 인해 이 점은 중요하다.

스프링 웹플럭스의 또다른 탄생 배경은 함수형 프로그래밍이다. 자바 8 에서 추가된, 함수형 API를 위한 람다 표현식은 자바 5 의 어노테이션(어노테이티드 REST 컨트롤러, 단위 테스트)만큼이나 자바 세계에 새로운 기회를 제공한다. 람다 표현식은 비동기 로직을 서술적 구성으로 작성 가능하도록 하는, 논 블로킹 어플리케이션과 continuation-style APIs(CompletableFuture 및 ReactiveX로 보급된)를 위한 유용한 도구이다. 프로그래밍 모델 레벨에서, 자바 8 은 스프링 웹플럭스에서 함수형 웹 엔드포인트를 어노테이티드 컨트롤러와 함께 제공하는 일을 가능하게 한다.

1.1.1. "리액티브" 의 정의(Define "Reactive")

위에서 "논 블로킹" 과 "함수형" 을 언급했다. 그런데 리액티브는 무슨 의미일까?

용어 "리액티브" 는 변경에 대한 반응에 중점을 두어 만들어진 프로그래밍 모델을 가리킨다. 네트워크 컴포넌트는 I/O 이벤트에 반응하며, UI 컨트롤러는 마우스 등과 같은 이벤트에 반응한다. 이 맥락에서, 논 블로킹은 리액티브이다. 왜냐하면 동작을 중단(blocking)하는 대신 명령의 완료 또는 데이터의 제공 등의 알림에 반응하는 방식을 취하기 때문이다.

스프링과 "리액티브" 의 연결과, 논 블로킹 백프레셔 기술에는 또다른 중요한 메커니즘이 있다. 동기 방식, 명령형 코드, 블로킹 호출은 요청자를 대기 상태로 두어 자연스럽게 백프레셔의 형태를 취한다. 논 블로킹 코드에서는 빠른 producer(이하 프로듀서)가 목적지(소비자)를 압도하지 않도록 하기 위해 이벤트의 속도를 제어하는 것이 중요하다.

리액티브 스트림은 자바 9 에서 채택된 작은 스펙이다. 리액티브 스트림은 백프레셔를 통해 비동기 컴포넌트들 사이의 비동기적 상호 작용을 정의한다. 예를 들어, 데이터 저장소(Publisher(이하 발행자) 역할)는 HTTP 서버(Subscriber(이하 구독자))가 응답에 쓰기 위한 위한 데이터를 생성한다. 리액티브 스트림의 주안점은 구독자로 하여금 발행자가 데이터를 얼마나 빠르게, 혹은 얼마나 천천히 생성할지 제어할 수 있게 한다는 데에 있다.

# 공통 질문: 발행자를 늦출 수 없으면 어떻게 되는가? 리액티브 스트림의 목적은 오직 그 메커니즘과 경계선을 확립하는 것이다. 발행자를 늦출 수 없다면 버퍼의 사용, 드랍 또는 실패 등을 결정해야 한다.

1.1.2. 리액티브 API(Reactive API)

리액티브 스트림은 시스템의 상호 정보 교환에 있어 중요한 역할을 한다. 이는 라이브러리와 인프라스트럭처 컴포넌트에는 흥미로운 점이지만, 어플리케이션 API에는 상대적으로 유용하지 못하다. 왜냐하면 이는 너무 로우 레벨이기 때문이다. 어플리케이션은 비동기 로직을 작성하기 위해서 보다 고수준의, 풍부한 함수형 API를 필요로 한다. 자바 8 의 스트림 API와 유사하지만 컬렉션에 국한되지 않는다. 이것이 리액티브 라이브러리의 역할이다.

Reactor(이하 리액터)는 스프링 웹플럭스가 채택한 리액티브 라이브러리이다. 리액터는 ReactiveX와 함께하는 풍부한 연산자를 통해 0..1(Mono) 와 0..N(Flux) 방식 API를 제공한다. 리액터는 리액티브 스트림 라이브러리다. 따라서 리액터의 연산자는 논 블로킹 백프레셔를 지원하며, 특히 서버 사이드 자바에 집중한다. 그리고 스프링과 긴밀하게 협업하여 개발되었다.

웹플럭스는 리액터에 핵심적인 의존성을 가지지만, 리액티브 스트림을 통해 다른 리액티브 라이브러리들과도 상호 운용이 가능하다. 일반적으로 웹플럭스 API는 플레인 Publisher를 인풋으로 받고, 내부적으로 이를 리액터 타입으로 맞추어 적용하고, 사용하고, Flux 또는 Mono를 아웃풋으로 반환한다. 때문에 어떠한 Publisher든 인풋으로 전달하여 아웃풋에 대한 연산을 적용할 수 있지만, 또다른 리액티브 라이브러리 사용을 위해 아웃풋을 형식에 맞추어야 한다. 웹플럭스는 언제든지 필요에 따라서(어노테이티드 컨트롤러 등) RxJava 또는 다른 리액티브 라이브러리에 쉽게 적용될 수 있다. 이에 관한 더 자세한 내용은 리액티브 라이브러리를 보라.

# 리액티브 API에 더하여, 웹플럭스는 코틀린의 Coroutines(이하 코루틴) API 와도 함께 사용될 수 있다. 코루틴은 보다 명령형의 프로그래밍을 제공한다. 앞으로의 코틀린 코드 샘플은 코루틴 API와 함께할 것이다.

1.1.3. 프로그래밍 모델(Programming Models)

spring-web 모듈은 스프링 웹플럭스의 근간이 되는 리액티브의 기반을 포함하며, HTTP 추상화, 지원되는 서버를 위한 리액티브 스트림 어댑터, 코덱, 그리고 서블릿 API와 유사하면서 논 블로킹 계약을 포함하는 핵심 WebHandler API를 포함한다.

이를 토대로 스프링 웹플럭스는 프로그래밍 모델에 있어 두 가지 선택지를 제공한다.

  • 어노테이티드 컨트롤러: 스프링 MVC와 일치하며, spring-web 모듈과 동일한 어노테이션을 기반으로 구성되었다. 스프링 MVC와 웹플럭스 컨트롤러는 리액티브(리액터, RxJava) 반환 타입을 지원하며, 결과적으로 이 둘을 구분하기가 쉽지 않게 되었다. 주목할만한 차이점은 웹플럭스는 리액티브 @RequestBody 아규먼트를 지원한다는 것이다.
  • 함수형 엔드포인트: 람다 기반의 경량 함수형 프로그래밍 모델. 소형 라이브러리, 혹은 요청을 라우팅하고 핸들링하기 위해 어플리케이션이 사용할 수 있는 유틸리티의 모음이라고 할 수 있다. 어노테이티드 컨트롤러와의 큰 차이점은, 어플리케이션이 요청 핸들링의 시작부터 끝까지 책임지느냐 vs 어노테이션을 통해 의사를 표시하고 콜백을 받느냐이다.


1.1.4. 적용 영역(Applicability)

스프링 MVC냐, 웹플럭스냐?

이 질문은 자연스럽지만 적절하지 못한 이분법이라 할 수 있다. 실제로 이 둘은 사용 가능한 옵션의 범위를 확장하기 위해 함께 사용될 수 있다. 이 둘은 서로간의 지속성과 일관성을 지향하도록 설계되었다. 나란히 함께 사용될 수 있으며, 서로가 서로에게 응답과 이점을 주고 받을 수 있다. 아래 다이어그램은 이 둘이 어떻게 연관되어 있는지 보여준다. 둘이 공통적으로 지닌 것, 한 쪽만 지니고 있는 것을 보여준다.

spring mvc and webflux venn

다음 사항에 주목하기 바란다:
  • 잘 작동 중인 기존 스프링 MVC 어플리케이션이 있다면, 변경할 필요가 없다. 명령형 프로그래밍은 작성하고 이해하고 디버깅 하기에 가장 쉬운 방법이며, 라이브러리 선택에 있어 최대한의 선택지를 가지게 된다. 대부분 블로킹 방식이기 때문에.
  • 이미 논 블로킹 웹 스택을 찾고 있다면, 스프링 웹플럭스는 다른 웹 스택과 동일한 실행 모델이라는 이점과 함께, 서버에 있어서의 선택지(네티, 톰캣, 제티, 언더토, 서블릿 3.1+ 컨테이너), 프로그래밍 모델에 있어서의 선택지(어노테이티드 컨트롤러, 함수형 웹 엔드포인트), 리액티브 라이브러리에 있어서의 선택지(리액터, RxJava 또는 그 외)를 제공한다.
  • 자바 8 람다 또는 코틀린과 함께 사용할 경량 함수형 웹 프레임워크를 원한다면, 스프링 웹플럭스 함수형 웹 엔드포인트를 사용할 수 있다. 또한 더 작은 어플리케이션이나, 더 훌륭한 명료성과 제어(control)라는 이점을 보다 적은 복잡도로 제공하는 마이크로서비스에도 스프링 웹플럭스는 좋은 선택지가 된다.
  • 마이크로서비스 아케텍처에서 스프링 MVC 또는 스프링 웹플럭스 컨트롤러 또는 스프링 웹플럭스 함수형 엔드포인트 각각으로 만들어진 서로 다른 어플리케이션을 혼합하여 사용할 수 있다. 이 두 프레임워크에가 동일한 어노테이션 기반 프로그래밍 모델을 지원한다는 점은 올바른 도구를 적재적소에 사용하는 동시에 기존 지식을 재사용하기 쉽게 만들어준다.
  • 어플리케이션을 평가하는 간단한 방법은 어플리케이션의 의존성을 확인하는 것이다. 블로킹 영속성 API(JPA, JDBC) 또는 네트워킹 API를 사용하고 있다면, 스프링 MVC는 적어도 공통 아키텍처에 있어서는 최고의 선택이 된다. 스프링 MVC는 기술적으로 개별 쓰레드에 대해 리액터와 RxJava를 사용하여 블로킹 호출을 수행하는 것이 가능하지만, 논 블로킹 웹 스택을 최대한 활용하지는 못한다.
  • 기존 스프링 MVC어플리케이션이 원격 서비스 호출(remoting)을 수행한다면, 리액티브 WebClient(이하 웹클라이언트)를 사용해보라. 스프링 MVC 컨트롤러 메서드로부터 리액티브 타입(Reactor, RxJava, 기타 등)을 반환값으로 직접 얻을 수 있다. 호출 당, 혹은 호출 간 상호 작용의 지연이 클수록, 더욱 드라마틱한 이점을 얻을 수 있다. 스프링 MVC 컨트롤러는 다른 리액티브 컴포넌트를 똑같이 호출할 수 있다.
  • 팀의 규모가 크다면, 논 블로킹, 함수형, 선언적 프로그래밍으로의 전환에 따르는 학습 곡선이 가파르다는 점을 유념해야 한다. 전체를 바꾸지 않고 시작하는 실용적인 방법은 리액티브 웹클라이언트를 사용하는 것이다. 이걸 넘어서면 작은 것부터 시작해보고, 이로부터 얻은 장점을 측정해보라. 대부분의 어플리케이션의 경우 이 전환이 필수적이지는 않다고 본다. 얻고자 하는 장점이 무엇인지 불확실하다면, 논 블로킹 I/O가 어떻게 작동하는지, 그리고 이것의 효과가 무엇인지 배우는 것에서 시작하도록 한다(예로, 싱글 쓰레드 Node.js의 동시성 처리가 있다).


1.1.5. 서버(Servers)

스프링 웹플럭스는 톰캣, 제티, 서블릿 3.1+ 컨테이너뿐만 아니라 네티, 언더토와 같은 논 서블릿 런타임에서도 지원된다. 다양한 서버에 고수준 프로그래밍 모델을 지원하기 위해, 모든 서버에는 로우 레벨 공통 API가 적용되어 있다.

스프링 웹플럭스에는 서버를 시작하고 정지하기 위한 내장형 기능은 없다. 하지만 스프링 설정과 웹플럭스 인프라스트럭처로 어플리케이션을 조립하기란 어렵지 않은 일이다. 그리고 단 몇 줄의 코드만으로 이 어플리케이션을 구동할 수 있다.

스프링 부트는 웹플럭스 스타터를 내장하고 있다. 웹플럭스 스타터는 이 과정을 자동화한다. 스타터는 기본 설정으로 네티를 사용하지만, 메이븐이나 그레들을 이용한 의존성 변경을 통해서 톰캣, 제티, 언더토 등 다른 서버를 사용하도록 쉽게 변경할 수 있다. 스프링 부트가 기본 설정으로 네티를 사용하는 이유는, 네티는 비동기, 논 블로킹 영역에서 폭넓게 사용되며 클라이언트과 서버가 자원을 공유하도록 하기 때문이다.

톰캣과 제티는 스프링 MVC와 웹플럭스 모두와 함께 사용할 수 있다. 그러나 이 둘은 그 작동 방식이 매우 다르다는 점을 유의해야 한다. 스프링 MVC는 서블릿 블로킹 I/O에 기반을 두며, 필요에 따라 어플리케이션이 서블릿 API를 직접 사용하도록 한다. 스프링 웹플럭스는 서블릿 3.1 논 블로킹 I/O에 기반을 두며, 로우 레벨 어댑터 뒷단에서 서블릿 API를 사용하며 이를 직접적으로 노출하지 않는다.

언더토의 경우 스프링 웹플럭스는 서블릿 API가 아닌 언더토 API를 직접 사용한다.

1.1.6. 퍼포먼스(Performance)

퍼포먼스는 많은 의미를 내포하고 있다. 리액티브와 논 블로킹은 어플리케이션을 더 빠르게 만들어주지 않는다. 몇몇 경우에 한하여 더 빨라질 수는 있다(예로, 병렬로 웹클라이언트를 사용하여 원격 호출을 실행할 때). 대체로 논 블로킹 방식은 더 많은 작업량을 필요로 하며, 이는 요청 처리 시간을 약간 늘어나게 할 수 있다.

리액티브와 논 블로킹을 사용할 때의 중요한 이점은 적고 고정된 수의 쓰레드와 보다 적은 메모리를 사용하도록 조정할 수 있는 능력에 있다. 이는 어플리케이션이 부하에 대해 더 탄력적으로 동작할 수 있도록 한다. 왜냐하면 보다 예측할 수 있는 방법으로 조정되기 때문이다. 하지만 이 스케일링을 관측하기 위해서는 약간의 지연을 필요로 한다(느리고 예측 불가능한 네트워크 I/O의 혼재로 인해). 여기서 리액티브 스택의 장점을 볼 수 있으며, 그 차이가 드라마틱하게 드러나는 지점이다.

1.1.7. 동시성 모델(Concurrency Model)

스프링 MVC와 스프링 웹플럭스는 모두 어노테이티드 컨트롤러를 지원하지만, 동시성 모델 및 블로킹과 쓰레드에 대한 기본적인 상정에 중요한 차이가 있다.

스프링 MVC(그리고 일반적인 서블릿 어플리케이션)에서는 어플리케이션은 현재 쓰레드가 블로킹될 것을 상정한다(예로, 원격 호출에 대하여). 그리고 이로 인하여 서블릿 컨테이너는 요청을 핸들링하는 동안 발생할 수 있는 잠재적인 블로킹에 대비하기 위해 큰 수의 쓰레드 풀을 사용하게 된다.

스프링 웹플럭스(그리고 일반적인 논 블로킹 서버에서)에서는 어플리케이션은 쓰레드를 블로킹 하지 않을 것을 상정한다. 따라서 논 블로킹 서버는 적고 고정된 크기의 쓰레드 풀을 사용하여 요청을 처리한다(이벤트 루프 워커).

# "스케일링" 과 "적은 수의 쓰레드" 는 모순으로 보일 수 있지만, 현재 쓰레드가 절대 블로킹되지 않는다는 것은 블로킹 호출을 받아들일 추가 쓰레드가 필요하지 않다는 의미가 된다.

블로킹 API 실행하기

블로킹 라이브러리를 사용해야 한다면? 리액터와 RxJava는 publishOn 연산자를 제공하여 다른 쓰레드가 처리하도록 한다. 이는 쉬운 대안이 될 수 있지만 블로킹 API는 이 동시성 모델에 잘 어울리지 않음을 유념해야 한다.

가변 상태(Mutable State)

리액터와 RxJava에서는 연산자를 통해서 로직을 선언한다. 그리고 런타임에 데이터가 순차적으로, 뚜렷한 단계로 처리되는 곳에서 리액티브 파이프라인이 형성된다. 여기서의 중요한 이점은 어플리케이션이 가변 상태를 보호할 필요가 없다는 점이다. 왜냐하면 이 파이프라인 안의 어플리케이션 코드는 절대로 동시에 실행되지 않기 때문이다.

쓰레딩 모델

스프링 웹플럭스로 구동되는 서버에서는 어떤 쓰레드를 볼 수 있는가?
  • 순수 스프링 웹플럭스 서버(예로, 데이터 접근이나 다른 선택적인 의존성이 존재하지 않는)에서는, 서버를 위한 쓰레드 하나, 요청을 처리하기 위한 쓰레드 여럿을 예상할 수 있다(보통 CPU 코어의 수가 쓰레드의 수가 된다). 그러나 서블릿 컨테이너의 경우, 서블릿 블로킹 I/O와 서블릿 3.1 논 블로킹 I/O를 모두 지원하기 위해 더 많은 수의 쓰레드가 사용될 수 있다(톰캣에서는 10개).
  • 이벤트 루프 방식의 리액티브 웹클라이언트 연산자. 적고 고정된 수의 요청 처리 쓰레드가 사용된다(예로, 리액터 네티 커넥터와 사용되는 reactor-http-nio-). 리액터 네티가 클라이언트와 서버 모두에서 쓰인다면, 이 둘은 기본적으로 이벤트 루프 자원을 공유한다.
  • 리액터와 RxJava는 Schedulers(이하 스케쥴러)라는 쓰레드 풀 추상화를 제공하여 publishOn 연산자와 함께 사용하며 다른 쓰레드 풀으로 처리를 전환한다. 스케쥴러는 특정한 동시성 전략을 제안한다. -예로, "parallel"(CPU 바운드 동작에는 제한된 수의 쓰레드 사용), 또는 "elastic"(I/O 바운드 동작에는 큰 수의 쓰레드 사용). 이러한 쓰레드를 본다는 것은 프로그램 코드가 특정 스케쥴러 전략을 사용하고 있음을 의미한다.
  • 데이터 접근 라이브러리와 써드파티 의존성은 각자 스스로의 쓰레드를 생성하고 사용할 수 있다.


설정하기

스프링 프레임워크는 서버를 시작하고 정지하는 기능을 제공하지 않는다. 서버의 쓰레딩 모델을 설정하기 위해서는 서버에 특화된 설정 API를 사용해야 한다. 스프링 부트를 사용한다면 스프링 부트의 서버 옵션을 확인하고, 웹클라이언트는 직접 설정 가능하다. 다른 라이브러리에 대해서는 각각의 레퍼런스 문서를 참조하도록 한다.

1.2. 리액티브 코어(Reactive Core)

spring-web 모듈은 리액티브 웹 어플리케이션 지원을 위한 다음의 제반 사항을 포함한다:
  • 서버의 요청 처리에 대해서는 두 가지 레벨의 지원이 있다.
    • HttpHandler: HTTP 요청 핸들링을 위한 논 블로킹 I/O와 리액티브 스트림 기반의 기본 핸들러. 리액터 네티, 언더토, 톰캣, 제티, 서블릿 3.1+ 컨테이너를 지원하는 어댑터와 함께 작동한다.
    • WebHandler API: 요청 처리를 위한 좀 더 고수준의, 다용도 웹 API. 어노테이티드 컨트롤러와 함수형 엔드포인트와 같은 구체적인 프로그래밍 모델 위에 존재한다.
  • 클라이언트 사이드에는 논 블로킹 I/O, 리액티브 스트림 백프레셔로 HTTP 요청을 수행하기 위한 기본 ClientHttpConnector 계약이 있다. 리액터 네티 및 리액티브 제티 HttpClient를 위한 어댑터를 사용해 작동한다. 여기에는 더 고수준의 웹클라이언트가 사용된다.
  • 클라이언트와 서버는 HTTP 요청과 응답 컨텐츠를 시리얼라이징/디시리얼라이징하기 위해 코덱을 사용한다.


1.2.1. HttpHandler

HttpHandler는 요청과 응답을 처리하는 싱글 메서드를 가진 단순한 계약이다. 의도적으로 작게 설계되었으며, 이것의 목적은 오로지 각기 다른 HTTP 서버 API에 대응하는 최소형의 추상화이다.

다음 테이블은 지원되는 서버 API에 대해 기술한다:

서버 사용되는 서버 API 리액티브 스트림 지원
네티 네티 API 리액터 네티
언더토 언더토 API spring-web: 언더토 to 리액티브 스트림 브릿지
톰캣 서블릿3.1 논 블로킹 I/O; byte[]에 대응하여 ByteBuffer를 읽고 쓰는 톰캣 API spring-web: 서블릿3.1 논 블로킹 I/O to 리액티브 스트림 브릿지
제티 서블릿3.1 논 블로킹 I/O; byte[]에 대응하여 ByteBuffer를 읽고 쓰는 제티 API spring-web: 서블릿3.1 논 블로킹 I/O to 리액티브 스트림 브릿지
서블릿3.1 컨테이너 서블릿3.1 논 블로킹 I/O spring-web: 서블릿3.1 논 블로킹 I/O to 리액티브 스트림 브릿지


다음 테이블은 서버 의존성에 대해 기술한다(지원되는 버전):

서버 그룹 아티팩트
리액터 네티 io.projectreactor.netty reactor-netty
언더토 io.undertow undertow-core
톰캣 org.apache.tomcat.embed tomcat-embed-core
제티 org.eclipse.jetty jetty-server, jetty-servlet


아래 코드 스니펫은 각 서버 API로 HttpHandler 어댑터를 사용하는 예제이다:

리액터 네티

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


언더토

Java
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
Kotlin
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()


톰캣

Java
HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);

Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();
Kotlin
val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)

val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()


제티

Java
HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);

Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();

ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
Kotlin
val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)

val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();

val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()


서블릿 3.1+ 컨테이너

어플리케이션을 WAR로 서블릿 3.1+ 컨테이너에 배포하려면 AbstractReactiveWebInitializer를 확장하여 WAR 안에 포함해야 한다. 이 클래스는 HttpHandler를 ServletHttpHandlerAdapter로 래핑하고 이를 서블릿으로 등록한다.

1.2.2. WebHandler API

다수의 WebExceptionHandlerWebFilter, 단일 WebHandler 컴포넌트를 통해 요청을 처리하는 다목적 웹 API 제공을 위해, org.springframework.web.server 패키지는 HttpHandler를 기반으로 한다. 이 컴포넌트들은 스프링 ApplicationContext로 참조되어 WebHttpHandlerBuilder와 함께 자동으로 감지되거나 등록되어 사용된다.

HttpHandler의 목적은 서로 다른 HTTP 서버에서의 사용을 위한 추상화에 있기에, WebHandler API는 웹 어플리케이션에서 일반적으로 사용되는 기능들보다 더 많은 기능을 제공한다. 웹 어플리케이션에서 일반적으로 사용되는 기능들은 다음과 같다:
  • 사용자 세션(세션 어트리뷰트)
  • 리퀘스트 어트리뷰트
  • 요청에 대한 리졸빙된 Locale or Principal
  • 파싱되고 캐싱된 폼 데이터 접근
  • 멀티파트 데이터 추상화
  • 기타 등등..


스페셜 빈 타입



아래 테이블은 스프링 ApplicationContext에서 자동으로 감지되는, 혹은 직접 등록되는, WebHttpHandlerBuilder가 참조하는 컴포넌트 목록이다:

빈 이름 빈 타입 카운트 설명
<any> WebExceptionHandler 0..N WebFilter 인스턴스들과 타겟 WebHandler에서 발생한 익셉션에 대한 핸들링을 제공한다. 자세한 내용은 Exceptions를 참조한다.
<any> WebFilter 0..N 인터셉터 스타일 로직을 적용하여 타겟 WebHandler의 전/후 동작을 담당한다. 자세한 내용은 Filters를 참조한다.
webHandler WebHandler 1 요청을 처리한다.
webSessionManager WebSessionManager 0..N ServerWebExchange의 메서드를 통해 노출된 WebSession 인스턴스를 관리한다. 디폴트는 DefaultWebSessionManager.
serverCodecConfigurer ServerCodecConfigurer 0..1 HttpMessageReader로 접근하여 ServerWebExchange의 메서드를 통해 노출된 폼 데이터와 멀티파트 데이터를 파싱하는 컴포넌트. 디폴트는 ServerCodecConfiguter.create()
localeContextResolver LocaleContextResolver 0..1 ServerWebExchange의 메서드를 통해 노출된 LocaleContext에 대한 리졸버. 디폴트는 AcceptHeaderLocaleContextResolver.
forwardedHeaderTransformer ForwardedHeaderTransformer 0..1 포워드 타입 헤더를 처리하기 위한 컴포넌트. 각 헤더는 추출과 제거 or 제거 only. 디폴트는 사용하지 않음.


폼 데이터



ServerWebExchange는 폼 데이터로의 접근을 위해 다음 메서드를 노출한다.

Java
Mono<MultiValueMap<String, String>> getFormData();
Kotlin
suspend fun getFormData(): MultiValueMap<String, String>


DefaultServerWebExchange는 설정된 HttpMessageReader를 사용하여 폼 데이터(application/x-www-form-urlencoded)를 MultiValueMap으로 파싱한다. 디폴트로 FormHttpMessageReader가 설정되어 ServerCodecConfigurer 빈이 이를 사용한다(Web Handler API를 보라).

멀티파트 데이터



ServerWebExchange는 멀티파트 데이터로의 접근을 위해 다음 메서드를 노출한다.

Java
Mono<MultiValueMap<String, Part>> getMultipartData();
Kotlin
suspend fun getMultipartData(): MultiValueMap<String, Part>


DefaultServerWebExchange는 설정된 HttpMessageReader<MultiValueMap<String, Part>>를 사용하여 multipart/form-data 내용을 MultiValueMap으로 파싱한다. 현재는 동기식 NIO 멀티파트가 유일하게 지원되는 써드파티 라이브러리이며, 멀티파트 요청을 논 블로킹으로 파싱한다. ServerCodeConfigurer 빈을 통해 활성화된다(Web Handler API 참조).

멀티파트 데이터를 스트리밍 방식으로 파싱하기 위해서는 HttpMessageReader가 반환하는 Flux<Part>를 사용한다. 예를 들어, 어노테이티드 컨트롤러에서 @RequestPart를 사용함은 Map 방식의, name을 사용한 개별 파트로의 접근을 암시한다. 따라서 이는 멀티파트 데이터 전체를 파싱해야 한다. 반면 @RequestBody를 사용하면 데이터를 MultiValueMap으로 모으지 않고 Flux<Part>로 디코딩할 수 있다.

포워디드 헤더(Forwareded Headers)



사용자 요청이 프록시를 통해 전송되면(예: 로드밸런서), 호스트, 포트, 스킴이 변경될 수 있고, 이는 클라이언트의 관점에서 정확한 호스트, 포트, 스킴으로의 링크를 생성하는 일을 방해한다.

RFC 7239는 Forwarded(이하 포워디드) HTTP 헤더를 정의한다. 이 헤더는 프록시가 원 요청 정보를 제공하기 위해 사용할 수 있다. 그리고 같은 목적으로 사용 가능한 X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl, X-Forwarded-Prefix와 같은 비표준 헤더들이도 있다.

ForwardedHeaderTransformer는 포워디드 헤더를 기반으로 요청의 호스트, 포트, 스킴을 변경하고 제거하는 컴포넌트이다. forwardedHeaderTransformer 빈으로 등록하면 감지되어 사용된다.

어플리케이션은 헤더가 프록시 혹은 악의적인 클라이언트에 의해 의도적으로 추가된 것인지 알 수 없기 때문에, 여기에 대한 보안 고려사항이 있다. 이것이 프록시가 신뢰의 경계에서 외부로부터 전송된 신뢰할 수 없는 트래픽을 제거하도록 설정되어야 하는 이유이다. ForwardedHeaderTransformer로 removeOnly=true 설정을 함으로써 헤더를 사용하지 않고 제거할 수 있다.

#ForwardedHeaderFilter는 버전 5.1 에서 deprecated 되면서 ForwardedHeaderTransformer로 대체되었다. 때문에 포워디드 헤더는 exchange의 생성 전에 먼저 처리될 수 있다. 필터가 설정되어 있다면 필터는 필터 목록에서 빠지고 대신 ForwardedHeaderTransformer가 사용된다.

1.2.3. 필터

WebHandler API에서는 WebFilter를 사용하여 WebHandler의 전/후처리 로직을 필터 체이닝 방식으로 적용할 수 있다. WebFlux Config를 사용하면 WebFilter를 등록하는 일은 스프링 빈을 등록하는 일만큼 간단하다. 그리고 빈 선언부에 @Order를 사용하거나 Ordered인터페이스를 구현하여 우선순위를 설정할 수 있다.

CORS



스프링 웹플럭스는 컨트롤러의 어노테이션을 통해 CORS설정을 잘 지원하기 위한 도구를 제공한다. 그러나 스프링 시큐리티와 함께 사용할 때는 내장된 CorsFilter를 사용할 것을 권한다. 이 필터는 반드시 스프링 시큐리티의 필터 체인보다 앞단에 적용되어야 한다.

더 자세한 정보는 CORSCORS WebFilter를 보라.

1.2.4. 익셉션(Exceptions)

WebHandler API에서는 WebExceptionHandler를 사용하여 WebFilter와 WebHandler에서 발생한 익셉션을 처리할 수 있다. WebFlux Config를 사용하면 WebExceptionHandler를 등록하는 일은 스프링 빈을 등록하는 일만큼 간단하다. 그리고 빈 선언부에 @Order를 사용하거나 Ordered인터페이스를 구현하여 우선순위를 설정할 수 있다.

다음 테이블은 사용할 수 있는 WebExceptionHandler구현체에 대해 기술한다:

익셉션 핸들러 설명
ResponseStatusExceptionHandler ResponseStatusException 타입 익셉션을 처리한다. 익셉션에 있는 HTTP 상태 코드를 응답에 세팅한다.
WebFluxResponseStatusExceptionHandler ResponseStatusExceptionHandler의 확장판. 익셉션 타입에 상관없이 @ResponseStatus의 HTTP 상태 코드를 결정한다. 이 핸들러는 WebFlux Config에서 선언한다.


1.2.5. 코덱

spring-web과 spring-core 모듈은 리액티브 스트림 백프레셔와 논 블로킹 I/O를 통해서 바이트 컨텐츠와 고수준 객체 사이의 시리얼라이징/디시리얼라이징을 지원한다.

  • EncoderDecoder는 HTTP와 독립적으로 콘텐츠를 인코딩/디코딩하기 위한 클래스이다.
  • HttpMessageReaderHttpMessageWriter는 HTTP 메시지 콘텐츠를 인코딩/디코딩하기 위한 클래스이다.
  • EncoderHttpMessageWriter는 Encoder를 래핑하여 웹 어플리케이션에 사용하고, 같은 이유로 DecoderHttpMessageReader는 Decoder를 래핑한다.
  • DataBuffer는 서로 다른 바이트버퍼들을 추상화한다.(네티의 ByteBuf, java.nio.ByteBuffer, 기타 등등) 그리고 모든 코덱은 여기서 작동한다. 이에 대해서는 "스프링 코어" 섹션의 Data Buffers and Codecs에서 더 알아볼 수 있다.


spring-core 모듈은 byte[], ByteBuffer, DataBuffer, Resource, String 인코더와 디코더 구현체를 제공한다. spring-web 모듈은 Jackson JSON, Jackson Smile, JAXB2, Protocol Buffers 그리고 기타 다른 인코더와 디코더를 제공한다. 이 인코더와 디코더는 폼 데이터, 멀티파트 컨텐츠, 서버 전송 이벤트 및 기타 웹 요청 처리를 위한 웹 전용 HTTP 메시지 reader와 writer 구현체와 함께한다.

ClientCodecConfigurer와 ServerCodecConfigurer는 어플리케이션에서의 코덱 사용 설정 및 커스터마이징을 위해 일반적으로 사용되는 클래스이다. 이 설정에 대해서는 HTTP message codecs에서 다룬다.

Jackson JSON



Jackson(이하 잭슨)라이브러리가 존재할 경우 JSON 과 바이너리 JSON(Smile) 이 지원된다.

Jackson2Decoder는 다음과 같이 동작한다:
  • 잭슨의 비동기, 논 블로킹 파서는 바이트 청크 스트림을 각각 JSON 객체를 나타내는 TokenBuffer로 종합하기 위해 사용된다.
  • 단일 값 퍼블리셔(Mono)를 디코딩하는 경우, 하나의 TokenBuffer가 존재한다.
  • 다수 값 퍼블리셔(Flux)를 디코딩하는 경우, 각 TokenBuffer는 완전하게 포맷팅된 객체가 되기 충분한 바이트를 받은 시점에 ObjectMapper에게 전달된다. 인풋 컨텐츠는 JSON 배열이 될 수 있고, 컨텐츠 타입이 "application/stream+json"인 경우 line-delimited JSON이 될 수 있다.
  • SSE의 경우 Jackson2Encoder는 이벤트마다 실행되며 그 아웃풋은 지연 없이 플러싱된다.


Jackson2Encoder는 다음과 같이 동작한다:
  • 단일 값 퍼블리셔(Mono)의 경우, 이를 간단히 ObjectMapper로 시리얼라이징한다.
  • 다수 값 "application/json" 퍼블리셔의 경우, 기본적으로 값들을 Flux#collectToList() 로 모은 뒤 그 결과를 시리얼라이징한다.
  • 다수 값 스트리밍 미디어 타입(예로, application/stream+json 또는 appliction/stream+x-jackson-smile) 퍼블리셔의 경우, line-delimited JSON 포맷을 사용하여 각 값을 독립적으로 인코딩하고, 쓰고, 플러싱한다.
  • SSE의 경우 Jackson2Encoder는 이벤트마다 실행되며 그 아웃풋은 지연 없이 플러싱된다.


#Jackson2Encoder와 Jackson2Decoder는 기본적으로 String 타입 요소를 지원하지 않는다. 대신 시리얼라이징된 JSON 컨텐츠는 한 문자 혹은 일련의 문자로 구성되어 있을 것을 상정하여 CharSequenceEncoder로 렌더링한다. Flux<String>으로 JSON 배열을 렌더링해야 한다면, Flux#collectToList()를 사용하고 Mono<List<String>>를 인코딩하라.

폼 데이터



"application/x-www-form-urlencoded" 컨텐츠의 인코딩 및 디코딩은 FormHttpMessageReader와 FormHttpMessageWriter가 처리한다.

다양한 곳으로부터의 폼 컨텐츠 접근이 자주 이루어지는 서버에서는 ServerWebExchange가 제공하는 getFormData() 메서드로 컨텐츠를 파싱한다. 이 파싱 작업은 FormHttpMessageReader를 통해 이루어지며, 반복되는 접근을 위해 파싱 결과를 캐싱한다. WebHandler API 섹션의 Form Data를 보라.

getFormData() 가 한 번 호출되면, 요청 본문에서 원본 컨텐츠는 더이상 읽을 수 없다. 이런 이유로 어플리케이션은 요청 본문에서 원본 컨텐츠를 읽는 대신 ServerWebExchange를 통해 캐싱된 폼 데이터로 접근하도록 한다.

멀티파트



"multipart/form-data" 컨텐츠의 인코딩 및 디코딩은 MultipartHttpMessageReader와 MultipartHttpMessageWriter가 처리한다. 결국 MultipartHttpMessageReader는 Flux<Part> 로의 실제 파싱 작업은 HttpMessageReader에게 위임하고, 간단히 그 결과를 MultiValueMap으로 모으기만 한다. 현재는 동기식 NIO 멀티파트가 실제 파싱에 사용된다.

다양한 곳으로부터의 멀티파트 컨텐츠 접근이 자주 이루어지는 서버에서는 ServerWebExchange가 제공하는 getMultipartData() 메서드로 컨텐츠를 파싱한다. 이 파싱 작업은 MultipartHttpMessageReader를 통해 이루어지며, 반복되는 접근을 위해 파싱 결과를 캐싱한다. WebHandler API 섹션의 Multipart Data를 보라.

getMultipartData() 가 한 번 호출되면, 요청 본문에서 원본 컨텐츠는 더이상 읽을 수 없다. 이런 이유로 어플리케이션은 반복적인 파트에의 맵 방식 접근에 대해서 getMultipartData()를 지속적으로 호출해야 하고, Flux<Part> 로의 한 번의 접근에 대해서는 SynchronossPartHttpMessageReader를 사용한다.

제한



Decoder와 HttpMessageReader 구현체는 인풋스트림을 버퍼링한다. 설정을 통해 메모리의 버퍼 바이트 사이즈의 최대치를 지정할 수 있다. 인풋 버퍼링이 발생할 수 있는 몇 가지 경우가 있다. 예를 들어 @RequestBody byte[] 나 x-www-form-urlencoded 데이터 및 기타 등등의 방식으로 데이터를 다루는 컨트롤러 메서드와 같이 인풋이 합쳐지고 하나의 객체로 나타나는 경우 버퍼링이 발생한다. 또한 스트리밍에서 인풋스트림을 분리할 때 버퍼링은 발생할 수 있다. 예를 들어 제한되지 않은 텍스트, JSON 객체 스트림 및 기타 등등의 경우가 있다. 이런 스트리밍의 경우 버퍼 바이트 사이즈 제한은 스트림 안의 하나의 객체에 연관되어 적용된다.

버퍼 사이즈를 설정하기 위해서는, 주어진 Decoder 또는 HttpMessageReader의 maxInMemorySize 프로퍼티 설정이 가능한지 확인하고, 가능하다면 관련 자바독에 기본값에 대한 상세 정보가 있을 것이다. 웹플럭스에는 ServerCodecConfigurer가 기본 코덱의 maxInMemorySize 프로퍼티를 통해 모든 코덱을 설정할 수 있는 단일한 위치를 제공한다. 클라이언트 단에서는 WebClientBuilder로 이 사이즈 제한을 변경할 수 있다.

maxInMemorySize 프로퍼티는 멀티파트 파싱에 적용되는 non-file 파트의 사이즈를 제한한다. 파일 파트에서는 디스크에의 파일 쓰기(writing) 작업의 임계치가 된다. 이 쓰기 작업에는 추가적으로 maxDiskUsagePerPart 프로퍼티가 있다. 이 프로퍼티는 파트 당 디스크의 크기에 대한 제한을 설정한다. 또, maxParts 프로퍼티는 하나의 멀티파트 요청의 전체 사이즈에 대한 제한을 설정한다. 웹플럭스에서 이 3 가지 프로퍼티를 모두 설정하려면 미리 설정된 MultipartHttpMessageReader 인스턴스를 ServerCodecConfigurer에게 설정해야 한다.

스트리밍



text/event-stream, application/stream+json 과 같은, HTTP 응답 스트리밍 시에는 연결이 끊어진 클라이언트를 가능한 빠르게 감지하기 위해 데이터를 주기적으로 전송하는 것이 중요하다. 이 한 번의 전송에는 코멘드만 담길 수도 있고, 비어 있는 SSE 이벤트가 될 수도 있다. 아니면 다른 어떤한 "무동작 명령(no-op)" 데이터든 가능하다. 이 데이터는 서버의 신호(heartbeat) 역할을 한다.

DataBuffer



DataBuffer는 웹플럭스의 바이트버퍼이다. 스프링 코어의 데이터 버퍼와 코덱에서 이에 관한 더 많은 정보를 얻을 수 있다. 여기서 핵심이 되는 부분은, 네티와 같은 서버에서는 바이트버퍼는 풀(pool)으로 관리되며, 참조는 카운팅된다. 그리고 소비(consume) 후에는 반드시 릴리즈하여 메모리 릭을 방지해야 한다.

데이터 버퍼를 직접 소비하거나 생성하지 않는 이상, 혹은 커스텀 코덱을 만들어 사용하지 않는 한, 아니면 반대로 더 고수준 객체들로의/로부터의 컨버팅 작업에 코덱을 사용하는 한은, 일반적으로 웹플럭스 어플리케이션은 이런 이슈로부터 자유롭다. 이런 경우에 대해서는 데이터 버퍼와 코덱를 다시 한 번 보길 바란다. 특히 DataBuffer 사용하기 섹션을 권한다.

1.2.6. 로깅

스프링 웹플럭스에서 DEBUG 레벨 로깅은 가볍게, 최소한으로, 사람에게 친화적으로 작성된다. 특정 문제를 디버깅할때만 유용한 다른 정보에 비해서 지속적으로 유용한 가치있는 정보에 중점을 둔다.

TRACE 레벨 로깅은 일반적으로 DEBUG와 같은 원칙을 따르지만, 어떠한 디버깅에도 쓰일 수 있다. 그리고 어떤 로그 메시지들은 TRACE와 DEBUG 레벨에 대해 각기 다른 수준의 디테일을 보인다.

좋은 로깅이란 로그 사용 경험으로부터 나온다. 로깅에 있어 위에 공언된 목표에 부합하지 않는 것이 보이면 알려주기 바란다.

Log Id



웹플럭스에서는 하나의 요청을 처리하는 데에 다수의 쓰레드가 작동할 수 있기에, 특정 요청에 대한 로그 메시지들간의 연관성을 찾는 데에 있어 쓰레드ID는 유용하지 못하다. 이런 이유로 웹플럭스 로그 메시지는 기본적으로 요청 전용ID를 접두어로 둔다.

서버 측에서는 로그ID는 ServerWebExchange의 어트리뷰트(LOG_ID_ATTRIBUTE) 로 저장된다. ServerWebExchange#getLogPrefix()를 통해 완전히 포맷팅된 ID를 얻을 수 있다. WebClient 측에서는 로그ID는 ClientRequest의 어트리뷰트(LOG_ID_ATTRIBUTE)로 저장된다. ClientRequest#logPrefix()를 통해 완전히 포맷팅된 ID를 얻을 수 있다.

민감한 데이터



DEBUG와 TRACE 로깅은 민감한 정보를 남길 수 있다. 때문에 폼 파라미터와 헤더는 기본적으로 마스킹되어야 하며, 전체 로깅 활성화는 명시적으로 이루어져야 한다.

다음 예제는 서버측 요청에 대한 상세 로그 설정 코드이다:

Java
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true);
    }
}
Kotlin
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true)
    }
}


다음 예제는 클라이언트측 요청에 대한 상세 로그 설정 코드이다:

Java
Consumer<ClientCodecConfigurer> consumer = configurer ->
        configurer.defaultCodecs().enableLoggingRequestDetails(true);

WebClient webClient = WebClient.builder()
        .exchangeStrategies(strategies -> strategies.codecs(consumer))
        .build();
Kotlin
val consumer: (ClientCodecConfigurer) -> Unit  = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }

val webClient = WebClient.builder()
        .exchangeStrategies({ strategies -> strategies.codecs(consumer) })
        .build()


커스텀 코덱



지원되는 미디어타입을 추가하거나 기본 코덱에서 지원되지 않는 동작을 지원하기 위해 어플리케이션에 커스텀 코덱을 등록할 수 있다.

개발자가 조정 가능한 몇가지 설정 옵션은 기본 코덱에 적용된다. 커스텀 코덱은 버퍼링 사이즈 제한 혹은 민감한 데이터 로깅처럼 이러한 기호(preferences)를 필요로 할 수 있다.

다음 예제는 클라이언트측 요청에 대한 커스텀 코덱 설정이다:

Java
WebClient webClient = WebClient.builder()
        .codecs(configurer -> {
                CustomDecoder decoder = new CustomDecoder();
                configurer.customCodecs().registerWithDefaultConfig(decoder);
        })
        .build();
Kotlin
val webClient = WebClient.builder()
        .codecs({ configurer ->
                val decoder = CustomDecoder()
                configurer.customCodecs().registerWithDefaultConfig(decoder)
         })
        .build()


1.3. DispatcherHandler

스프링 웹플럭스는 스프링 MVC와 유사한 프론트 컨트롤러 패턴으로 설계되었다. 중앙의 WebHandler, DispatcherHandler는 요청 처리 알고리즘이 동일하다. 실제 작업은 설정 가능한 위임 컴포넌트에 의해 이루어진다. 이 모델은 유연하며, 다양한 형태의 작업 흐름을 지원한다.

DispatcherHandler는 스프링 설정으로부터 필요한 위임 컴포넌트를 찾는다. 스프링 빈으로 만들어졌으며, ApplicationContextAware를 구현하여 자신이 속한 컨텍스트에 접근한다. DispatcherHandler의 빈 이름이 webHandler으로 선언되면 결국 WebHttpHandlerBuilder에 의해 발견되어 사용된다. WebHttpHandlerBuilder는 WebHandler API에 따라 요청 처리 체인을 구성한다.

웹플럭스 어플리케이션의 스프링 설정은 보통 다음의 사항을 포함한다:

이 설정은 WebHttpHandlerBuilder에게 주어져 요청 처리 체인이 만들어진다. 다음은 그 예제이다:

Java
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
Kotlin
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()


위 코드 결과의 HttpHandler는 서버 어댑터와 함께 사용된다.

1.3.1. 스페셜 빈 타입

DispatcherHandler는 요청을 처리하고 적절한 응답을 주기 위해 스페셜 빈으로 작업을 위임한다. "스페셜 빈" 이란 웹플럭스 프레임워크의 요소를 구현한 스프링으로 관리되는 객체 인스턴스를 의미한다. 기본 내장 형태(built-in)가 보통이지만, 그 속성값을 변경하거나 빈을 확장(extend)하거나 다른 빈으로 대체(replace)하는 일도 가능하다.

다음 테이블은 DispatcherHandler에 의해 감지되는 스페셜 빈 목록을 보여준다. 로우 레벨에서는 이 밖에 다른 빈들도 존재한다.(웹 핸들러 API의 Special bean types를 보라).

빈 타입 설명
HandlerMapping 요청을 핸들러에 매핑한다. 매핑은 몇 가지 조건에 기반하여 이루어진다. 이 조건은 HandlerMapping 구현체에 따라 달라진다. -어노테이티드 컨트롤러, 단순 URL 패턴 매핑, 기타 등 핸들러.

@RequestMapping 적용 메서드에 대한 주요 HandlerMapping 구현체는 RequestMappingHandlerMapping, 함수형 엔드포인트 라우팅에 대해서는 RouterFunctionMapping, 명시적 URI 경로 패턴 및 WebHandler 인스턴스 등록에 대해서는 SimpleUrlHandlerMapping이 된다.
HandlerAdapter DispatcherHandler가 요청에 매핑된 핸들러를 실행하는 작업을 돕는다. 실제로 핸들러가 어떤 방식으로 실행되는지는 상관없다. 예를 들어, 어노테이티드 컨트롤러는 어노테이션 리졸빙을 필요로한다. HandlerAdapter의 주 목적은 DispatcherHandler를 그러한 구체적인 부분으로부터 분리하는 것이다.
HandlerResultHandler 핸들러의 실행 결과를 처리하여 응답을 마친다. Result Handling에서 다룬다.


1.3.2. 웹플럭스 설정

어플리케이션의 요청 처리의 기반이 되는 빈을 선언할 수 있다.(Web Handler APIDispatcherHandler에 나열된 빈) 그러나 대부분의 경우에 있어 WebFlux Config는 최고의 시작점이 된다. 필요한 빈을 선언하고, 커스터마이징을 위한 더 고수준의 콜백 API 설정을 제공한다.

#스프링 부트는 웹플럭스 설정을 통해 스프링 웹플럭스를 설정하며, 또한 많은 편리한 옵션을 추가로 제공한다.

1.3.3. 처리

DispatcherHandler는 다음에 따라 요청을 처리한다:
  • 각 HandlerMapping에게 매칭 핸들러를 요청한다. 처음 매칭된 핸들러가 사용된다.
  • 핸들러를 찾으면, 적절한 HandlerAdapter를 통해 이 핸들러를 실행한다. 핸들러 실행의 반환값은 HandlerResult이다.
  • HandlerAdapter 로부터 반환된 HandlerResult는 적절한 HandlerResultHandler에게 주어지며, 직접 응답을 주거나, 혹은 뷰를 사용하여 요청 처리를 완료한다.


1.3.4. 결과 핸들링

HandlerAdapter로 핸들러를 실행하여 나온 반환값은 몇몇 추가적인 컨텍스트와 함께 HandlerResult로 래핑된다. 그리고 이 반환값은 HandlerResult 핸들링을 지원하는 첫 HandlerResultHandler에게 전달된다. 다음 테이블은 사용 가능한 HandlerResultHandler 구현체를 보여준다. 여기에 나온 모든 구현체는 WebFlux Config에서 정의한다:

타입 반환값 디폴트 적용 순서
ResponseEntityResultHandler ResponseEntity, 보통 @Controller 인스턴스로부터 반환된다. 0
ServerResponseResultHander ServerResponse, 보통 함수형 엔드포인트로부터 반환된다. 0
ResponseBodyResultHandler @ResponseBdoy 메서드나 @RestController 클래스로부터의 반환값을 핸들링한다. 100
ViewResolutionResultHandler CharSequence, View, Model, Map, Rendering, 이외 모델 어트리뷰트로 취급되는 Object.

View Resolution에서 다룬다.
Integer.MAX_VALUE


1.3.5. 익셉션

HandlerAdapter 로부터 반환된 HandlerResult는 몇가지 핸들러 특징적인 메커니즘에 기반한 에러 핸들링 함수를 제공한다. 이 에러 핸들링 함수가 호출되는 조건은 다음과 같다:
  • 핸들러 실행 실패
  • HandlerResultHandler의 핸들러 결과값 핸들링 실패


핸들러가 반환한 리액티브 타입이 데이터를 생성하기 전에 에러 시그널이 발생하게 되면 이 에러 핸들링 함수는 응답을 변경할 수 있다.(예로, 에러 상태 코드)

이것이 @Controller 클래스의 @ExceptionHandler 메서드가 작동하는 방식이다. 반면 스프링 MVC는 HandlerExceptionResolver를 기반으로 한다. 이 차이점은 보통 큰 상관이 없지만, 웹플럭스에서는 핸들러가 선택되기 전에 발생한 익셉션은 @ControllerAdvice으로 핸들링할 수 없음은 유념해야 한다.

더한 내용은 "어노테이티드 컨트롤러" 섹션의 Managing Exceptions 또는 웹 핸들러 API 섹션의 Exceptions에서 다룬다.

1.3.6. 뷰 리솔루션

뷰 리솔루션은 특정한 뷰 기술에 구애받지 않으면서 HTML 템플릿과 모델을 이용하여 브라우저에 응답을 렌더링하도록 한다. 스프링 웹플럭스의 뷰 리솔루션은 HandlerResultHandler을 통해 이루어진다. HandlerResultHandler는 VeiwResolver 인스턴스를 사용하여 논리적 뷰 이름을 나타내는 String을 View 인스턴스로 매핑한다. 이 View는 응답 렌더링에 사용된다.

핸들링



ViewResolutionResultHandler로 전달된 HandlerResult는 핸들러의 반환값과 모델이 가진, 요청 처리 과정에서 추가된 어트리뷰트를 포함한다. 이 반환값은 다음과 같이 처리된다:
  • String, CharSequence: 논리적 뷰 이름으로, 설정된 ViewResolver 구현체 목록에서 View를 가져온다.
  • void: 요청 경로에 기반하여 기본 디폴트 뷰 이름을 선택한다. 경로 처음과 끝 슬래시를 뺀 값을 View로 한다. 반환된 뷰 이름이 없을 경우나(예를 들어, 모델 어트리뷰터가 반환되지 않을 때), 값이 비동기 반환값일 때도 똑같이 동작한다(예를 들어, Mono가 빈 값으로 완료될 때).
  • Rendering: 뷰 리솔루션 시나리오 API. 통합개발환경(IDE)의 코드 컴플리션의 옵션을 탐색한다.
  • Model, Map: 추가적인 모델 어트리뷰트로, 요청의 모델에 추가된다.
  • 그 외: BeanUtils#isSimpleProperty 에 의해 결정되는 단순 타입을 제외하고, 이외의 반환값은 요청의 모델에 추가되는 모델 어트리뷰트로 취급된다. @ModelAttribute 핸들러 메서드가 아니라면, 어트리뷰트 이름은 conventions를 사용하여 클래스 이름으로부터 얻어진다.


모델은 비동기, 리액티브 타입을 포함할 수 있다(예로, Reactor 또는 RxJava 로부터). AbstractView는 이러한 모델 어트리뷰트를 구체적인 값으로 리졸빙하고 그 모델을 업데이트한다. 이 작업은 뷰 렌더링 작업 전에 이루어진다. 단일 값 리액티브 타입은 단일 값 혹은 빈 값(비어있다면)으로 리졸빙되며, Flux<T> 와 같은 다수 값 리액티브 타입은 모아져(collected) List<T> 로 리졸빙된다.

뷰 리솔루션 설정은 스프링 설정에 ViewResolutionResultHandler 빈을 추가하는 것 만큼 쉽다. WebFlux Config는 뷰 리솔루션 전용 설정을 제공한다.

스프링 웹플럭스와 통합된 뷰 기술에 대해서는 View Technologies에서 더 많은 정보를 찾아볼 수 있다.

리다이렉팅



뷰 네임의 접두어로 쓰이는 redirect: 는 리다이렉팅을 수행한다. UrlBasedViewResolver(그리고 그 서브클래스들) 은 이 접두어를 리다이렉팅 요청으로 받아들인다. 접두어를 제외한 나머지 부분이 리다이렉팅 경로 URL이 된다.

redirect: 의 리다이렉팅에 있어서의 효과는 컨트롤러가 RedirectView 또는 Rendering.redirectTo("abc").build()를 반환한 것과 동일하지만, 이렇게 할 경우 컨트롤러는 자체적으로 뷰 이름에 관한 연산이 가능해진다. redirect:/some/resource/ 와 같은 뷰 이름은 현재 어플리케이션으로 연결되지만, redirect:https://example.com/arbitrary/path는 절대 경로 URL으로 리다이렉팅한다.

컨텐츠 협상



ViewResolutionResultHandler는 컨텐츠 협상을 지원한다. 요청 미디어 타입을 선택된 View가 지원하는 미디어 타입과 비교한다. 그리고 요청된 미디어 타입을 지원하는, 첫 번째로 발견된 View가 사용된다.

JSON, XML 과 같은 미디어 타입을 지원하기 위해, 스프링 웹플럭스는 HttpMessageWriterView를 제공한다. 이 특별한 View는 HttpMessageWriter를 통해 렌더링 작업을 수행한다. 보통 WebFlux Configuration을 통해 이 View를 디폴트 뷰로 설정하게 된다. 요청 미디어 타입에 매칭될 경우, 언제나 디폴트 뷰가 선택되어 사용된다.

1.4. 어노테이티드 컨트롤러

스프링 웹플럭스는 어노테이션 기반 프로그래밍 모델을 제공한다. @Controller와 @RestController 컴포넌트는 어노테이션을 사용하여 요청 매핑, 요청 인풋, 익셉션 핸들링 그리고 기타 필요한 작업을 지정한다. 어노테이티드 컨트롤러는 유연한 메서드 시그니처를 가지며, 기반 클래스를 확장(extend)하거나 특정 인터페이스를 구현할 필요가 없다.

다음은 어노테이티드 컨트롤러의 기본적인 예제이다:

Java
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String handle() {
        return "Hello WebFlux";
    }
}
Kotlin
@RestController
class HelloController {

    @GetMapping("/hello")
    fun handle() = "Hello WebFlux"
}


위 예제의 메서드는 String를 반환하고, 이 반환값은 요청 본문에 쓰인다.

1.4.1 @Controller

표준 스프링 빈 정의에 따라 컨트롤러 빈을 정의할 수 있다. @Controller 스테레오타입은 클래스패스의 @Component 클래스 자동 감지 및 빈 등록을 허용한다. 또한 웹 컴포넌트임을 나타내는 어노테이션 적용 클래스의 스테레오 타입 역할을 한다.

아래와 같이, 자바 설정에 컴포넌트 스캐닝을 추가하여 @Controller와 같은 빈의 자동 감지를 활성화한다:

Java
@Configuration
@ComponentScan("org.example.web") (1)
public class WebConfig {

    // ...
}
Kotlin
@Configuration
@ComponentScan("org.example.web") (1)
class WebConfig {

    // ...
}


(1) org.example.web 패키지를 스캐닝한다.

@RestController는 스스로 @Contoller와 @ResponseBody를 적용하는 composed annotation이다. 타입 레벨으로 모든 메서드에 @ResponseBody가 적용되는 컨트롤러임을 나타내며, 고로 뷰 리솔루션와 HTML 템플릿 렌더링을 수행하지 않고 응답 본문을 직업 작성한다.

1.4.2. 요청 매핑

@RequestMapping 어노테이션은 컨트롤러에의 메서드와 요청을 매핑하기 위해 사용된다. 이 어노테이션은 URL, HTTP 메서드, 요청 파라미터, 헤더, 미디어 타입 각각으로 요청을 매핑하기 위한 다양한 어트리뷰트를 가지고 있다. 이 어노테이션은 클래스 레벨에 적용하여 메서드들의 매핑 공유에 사용할 수도 있고, 메서드 레벨에 적용하여 특정 엔드포인트로의 매핑을 지정할 수도 있다.

다음 어노테이션들은 HTTP 메서드로 요청을 매핑하는 @RequestMapping의 변형이다:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping


위 어노테이션들은 함께 제공되는 Custom Annotations이다. 이런 어노테이션들이 제공되는 이유는, 대부분의 컨트롤러에 있어서 @RequestMapping을 기본 형태로 사용하여 HTTP 메서드를 고려하지 않는 URL 매핑을 적용하기 보다는 특정 HTTP 메서드로의 매핑을 적용하는 것이 바람직하기 때문이다. 동시에 @RequestMapping은 클래스 레벨로 적용하여 매핑 범위를 공유하는 데에 사용할 수 있다.

다음은 클래스 레벨과 메서드 레벨 매핑 예제이다:

Java
@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}
Kotlin
@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    fun getPerson(@PathVariable id: Long): Person {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun add(@RequestBody person: Person) {
        // ...
    }
}


URI 패턴



글롭 패턴(glob pattern) 과 와일드카드를 사용하여 요청을 매핑할 수 있다:
  • ? 는 한 문자와 매칭된다.
  • * 는 한 경로 세그먼트 안에서 0 개 이상의 문자와 매칭된다.
  • ** 는 경로 세그먼트를 포함하여 0 개 이상의 문자와 매칭된다.


URI 변수를 선언하고 변수의 값을 @PathVariable으로 접근하는 일도 가능하다:

Java
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}
Kotlin
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
    // ...
}


클래스 레벨과 메서드 레벨에서 각각 URL 변수를 선언하는 일도 가능하다:

Java
@Controller
@RequestMapping("/owners/{ownerId}") (1)
public class OwnerController {

    @GetMapping("/pets/{petId}") (2)
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}
Kotlin
@Controller
@RequestMapping("/owners/{ownerId}") (1)
class OwnerController {

    @GetMapping("/pets/{petId}") (2)
    fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
        // ...
    }
}


(1) 클래스 레벨 URI 매핑 (2) 메서드 레벨 URI 매핑

URI 변수는 적절한 타입으로 자동 컨버팅되거나 TypeMismatchException 이 발생한다. int, long, Date 및 기타 등등과 같은 단순 타입은 기본으로 지원되고, 이 외 다른 데이터 타입을 등록하는 일도 가능하다. 이 부분은 Type ConversionDataBinder에서 다룬다.

@PathVariable("customId") 처럼, URI 변수명은 명시적으로 지정될 수 있지만, 사용되는 이름이 동일하고, 디버깅 정보로 컴파일이 이루어지거나 Java8 의 -parameter 컴파일러 플래그를 사용한다면 이런 상세한 부분은 생략할 수 있다.

{*varName} 문법은 0 개 이상의 남은 경로 세그먼트와 매칭되는 URI 변수를 선언한다. 예를 들어 /resources/{*path} 는 /resources/ 의 모든 파일과 매칭되고, "path" 변수는 완전한 상대경로를 캡쳐한다.

{varName:regex} 문법은 URI 변수를 {varName:regex} 문법을 가진 정규식으로 선언한다. 예를 들어 URL 경로 /spring-web-3.0.5.jar가 주어지면 다음 메서드는 이름, 변수, 파일 확장자를 추출한다:

Java
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
    // ...
}
Kotlin
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable version: String, @PathVariable ext: String) {
    // ...
}


URI 경로 패턴은 내장된 ${...} 플레이스홀더를 가지고 있다. 이 플레이스홀더는 로컬, 시스템, 환경, 그리고 다른 프로퍼티 자원에 대한 PropertyPlaceHolderConfigurer을 통해 시작 시점에 리졸빙된다. 이 기능은 외부 설정에 기반한 기본 URL 파라미터 처리에 사용될 수 있다.

# 스프링 웹플럭스는 PathPattern 과 PathPatternParser를 사용하여 URI 경로 매칭을 지원한다. 이 두 클래스는 spring-web 에 포함되어 있으며, 런타임에 많은 수의 URI 경로 패턴 매칭이 발생하는 웹 어플리케이션에서 HTTP URL 경로와 함께 사용되도록 명시적으로 만들어져 있다.

스프링 웹플럭스는 접미어 패턴 매칭을 지원하지 않는다. 스프링 MVC 에서는 /person.* 이 /person으로 매칭되지만, 스프링 웹플럭스는 이를 지원하지 않는다. URL 기반 컨텐츠 협상에 대해서는 필요하다면 쿼리 파라미터를 사용하길 권한다. 쿼리 파라미터는 더 단순하고, 더 명시적이며, URL 경로를 악용하는 취약성 측면에서 더 낫다.

패턴 비교



한 URL 에 다수의 패턴이 매칭될 때는 이들 중 가장 적합한 매칭을 찾기 위해 패턴 비교 작업이 필요하다. 이 작업은 PathPattern.SPECIFICITY_COMPARATOR으로 수행된다. 보다 구체적으로 매칭되는 패턴을 찾는다.

모든 패턴에는 URI 변수와 와일드카드의 숫자에 근거한 점수가 산정된다. URI 변수는 와일드카드보다 낮은 점수를 가진다. 점수의 총합이 낮은 패턴이 선택되며, 두 패턴이 같은 점수를 가질 경우 더 긴 패턴이 선택된다.

캐치올(catch-all) 패턴(**, {*varName}) 은 이 점수 산정에서 제외되고, 언제나 가장 낮은 우선순위를 갖는다. 두 패턴이 모두 캐치올 패턴이라면, 더 긴 패턴이 선택된다.

소비형(consumable) 미디어 타입



다음과 같이, 요청의 Content-Type으로 요청 매핑을 좁힐 수 있다:

Java
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
    // ...
}
Kotlin
@PostMapping("/pets", consumes = ["application/json"])
fun addPet(@RequestBody pet: Pet) {
    // ...
}


consumes 어트리뷰트는 협상 표현식을 지원한다. 예로, !text/plain은 text/plain을 제외한 컨텐츠 타입을 의미한다.

클래스 레벨에 consumes 어트리뷰트를 선언하여 메서드 매핑에 공유할 수 있다. 다른 대부분의 요청 매핑 어트리뷰트와는 달리, consume 어트리뷰트가 클래스 레벨과 메서드 레벨에서 함께 사용될 경우 메서드 레벨의 어트리뷰트가 적용되고, 클래스 레벨의 어트리뷰트는 무시된다.

# MediaType은 공통적으로 사용되는 미디어 타입 상수를 제공한다 - 예로, APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE.

생산형(producible) 미디어 타입



다음과 같이, 요청 헤더의 Accept와 컨트롤러 메서드가 생산하는 컨텐츠 타입 목록으로 요청 매핑을 좁힐 수 있다:

Java
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}
Kotlin
@GetMapping("/pets/{petId}", produces = ["application/json"])
@ResponseBody
fun getPet(@PathVariable String petId): Pet {
    // ...
}


이 미디어 타입은 문자 집합을 특정할 수 있다. 부정 표현식이 지원된다 - 예로, !text/plain은 text/plain을 제외한 컨텐츠 타입을 의미한다.

클래스 레벨에 produces 어트리뷰트를 선언하여 메서드 매핑에 공유할 수 있다. 다른 대부분의 요청 매핑 어트리뷰트와는 달리, produces 어트리뷰트가 클래스 레벨과 메서드 레벨에서 함께 사용될 경우 메서드 레벨의 어트리뷰트가 적용되고, 클래스 레벨의 어트리뷰트는 무시된다.

# MediaType은 공통적으로 사용되는 미디어 타입 상수를 제공한다 - 예로, APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE.

파라미터와 헤더



쿼리 파라미터 조건으로 요청 매핑을 좁힐 수 있다. 특정 쿼리 파라미터의 존재 여부로 체크할 수 있고(myParam, !myParam) , 특정 값 여부로 체크할 수도 있다(myParam=myValue). 다음은 그 예제이다:

Java
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
Kotlin
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}


(1) myParam 파라미터의 값이 myValue 인지 체크한다.

또한 헤더의 조건으로 체크할 수도 있다:

Java
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
Kotlin
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}


(1) myHeader의 값이 myValue 인지 체크한다.

HTTP HEAD, OPTIONS



@GetMappping 과 @RqeustMapping(method=HttpMethod.GET) 은 요청 매핑 목적의 HTTP HEAD를 투명하게 지원한다. 컨트롤러 메서드는 변경될 필요가 없다. HttpHandler 서버 어댑터의 응답 래퍼는 실제 응답을 주지 않으면서 Content-Length 헤더에 바이트 수를 설정한다.

기본적으로 HTTP OPTIONS 핸들링은 매칭 URL 패턴을 가진 모든 @RequsetMapping 메서드의 HTTP 메서드 목록에 Allow 응답 헤더를 설정함으로써 이루어진다.

HTTP 메서드 선언이 없는 @RequestMapping의 경우, Allow 헤더는 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS로 설정된다. 컨트롤러 메서드는 언제나 지원되는 HTTP 메서드로 선언되어야 한다(예로, @GetMapping, @PostMapping 및 기타 등과 같은 HTTP 메서드 특정 매핑을 사용한다).

@RequestMapping 메서드를 HTTP HEAD와 HTTP OPTIONS으로 명시적으로 매핑할 수 있지만, 보통의 경우 불필요한 일이다.

커스텀 어노테이션



스프링 웹플럭스는 요청 매핑에 있어 composed annoations(이하 컴포즈드 어노테이션) 사용을 지원한다. 이런 어노테이션은 스스로 @RequestMapping을 적용하고 구성하여 필요한 @RequestMapping 어트리뷰트를 설정하여 보다 특정적이고 구체적인 목적으로 사용할 수 있다.

@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping은 컴포즈드 어노테이션의 예이다. 이런 어노테이션들이 제공되는 이유는, 대부분의 컨트롤러에 있어서 @RequestMapping을 기본 형태로 사용하여 HTTP 메서드를 고려하지 않는 URL 매핑을 적용하기 보다는 특정 HTTP 메서드로의 매핑을 적용하는 것이 바람직하기 때문이다. 컴포즈드 어노테이션의 예제가 필요하다면 이 어노테이션들이 어떻게 선언되어 있는지 보면 된다.

스프링 웹플럭스는 또한 커스텀 요청 매칭 로직을 가진 커스텀 요청 매핑 어트리뷰트를 지원한다. 이 방법은 RequestMappingHandlerMapping을 확장하여 getCustomMethodCondition 메서드를 오버라이딩하는, 보다 고차원의 옵션이다. getCustomMethodCondition 메서드는 커스텀 어트리뷰트를 체크하고 당신만의 RequestCondition를 반환할 수 있다.

명시적 등록



핸들러 메서드를 프로그래밍 방식으로 등록할 수 있다. 이 방식은 핸들러 메서드를 동적으로 등록하거나, 같은 핸들러의 서로 다른 인스턴스들로 서로 다른 URL을 처리하는 경우와 같이 보다 고차원적인 목적으로 사용될 수 있다. 다음은 그 예제이다:

Java
@Configuration
public class MyConfig {

    @Autowired
    public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
            throws NoSuchMethodException {

        RequestMappingInfo info = RequestMappingInfo
                .paths("/user/{id}").methods(RequestMethod.GET).build(); (2)

        Method method = UserHandler.class.getMethod("getUser", Long.class); (3)

        mapping.registerMapping(info, handler, method); (4)
    }

}
Kotlin
@Configuration
class MyConfig {

    @Autowired
    fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)

        val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)

        val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)

        mapping.registerMapping(info, handler, method) (4)
    }
}


(1) 타겟 핸들러와 핸들러 매핑을 컨트롤러에 주입한다. (2) 요청 매핑 메타데이터를 준비한다. (3) 핸들러 메서드를 얻는다. (4) 등록한다.

1.4.3. 핸들러 메서드

@RequestMapping 핸들러 메서드는 유연한 시그니처를 가지며, 지원되는 컨트롤러 메서드 아규먼트와 반환값을 골라 사용할 수 있다.

메서드 아규먼트



다음 테이블은 지원되는 컨트롤러 메서드 아규먼트를 보여준다.

리액티브 타입(Reactor, RxJava, 기타 등) 블로킹 I/O를 요구하는 아규먼트를 지원한다(예로, 요청 본문 읽기). 이는 설명 컬럼에 표시되어 있다. 리액티브 타입은 블로킹을 요구하지 않는 아규먼트에는 존재하지 않는다.

JDK 1.8 의 java.util.Optional은 requried 어트리뷰트를 가진 어노테이션과 함께 메서드 아규먼트로 지원되며(@RequestParam, @RequestHeader, 기타 등), required=false와 동등하다.

컨트롤러 메서드 아규먼트 설명
ServerWebExchange ServerWebExchange 전체에 접근한다 - HTTP 요청과 응답, 요청과 세션 어트리뷰트, checkNotModified 메서드, 그리고 기타 등등을 얻을 수 있다.
ServerHttpRequest ServerHttpResponse / HTTP 요청이나 응답에 접근한다.
WebSession 세션에 접근한다. 따로 추가된 어트리뷰트가 없다면, 새 세션의 시작을 강제하지는 않는다. 리액티브 타입을 지원한다.
java.security.Principal 현재 인가된 유저 - 알려진 특정 Principal 구현체가 있다면 그것이 될 것이다. 리액티브 타입을 지원한다.
org.springframework.http.HttpMethod 요청의 HTTP 메서드.
java.util.Locale 현재 요청의 로케일. 사용 가능한 가장 구체적인 LocaleResolver 에 의해 결정된다. 사실상, 설정된 LocaleResolver / LocaleContextResolver가 된다.
java.util.TimeZone + java.time.ZoneId 현재 요청과 관련된 타임 존. LocaleContextResolver 에 의해 결정된다.
@PathVariable URI 템플릿 변수로 접근하기 위해 사용된다. URI Patterns를 보라.
@MatrixVariable URI 경로 세그먼트의 이름-값 쌍으로 접근하기 위해 사용된다. Matrix Variables를 보라.
@RequestParam 서블릿 요청 파라미터에 접근하기 위해 사용된다. 파라미터 값은 선언된 메서드 아규먼트 타입으로 컨버팅된다. @RequestParam을 보라.

@RequestParam 사용은 선택적이다 - 예로, 이 어노테이션의 어트리뷰트를 설정하기 위해 사용할 수 있다. 이 테이블의 "이 외의 아규먼트"를 보라.
@RequestHeader 요청 헤더에 접근하기 위해 사용된다. 헤더 값은 선언된 메서드 아규먼트 타입으로 컨버팅된다. @RequestHeader를 보라.
@CookieValue 쿠키에 접근하기 위해 사용된다. 쿠키 값은 선언된 메서드 아규먼트 타입으로 컨버팅된다. @CookieValue를 보라.
@RequestBody 요청 본문에 접근하기 위해 사용된다. 본문 내용은 선언된 메서드 아규먼트 타입으로 컨버팅된다. 이 컨버팅에는 HttpMessageReader 인스턴스가 사용된다. @RequestBody를 보라.
HttpEntity 요청 헤더와 본문에 접근하기 위해 사용된다. 본문은 HttpMessageReader 인스턴스를 사용하여 컨버팅된다. 리액티브 타입을 지원한다. HttpEntity를 보라.
@RequestPart multipart/form-data 요청의 파트에 접근하기 위해 사용된다. 리액티브 타입을 지원한다. Multipart ContentMultipart Data를 보라.
HttpEntity 요청 헤더와 본문에 접근하기 위해 사용된다. 본문은 HttpMessageReader 인스턴스를 사용하여 컨버팅된다. 리액티브 타입을 지원한다. HttpEntity를 보라.
java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap HTML 컨트롤러에 사용되고 뷰 렌더링의 일부로서 템플릿이 되는 모델에 접근하기 위해 사용된다.
@ModelAttribute 데이터 바인딩과 검증이 적용되는 모델(없다면 초기화한다)에 존재하는 어트리뷰트에 접근하기 위해 사용된다. @ModelAttibute 그리고 ModelDataBinder를 보라.

@ModelAttibute는 선택적이다 - 예로, 이 어노테이션의 어트리뷰트를 설정하기 위해 사용할 수 있다. 이 테이블의 "이 외의 아규먼트"를 보라.
Errors, BindingResult 벨리데이션과 커맨드 객체(@ModelAttribute 아규먼트) 로의 데이터 바인딩, 혹은 @RequestBody, @RequestPart 아규먼트의 벨리데이션에서 발생한 에러로의 접근을 위해 사용한다. Erros 또는 BindingResult 아규먼트는 반드시 벨리데이션 대상 메서드 아규먼트의 바로 다음에 선언되어야 한다.
SessionStatus + class-level @SessionAttributes 요청 처리 완료를 표시하기 위해 사용된다. 요청 처리 완료 시 클래스 레벨 @SessionAttribute 어노테이션을 통해 선언된 세션 어트리뷰트를 비운다. 더 자세한 정보는 @SessionAttributes에서 찾을 수 있다.
UriComponentsBuilder 현재 요청의 호스트, 포트, 스킴, 경로에 연관된 URL을 준비하기 위해 사용된다. URI Links를 보라.
@SessionAttribute 세션 어트리뷰트에 접근하기 위해 사용된다 - 클래스 레벨 @SessionAttribute의 결과로 세션에 저장된 모델 어트리뷰트와 대조된다. 더 자세한 정보는 @SessionAttributes에서 찾을 수 있다.
@RequestAttribute 요청 어트리뷰트에 접근하기 위해 사용된다. 더 자세한 정보는 @RequestAttributes에서 찾을 수 있다.
UriComponentsBuilder 현재 요청의 호스트, 포트, 스킴, 경로에 연관된 URL을 준비하기 위해 사용된다. URI Links를 보라.
이 외의 아규먼트 메서드 아규먼트가 위에서 다룬 아규먼트들와 매칭되지 않는다면, 단순 타입의 경우 기본적으로 @RequestParam를 통해 리졸빙된다. 단순 타입 여부는 BeanUtils#isSimpleProperty를 통해 결정된다. @RequestParam 이 아니면 @ModelAttribute가 된다.


반환값



다음 테이블은 지원되는 컨트롤러 메서드 반환값을 보여준다. Reactor, RxJava, 혹은 이 외의 리액티브 라이브러리의 리액티브 타입은 일반적으로 모든 반환값을 지원한다.

컨트롤러 메서드 반환값 설명
@ResponseBody 반환값은 HttpMessageWriter 인스턴스를 통해 인코딩되어 응답으로 작성된다. @ResponseBody를 보라.
@HttpEntity, ResponseEntity 반환값이 응답 전체를 지정한다. HTTP 헤더와 본문 모두 HttpMessageWriter 인스턴스를 통해 인코딩되어 응답으로 작성된다. @ResponseEntity를 보라.
@HttpHeader 응답 헤더를 반환하기 위해 사용된다. 본문은 취급하지 않는다.
String / ViewResolver 인스턴스가 리졸빙에 사용할 뷰 이름이 된다. 그리고 내포된 모델과 함께 사용된다 - 모델은 커맨드 객체와 @ModelAttribute 메서드를 통해 결정된다. 핸들러 메서드는 Model 아규먼트 선언을 통해 프로그래밍 방식으로 모델을 더욱 풍부하게 만들 수 있다(앞서 설명되었다).
String 내포된 모델과 함께 렌더링될 View 인스턴스 - 커맨드 객체와 @ModelAttribute 메서드를 통해 결정된다. 핸들러 메서드는 Model 아규먼트 선언을 통해 프로그래밍 방식으로 모델을 더욱 푸부하게 만들 수 있다(앞서 설명되었다).
java.util.Map, org.springframework.ui.model 내포된 모델에 어트리뷰트를 추가하기 위해 사용된다. 요청 경로를 바탕으로 암시적으로 선택된 뷰 이름과 함께 작동한다.
@ModelAttribute 모델에 한 어트리뷰트를 추가하기 위해 사용된다. 요청 경로를 바탕으로 암시적으로 선택된 뷰 이름과 함께 작동한다.

@ModelArribute를 선택적이다. 이 테이블의 "이 외의 반환값" 을 보라.
Rendering 모델과 뷰 렌더링 시나리오를 위한 API.
void 반환 타입이 void, 비동기(Mono), 혹은 null 반환값 인 메서드는 ServerHttpResponse, ServerWebExchange 아규먼트를 갖거나, 혹은 @ResponseStatus 어노테이션이 적용된 경우, 응답 요소 전체를 핸들링한다. 컨트롤러가 낙관적 ETag 또는 lastModified 타임스탬프 체크를 생성한 경우에도 마찬가지이다. Controller에서 더 자세한 정보를 찾을 수 있다. 위 사항에 모두 해당되지 않는 경우, void 반환 타입은 REST 컨트롤러에선 "응답 본문이 없음" 을 의미하고, HTML 컨트롤러에선 디폴트 뷰 이름이 선택된다.
Flux, Observable, 혹은 이 외 리액티브 타입 서버 전송 이벤트를 발생시킨다. ServerSentEvent 래퍼는 오직 데이터가 작성될 경우에만 생략할 수 있다(그러나 text/event-stream은 반드시 요청되거나 혹은 produces 어트리뷰트 선언을 통해 매핑되어야 한다).
이 외의 반환값 반환값이 위의 어느 것에도 매칭되지 않을 경우, 기본적으로 String 또는 void(디폴트 뷰 이름 적용) 라면 뷰 이름으로 취급되며, 여기에 해당하지 않으면서 BeanUtils#isSimpleProperty가 단순 타입으로 판단하지 않는다면, 모델로 추기되는 모델 어트리뷰트로 취급된다(리졸빙되지 않은 상태로 남는 경우).


타입 컨버젼



스트링 기반 요청 인풋(예: @RequestParam, @RequestHeader, @PathVariable, @MatrixVariable, @CookieValue) 을 나타내는 몇몇 어노테이티드 컨트롤러 메서드 아규먼트들은, 아규먼트가 String 외의 타입으로 선언된 경우 타입 컨버젼을 필요로 한다.

이런 경우 타임 컨버젼은 설정된 타입 컨버터에 기반하여 자동으로 수행된다. 단순 타입(int, long, Date, 기타 등등) 은 기본으로 지원된다. 타입 컨버젼은 WebDataBinder([mvc-ann-initbinder]를 보라) 또는 Formatters를 FormattingConversionService(Spring Field Formatting을 보라) 와 등록함으로써 통해 커스터마이징 가능하다.

매트릭스 변수(Matrix Variables)



RFC 3986 은 경로 세그먼트 안의 이름-값 쌍을 주제로 한다. 스프링 웹플럭스에선 팀 버너스 리의 옛 포스팅에 근거하여, 이를 "매트릭스 변수" 라 칭한다. 또한 URI 경로 파라미터라 칭하기도 한다.

매트릭스 변수는 어떠한 경로 세그먼트에서도 존재할 수 있다. 각 변수는 세미콜론으로 구분되며, 콤마로 나뉘어진 다수의 값으로 이루어져 있다 - 예: "/cars'color=red,green;year=2012". 다수 값은 반복적인 이름과 함께 지정될 수도 있다 - 예: "color=red;color=green;color=blue".

스프링 MVC 와는 달리, 웹플럭스에서는 URL 에서 매트릭스 변수의 존재 여부는 요청 매핑에 어떠한 영향도 주지 않는다. 다시 말해서, 변수 컨텐츠를 마스킹하기 위해 URI 변수를 사용할 필요는 없다. 그렇지만, 컨트롤러 메서드에서 매트릭스 변수에 접근해야 한다면 URI 변수를 매트릭스 변수가 위치하는 경로 세그먼트에 추가할 필요가 있다. 다음은 그 예제이다:

Java
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

    // petId == 42
    // q == 11
}
Kotlin
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {

    // petId == 42
    // q == 11
}


주어진 모든 경로 세그먼트는 매트릭스 변수를 포함할 수 있다. 때로는 매트릭스 변수가 어떤 경로 변수의 것인지 구분할 필요가 있을 수 있다. 다음은 그에 대한 예제이다:

Java
// GET /owners/42;q=11/pets/21;q=22

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable(name="q", pathVar="ownerId") int q1,
        @MatrixVariable(name="q", pathVar="petId") int q2) {

    // q1 == 11
    // q2 == 22
}
Kotlin
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
        @MatrixVariable(name = "q", pathVar = "petId") q2: Int) {

    // q1 == 11
    // q2 == 22
}


선택적인 매트릭스 변수를 기본값과 함께 선언할 수도 있다:

Java
// GET /pets/42

@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {

    // q == 1
}
Kotlin
// GET /pets/42

@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {

    // q == 1
}


MultiValueMap을 사용해서 모든 매트릭스 변수를 가져올 수도 있다:

Java
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable MultiValueMap<String, String> matrixVars,
        @MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}
Kotlin
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable matrixVars: MultiValueMap<String, String>,
        @MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}


@RequestParam



@RequestParam를 사용하여 컨트롤러에서 쿼리 파라미터를 메서드 아규먼트로 바인딩할 수 있다.

Java
@Controller
@RequestMapping("/pets")
public class EditPetForm {

    // ...

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

    // ...
}
Kotlin
import org.springframework.ui.set

@Controller
@RequestMapping("/pets")
class EditPetForm {

    // ...

    @GetMapping
    fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
        val pet = clinic.loadPet(petId)
        model["pet"] = pet
        return "petForm"
    }

    // ...
}


(1) @RequestParam 사용.

# 서블릿 API "요청 파라미터" 컨셉은 쿼리 파라미터, 폼 데이터, 멀티파트를 하나로 묶는다. 그러나 웹플럭스는 이 각각에 대해 ServerWebExchange를 통해 독립적으로 접근한다. @RequestParam 이 쿼리 파라미터만을 대상으로 하는 것과는 달리, 데이터 바인딩을 사용하여 쿼리 파라미터, 폼 데이터, 멀티파트를 커맨드 객체에 적용할 수 있다.

@RequestParam 어노테이션을 사용하는 메서드 파라미터는 기본적으로 필수값이 된다. @RequestParam의 required 플래그를 false로 설정하거나, 아규먼트를 java.util.Optional 래퍼로 선언해서 선택적 파라미터로 지정할 수 있다.

타겟 메서드 파라미터 타입이 String 이 아닐 경우, 타입 컨버젼은 자동으로 적용된다. [mvc-ann-typeconversion]을 보라.

@RequestParam 어노테이션이 Map<String, Stirng>이나 MultiValueMap<String, String> 아규먼트에 선언되면, 이 맵에 모든 쿼리 파라미터를 담는다.

@RequestParam 사용은 선택적이다 - 예로, 이 어노테이션에 어트리뷰트를 설정하기 위해 사용할 수 있다. 기본적으로 BeanUtils#isSimpleProperty로 단순 타입 값으로 판단된 아규먼트이면서, 어떠한 아규먼트 리졸버에 의해서도 리졸빙되지 않은 아규먼트는 @RequestParam 이 적용된 것과 같이 작동한다.

@RequestHeader



@RequestHeader를 사용하여 컨트롤러에서 요청 헤더를 메서드 아규먼트로 바인딩할 수 있다.

다음은 요청 헤더의 예이다:

요청헤더

다음 예제는 Accept-Encoding 과 Keep-Alive 헤더의 값을 가져온다:

Java
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
Kotlin
@GetMapping("/demo")
fun handle(
        @RequestHeader("Accept-Encoding") encoding: String, (1)
        @RequestHeader("Keep-Alive") keepAlive: Long) { (2)
    //...
}


(1) Accept-Endocing 헤더의 값을 가져온다. (2) Keep-Alive 헤더의 값을 가져온다.

타겟 메서드 파라미터 타입이 String 이 아닐 경우, 타입 컨버젼은 자동으로 적용된다. [mvc-ann-typeconversion]을 보라.

@RequestParam 어노테이션이 Map<String, Stirng> 또는 MultiValueMap<String, String> 아규먼트에 선언되면, 이 맵에 모든 헤더값을 담는다.

# 기본 내장형 컨버터는 컴마로 구분된 문자열을 스트링 배열이나 컬렉션, 혹은 이 외 다른 알려진 타입으로의 컨버젼을 지원한다. 예를 들어, @RequestHeader("Accept") 어노테이션이 적용된 메서드 파라미터는 String 또는 String[], List<String> 로 컨버팅 될 수 있다.

@CookieValue



@CookieValue를 사용하여 HTTP 쿠키를 메서드 아규먼트로 바인딩할 수 있다.

다음은 쿠키 예시이다:

쿠키

다음은 쿠키값을 가져오는 샘플이다:

Java
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
    //...
}
Kotlin
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
    //...
}


(1) 쿠키값을 가져온다.

타겟 메서드 파라미터 타입이 String 이 아닐 경우, 타입 컨버젼은 자동으로 적용된다. [mvc-ann-typeconversion]을 보라.

@ModelAttribute



@ModelAttribute를 메서드 아규먼트에 적용하여 모델의 어트리뷰트에 접근하거나, 기존 모델이 없는 경우 초기화를 수행하도록 할 수 있다. 이 모델 어트리뷰트는 쿼리 파라미터와 필드 이름과 매칭된 폼 필드들을 덮어쓴다. 이는 데이터 바인딩이라 할 수 있고, 이 동작은 각각의 쿼리 파라미터와 폼 필드를 파싱하고 컨버팅하는 작업으로부터 자유롭게 해준다. 다음은 Pet 인스턴스 바인딩 예제이다:

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)


(1) Pet 인스턴스에 바인딩한다.

위 예제의 Pet 인스턴스는 다음과 같이 리졸빙된다.
  • 이미 기존 Model에 존재한다면 모델로부터 얻는다.
  • @SessionAttribute을 통해 HTTP 세션으로부터 얻는다.
  • 기본 생성자를 실행하여 얻는다.
  • 쿼리 파라미터나 폼 필드와 매칭되는 아규먼트를 가진 "주요 생성자"를 실행하여 얻는다. 아규먼트의 이름은 자바빈 @ConstructorProperties 또는 바이트코드의 런타임 파라미터 이름을 통해 확정된다.


모델 어트리뷰트 인스턴스를 얻은 뒤, 데이터 바인딩이 적용된다. WebExchangeDataBinder 클래스는 쿼리 파라미터와 폼 필드의 이름을 타겟 Object의 필드 이름과 매칭한다. 매칭 필드 필요에 따라 타임 컨버젼을 적용하여 값을 세팅한다. 데이터 바인딩와 밸리데이션에 관한 더 자세한 정보는 Validation에서 찾을 수 있다. 데이터 바인딩에 관하여는 DataBinder를 보라.

데이터 바인딩에서 에러가 발생할 수 있다. 기본적으로 WebExchangeBindException 이 발생하지만, 컨트롤러 메서드에서 이러한 에러를 체크하기 위해서는 BindingResult 아규먼트를 @ModelAttribute 바로 다음에 선언해야 한다. 다음은 그 예제이다:

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}


(1) BindingResult를 추가한다.

javax.validation.Valid 어노테이션이나 스프링의 @Validated 어노테이션을 추가하여 데이터 바인딩 뒤 자동 벨리데이션을 적용할 수 있다(Bean ValidationSpring validation을 보라). 다음은 @Valid 어노테이션의 사용 예이다:

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}


(1) @Valid를 모델 어트리뷰트 아규먼트에 적용한다.

스프링 MVC 와는 달리, 스프링 웹플럭스는 모델에서 리액티브 타입을 지원한다 - 예: Mono<Account>, io.reactivex.Single<Account>. @ModelAttribute 아규먼트를 리액티브 타입 래퍼와 함께 사용하거나 사용하지 않거나 하면 필요에 따라 그에 맞게 실제 값으로 리졸빙된다. 하지만 위의 예제에서처럼 BindingResult 아규먼트는 반드시 위치 상 @ModelAttribute 아규먼트 직후에, 리액티브 타입 래퍼 없이 사용해야 한다. 그게 아니라면 아래와 같이 리액티브 타입을 통해 에러 핸들링을 수행할 수 있다:

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
    return petMono
        .flatMap(pet -> {
            // ...
        })
        .onErrorResume(ex -> {
            // ...
        });
}
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") petMono: Mono<Pet>): Mono<String> {
    return petMono
            .flatMap { pet ->
                // ...
            }
            .onErrorResume{ ex ->
                // ...
            }
}


@ModelAttribute 사용은 선택적이다 - 예로, 이 어노테이션에 어트리뷰트를 설정하기 위해 사용할 수 있다. 기본적으로 BeanUtils#isSimpleProperty로 단순 타입 값이 아닌 것으로 판단된 아규먼트이면서, 어떠한 아규먼트 리졸버에 의해서도 리졸빙되지 않은 아규먼트는 @ModelAttribute가 적용된 것과 같이 작동한다.

@SessionAttributes



@SessionAttributes는 요청 간 모델 어트리뷰트를 WebSession 에 저장하기 위해 사용된다. 타입 레벨 어노테이션으로, 특정 컨트롤러에 의해 사용되며 세션 어트리뷰트를 선언한다. 보통 이어지는 요청에서의 접근을 위해, 세션에 그대로 저장되어야 하는 모델 어트리뷰트의 이름이나 타입을 나열한다.

다음 예제를 보자:

Java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
Kotlin
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
    // ...
}


(1) @SessionAttributes 어노테이션을 적용한다.

최초 요청에서, 모델 어트리뷰트가 이름 pet으로 모델에 추가된다. 이 모델 어트리뷰트는 자동으로 WebSession 에 저장된다. 이 모델은 다른 컨트롤러 메서드가 SessionStatus 메서드 아규먼트로 세션 저장소를 클리어하기 전까지 남아 있는다. 다음은 그 예제이다:

Java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) { (2)
        if (errors.hasErrors()) {
            // ...
        }
            status.setComplete();
            // ...
        }
    }
}
Kotlin
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { (2)
        if (errors.hasErrors()) {
            // ...
        }
        status.setComplete()
        // ...
    }
}


(1) @SessionAttributes 어노테이션을 적용한다. (2) SessionStatus 변수를 사용한다.

@SessionAttribute



전역적으로 관리되는(컨트롤러 바깥에서 - 예: 필터) 기존 세션 어트리뷰트로 접근해야 하고, 그 존재 여부가 확실치 않다면, @SessionAttribute 어느테이션을 메서드 파라미터에 적용할 수 있다:

Java
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
    // ...
}
Kotlin
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
    // ...
}


(1) @SessionAttribute 어노테이션을 적용한다.

세션 어트리뷰트를 추가하거나 제거하는 경우, WebSession을 컨트롤러에 주입할 것을 고려해볼 수 있다.

컨트롤러의 워크플로우의 일부로서 세션에 모델 어트리뷰트를 임시 저장하려면 SessionAttribute를 사용할 수 있다. @SessionAttribute에 설명되어 있다.

@RequestAttribute



@SessionAttribute와 유사하게, @RequestAttribute를 사용하여 이전에 생성된 기존 요청 어트리뷰트에 접근할 수 있다(예: WebFilter 에서 생성한). 다음은 그 예제이다:

Java
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
    // ...
}
Kotlin
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
    // ...
}


(1) @RequestAttribute를 적용한다.

멀트파트 컨텐츠



Multipart Data에서 설명된대로, ServerWebExchange는 멀티파트 컨텐츠에 접근할 수 있다. 컨트롤러에서 파일 업로드 폼을 핸들링하는 가장 최적의 방법은 커맨드 객체 로 데이터 바인딩하는 것이다. 다음은 그 예제이다:

Java
class MyForm {

    private String name;

    private MultipartFile file;

    // ...

}

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(MyForm form, BindingResult errors) {
        // ...
    }

}
Kotlin
class MyForm(
        val name: String,
        val file: MultipartFile)

@Controller
class FileUploadController {

    @PostMapping("/form")
    fun handleFormUpload(form: MyForm, errors: BindingResult): String {
        // ...
    }

}


RESTful 서비스 시나리오의 브라우저가 아닌 클라이언트로부터의 멀티파트 요청을 전송할 수 있다. 다음은 파일과 JSON을 요청에 함께 사용한 예제이다:

요청

@RequestPart로 각 파트에 접근할 수 있다:

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
        @RequestPart("file-data") FilePart file) { (2)
    // ...
}
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
        @RequestPart("file-data") FilePart file): String { (2)
    // ...
}


(1) @RequestPart로 metadata를 얻는다. (2) @RequestPart로 file을 얻는다.

원본 파트 컨텐츠를 디시리얼라이징하기 위해(예: JSON으로 - @RequestBody와 유사), Part 대신 구체적인 타겟 Object를 선언할 수 있다:

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
    // ...
}
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
    // ...
}


(1) @RequestPartmetadata를 얻는다.

@RequestPart와 javax.validation.Valid 또는 스프링의 @Validated를 함께 사용할 수 있다. javax.validation.Valid와 @Validated는 표준 빈 밸리데이션이 적용된다. 기본적으로 밸리데이션 에러는 WebExchangeBindException을 발생시킨다. 이 익셉션은 400(BAD_REQUEST) 응답이 된다. 그렇지 않으면, 컨트롤러 안에서 Errors 또는 BindingResult 아규먼트로 밸리데이션 에러를 핸들링할 수 있다:

Java
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
    // use one of the onError* operators...
}
Kotlin
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
    // ...
}


@RequestBody를 사용하여 멀티파트 데이터 전체를 MultiValueMap으로 접근할 수 있다:

Java
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
    // ...
}
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
    // ...
}


(1) @RequestBody를 적용한다.

@RequestBody와 Flux<Part>(코틀린에선 Flow<Part>)를 사용하여 멀티파트 데이터를 순차적으로, 스트리밍 방식으로 접근할 수 있다:

Java
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
    // ...
}
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { (1)
    // ...
}


(1) @RequestBody를 적용한다.

@RequestBody



@RequestBody를 사용하여 요청 본문을 읽고 Object로 디시리얼라이징할 수 있다. 내부적으로 HttpMessageReader를 사용한다:

Java
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}
Kotlin
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
    // ...
}


스프링 MVC 와는 달리, 웹플럭스에서는 @RequestBody 메서드 아규먼트는 리액티브 타입과 완전한 논 블로킹 읽기,(클라이언트에서 서버로) 스트리밍을 지원한다.

Java
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
    // ...
}
Kotlin
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
    // ...
}


WebFlux ConfigHTTP message codec 옵션을 사용하여 메시지 리더(readers)를 설정하거나 커스터마이징할 수 있다.

@RequestBody와 javax.validation.Valid 또는 스프링의 @Validated를 함께 사용할 수 있다. javax.validation.Valid와 @Validated는 표준 빈 밸리데이션이 적용된다. 기본적으로 밸리데이션 에러는 WebExchangeBindException을 발생시킨다. 이 익셉션은 400(BAD_REQUEST) 응답이 된다. 그렇지 않으면, 컨트롤러 안에서 Errors 또는 BindingResult 아규먼트로 밸리데이션 에러를 핸들링할 수 있다:

Java
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
    // use one of the onError* operators...
}
Kotlin
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Mono<Account>) {
    // ...
}


HttpEntity

HttpEntity는 @RequestBody를 사용하는 것과 거의 동일하지만, HttpEntity는 요청 헤더와 본문을 노출하는 컨테이너 객체에 기반한다. 다음은 HttpEntity 사용 예제이다:

Java
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    // ...
}
Kotlin
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
    // ...
}


@ResponseBody



@ResponseBody를 메서드에 적용하여 반환값을 시리얼라이징하고 응답 본문에 작성할 수 있다. 내부적으로 HttpMessageWriter를 사용한다:

Java
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}
Kotlin
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
    // ...
}


@ResponseBody를 클래스 레벨에 적용하여 컨트롤러 안의 모든 메서드에 공통 적용할 수도 있다. 이는 @RestController의 효과와 동일하다. @Controller와 @ResponseBody를 메타 어노테이션으로 적용하는 것과 같다.

@ResponseBody는 리액티브 타입을 지원한다. Reactor 또는 RxJava 타입을 반환할 수 있고, 리액티브 타입이 생성하는 값을 비동기로 응답에 작성할 수 있다. 더 자세한 내용은 StreamingJSON rendering에서 다룬다.

@ResponseBody 메서드를 JSON 시리얼라이징 뷰와 함께 사용할 수 있다. 자세한 내용은 Jackson JSON를 보라.

WebFlux ConfigHTTP message codecs 옵션을 사용하여 메시지 작성을 설정하고 커스터마이징할 수 있다.

ResponseEntity



ResponseEntity는 @ResponseBody와 비슷하지만, 상태(status)와 헤더를 가지고 있다:

Java
@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body = ... ;
    String etag = ... ;
    return ResponseEntity.ok().eTag(etag).build(body);
}
Kotlin
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
    val body: String = ...
    val etag: String = ...
    return ResponseEntity.ok().eTag(etag).build(body)
}


웹플럭스는 ResponseEntity를 비동기로 생성하기 위한 단일 값 리액티브 타입을 지원한다. 그리고/또는 단일과 다수 값 티액티브 타입을 본문에 사용할 수 있다.

Jackson JSON



스프링은 Jackson JSON 라이브러리를 지원한다.

JSON Views

스프링 웹플럭스는 내장형 Jackson's Serialization Views를 제공한다. Object의 필드 중 일부만을 렌더링할 수 있다. @ResponseBody 또는 ResponseEntity 컨트롤러 메서드와 함께 사용하기 위해서는 Jackson's @JsonView를 사용할 수 있다. 이 어노테이션으로 시리얼라이징 뷰 클래스를 활성화한다. 다음은 그 예제이다:

Java
@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @JsonView(WithoutPasswordView.class)
    public String getUsername() {
        return this.username;
    }

    @JsonView(WithPasswordView.class)
    public String getPassword() {
        return this.password;
    }
}
Kotlin
@RestController
class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView::class)
    fun getUser(): User {
        return User("eric", "7!jd#h23")
    }
}

class User(
        @JsonView(WithoutPasswordView::class) val username: String,
        @JsonView(WithPasswordView::class) val password: String
) {
    interface WithoutPasswordView
    interface WithPasswordView : WithoutPasswordView
}


# @JsonView는 뷰 클래스 지정 시 배열을 허용하지만, 한 컨트롤러 메서드 당 하나의 뷰만 지정할 수 있다. 다수의 뷰를 활성화하려면 컴포짓 인터페이스를 사용하라.

1.4.4. Model

@ModelAttribute를 다음과 같이 사용할 수 있다:
  • @RequestMapping 메서드의 메서드 아규먼트에 적용하여 모델로부터의 Object를 생성하거나 접근하고, WebDataBinder를 통해 이를 요청에 바인딩한다.
  • @Controller 또는 @ControllerAdvice 클래스의 메서드 레벨 어노테이션으로 적용하여 @RequestMapping 메서드 실행 전 모델 초기화 동작을 수행하도록 한다.
  • @RequestMapping 메서드에 적용하여 이 메서드의 반환값이 모델 어트리뷰트임을 표시한다.


이 섹션은 @ModelAttribute 메서드나, 위 목록의 두 번째 - 메서드 레벨 어노테이션을 주제로 한다. 컨트롤러는 @ModelAttribute 메서드를 몇 개든 가질 수 있다. 이 메서드들은 같은 컨트롤러 안의 @RequestMapping 메서드가 실행되기 전에 먼저 실행된다. @ModelAttribute 메서드는 @ControllerAdvice를 통해 컨트롤러 간 공유되어 사용될 수 있다. 더 자세한 내용은 Controller Advice를 보라.

@ModelAttribute 메서드는 유연한 시그니처를 갖는다. @RequestMapping 메서드와 동일한 아규먼트를 다수 지원한다(@ModelAttribute 자체는 는 요청 본문과 무관하다는 점을 제외하고).

다음은 @ModelAttribute 메서드 예제이다:

Java
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // add more ...
}
Kotlin
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
    model.addAttribute(accountRepository.findAccount(number))
    // add more ...
}


다음 예제는 한 어트리뷰트만 추가한다:

Java
@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}
Kotlin
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
    return accountRepository.findAccount(number);
}


# @ModelAttribute의 이름이 명시적으로 지정되지 않은 경우, Convention 자바독에 기술된 내용에 근거한 디폴트 이름이 선택된다. 오버로딩된 addAttribute 메서드나 @ModelAttribute의 name 어트리뷰트를 통해 언제나 명시적 이름을 지정할 수 있다(반환값에 대한).

스프링 MVC와 달리, 스프링 웹플럭스는 모델에서의 리액티브 타입을 지원한다(예: Mono<Account>, io.reactivex.Single<Account>). 이런 비동기 모델 어트리뷰트는 @RequestMapping 메서드의 실행 시점에 실제 값으로 투명하게 리졸빙된다(그리고 모델이 갱신된다). @RequestMapping 메서드의 @ModelAttribute 아규먼트는 래퍼 없이 실제 타입으로 선언된다. 다음은 그 예제이다:

Java
@ModelAttribute
public void addAccount(@RequestParam String number) {
    Mono<Account> accountMono = accountRepository.findAccount(number);
    model.addAttribute("account", accountMono);
}

@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
    // ...
}
Kotlin
import org.springframework.ui.set

@ModelAttribute
fun addAccount(@RequestParam number: String) {
    val accountMono: Mono<Account> = accountRepository.findAccount(number)
    model["account"] = accountMono
}

@PostMapping("/accounts")
fun handle(@ModelAttribute account: Account, errors: BindingResult): String {
    // ...
}


추가로, 리액티브 타입 래퍼를 가진 모델 어트리뷰트는 뷰 렌더링 바로 전에 실제 값으로 리졸빙된다(그리고 모델이 갱신된다).

@RequestMapping 메서드의 반환값을 모델 어트리뷰트로 해석하는 경우, @ModelAttribute를 @RequestMapping 메서드의 메서드 레벨 어노테이션으로 적용할 수도 있다. HTML 컨트롤러에서는 이것이 반환값이 뷰 이름이 되는 String 이 아닐 때의 기본 동작이기 때문에 일반적으로는 필요치 않다. @ModelAttribute를 사용하여 모델 어트리뷰트의 이름을 커스터마이징할 수도 있다. 다음은 그 예제이다:

Java
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    // ...
    return account;
}
Kotlin
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
    // ...
    return account
}


1.4.5. DataBinder

@Controller, @ControllerAdvice 클래스는 @InitInder 메서드로 WebDataBinder 인스턴스를 초기화할 수 있다. 이 인스턴스는 아래와 같이 사용된다:
  • 요청 파라미터(폼 데이터나 쿼리)를 모델 객체에 바인딩한다.
  • String 기반 요청 값(요청 파라미터, 경로 변수, 헤더, 쿠키, 기타 등등) 을 타겟 컨트롤러 메서드 아규먼트 타입으로 컨버팅한다.
  • HTML 폼 렌더링 시 모델 객체값들을 String 값으로 포맷팅한다.


@InitBinder 메서드는 컨트롤러 특징적인 java.bean.PropertyEditor 혹은 스프링 Converter와 Formatter 컴포넌트롤 등록할 수 있다. 추가로 WebFlux Java configuration를 사용하여 Converter와 Formatter 타입을 전역적으로 사용되는 FormattingConversionService 에 등록할 수 있다.

@InitBinder 메서드는 @RequestMapping 메서드와 동일한 아규먼트를 다수 지원한다(@ModelAttribute(커맨드 객체)를 제외하고). 일반적으로 아규먼트는 컴포넌트 등록을 위해 WebDataBinder 아규먼트와 함께 void 반환값으로 선언된다. 다음은 @InitBinder 사용 예제이다:

Java
@Controller
public class FormController {

    @InitBinder (1)
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // ...
}
Kotlin
@Controller
class FormController {

    @InitBinder (1)
    fun initBinder(binder: WebDataBinder) {
        val dateFormat = SimpleDateFormat("yyyy-MM-dd")
        dateFormat.isLenient = false
        binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
    }

    // ...
}


(1) @InitBinder를 적용한다.

다른 방법으로는, 전역 FormattingConversionService를 통한 Formatter 기반 설정 사용 시, 같은 방식을 재사용하며 컨트롤러 특징적 Formatter 인스턴스를 등록할 수 있다. 다음은 그 예제이다:

Java
@Controller
public class FormController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
    }

    // ...
}
Kotlin
@Controller
class FormController {

    @InitBinder
    fun initBinder(binder: WebDataBinder) {
        binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1)
    }

    // ...
}


(1) 커스텀 포맷터를 추가한다(여기서는 DataFormatter를 사용했다).

1.4.6. 익셉션 관리

@Controller, @ControllerAdvice 클래스는 @ExceptionHandler 메서드로 컨트롤러 메서드에서 발생한 익셉션을 핸들링할 수 있다. 다음은 그 예제이다:

Java
@Controller
public class SimpleController {

    // ...

    @ExceptionHandler (1)
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}
Kotlin
@Controller
class SimpleController {

    // ...

    @ExceptionHandler (1)
    fun handle(ex: IOException): ResponseEntity<String> {
        // ...
    }
}


(1) @ExceptionHandler를 적용한다.

익셉션 아규먼트는 전파된 익셉션의 최상위 레벨 익셉션과 매칭할 수 있다(예제의 IOException). 혹은 최상위 레벨 래퍼 익셉션의 원인이 되는 익셉션과도 매칭할 수 있다(예: IOException으로 래핑된 IllegalStateException).

위 예제에서와 같이, 매칭 익셉션 타입으로는 가급적 타겟 익셉션을 메서드 아규먼트로 선언하는 것이 좋다. 아니면 어노테이션을 선언하여 매칭 익셉션 범위를 좁힐 수도 있다. 최대한 구체적인 익섹션을 아규먼트 시그니처에 사용하여 주요한 루트 익셉션 매핑을 상응하는 순서에 맞는 @ControllerAdvice 에 선언할 것을 권한다. 더 자세한 정보는 MVC 섹션에서 다룬다.

# 웹플럭스의 @ExceptionHandler 메서드는 요청 본문의 익셉션과 @ModelAttribute 관련 메서드 아규먼트로 @RequestMapping 메서드와 같은 메서드 아규먼트와 반환값을 지원한다.

REST API 익셉션



REST 서비스의 공통 요건은 응답 본문에 상세한 에러 내용을 포함하는 것이다. 스프링 프레임워크는 이 작업을 자동으로 해주지 않기 때문에, 응답 본문의 상세한 에러 내용 표시는 어플리케이션에 특징적이다. 하지만 @RestController는 @ExceptionHandler 메서드를 ResponseEntity 반환값과 함께 사용하여 상태 코드와 응답 본문을 작성할 수 있다. 이런 메서드는 @ControllerAdvice 클래스에도 선언되어 전역적인 적용이 가능하다.

# 스프링 웹플럭스는 스프링 MVC의 ResponseEntityExceptionHandler와 같은 컴포넌트를 제공하지 않는다. 왜냐하면 웹플럭스에서 발생하는 익셉션은 모두 ResponseStatusException(혹은 이것의 서브클래스) 이고, 이 익셉션들은 HTTP 상태 코드로 해석될 필요가 없기 때문이다.

1.4.7. 컨트롤러 어드바이스(Controller Advice)

보통 @ExceptionHandler, @InitBinder, @ModelAttribute 메서드는 자신이 선언된 @Controller 클래스(혹은 클래스 계층) 안에서 적용된다. 클래스에 @ControllerAdvice 또는 @RestControllerAdvice를 적용하면 이런 메서드들을 더 넓은 범위로(컨트롤러 간) 적용할 수 있다.

@ControllerAdvice는 @Component와 함께 적용된다. 이는 컨트롤러 어드바이스 클래스는 컴포넌트 스캐닝을 통해 스프링 빈으로 등록될 수 있음을 의미한다. @RestControllerAdvice는 컴포즈드 어노테이션으로, @ControllerAdvice와 @ResponseBody를 함께 적용한다. 이것은 곧 @ExceptionHandler 메서드가 메시지 컨버팅을 통해 응답 본문으로 작성된다는 뜻이다(뷰 리솔루션이나 템플릿 렌더링에 대응한다).

어플리케이션 시작 시에 @RequestMapping 과 @ExceptionHandler 메서드의 기반 클래스들이 @ControllerAdvice가 적용된 스프링 빈을 감지하고, 이 메서드들을 런타임에 적용한다. 전역 @ExceptionHandler 메서드(@ControllerAdvice 에서 선언된)는 지역 @ExceptionHandler 메서드(@Controller 에서 선언된)가 적용된 다음에 적용된다. 이와 반대로 전역 @ModelAttribute, @InitBinder 메서드들은 지역 메서드들이 적용되기 전에 먼저 적용된다.

기본적으로, @ControllerAdvice 메서드는 모든 요청에 적용되지만(모든 컨트롤러에 적용), 어노테이션에 어트리뷰트를 사용하여 컨트롤러 적용 범위를 좁힐 수 있다. 다음은 그 예제이다:

Java
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
Kotlin
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
public class ExampleAdvice3 {}


위 예제의 컨트롤러 셀렉터는 런타임에 평가되기 때문에, 광범위하게 사용될 경우 성능에 부정적인 영향을 줄 수 있다. ControllerAdvice 자바독에서 더 자세한 정보를 찾을 수 있다.

1.5. 함수형 엔드포인트(Functional Endpoints)

스프링 웹플럭스는 WebFlux.fn를 포함한다. WebFlux.fn은 경량 함수형 프로그래밍 모델으로, 함수는 요청을 라우팅하고 핸들링하며, 각 요소는 불변형(immutable)이다. 어노테이션 기반 프로그래밍 모델의 대체제인 동시에 동일한 Reactive Core 기반 위에 작동한다.

1.5.1. 개요

WebFlux.fn 에서 HTTP 요청은 HandlerFunction으로 핸들링한다: ServerRequest 아규먼트를 가지며, 지연된 ServerResponse를 반환한다(Mono). 요청과 응답 객체 모두 불변형이며 HTTP 요청과 응답에의 접근에 있어 자바 8 에 친화적인 방식을 제공한다. HandlerFunction은 어노테이션 기반 프로그래밍 모델의 @RequestMapping 메서드의 본문와 동등하다.

RouterFunction은 들어오는 요청을 핸들러 함수로 라우팅한다: ServerRequest 아규먼트를 가지며, 지연된 HandlerFunction을 반환한다(Mono). 라우터 함수가 매칭되면 핸들러 함수를 반환한다. 매칭되지 않으면 빈 Mono를 반환한다. RouterFunction은 @RequestMapping 어노테이션과 동등하지만, 라우터 함수가 제공하는 큰 차이점은 데이터가 아닌 그 동작에 있다.

RouterFunctions.route() 는 라우터를 생성하는 라우터 빌더를 제공한다. 다음은 그 예제이다:

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    .POST("/person", handler::createPerson)
    .build();


public class PersonHandler {

    // ...

    public Mono<ServerResponse> listPeople(ServerRequest request) {
        // ...
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) {
        // ...
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) {
        // ...
    }
}
Kotlin
val repository: PersonRepository = ...
val handler = PersonHandler(repository)

val route = coRouter { (1)
    accept(APPLICATION_JSON).nest {
        GET("/person/{id}", handler::getPerson)
        GET("/person", handler::listPeople)
    }
    POST("/person", handler::createPerson)
}


class PersonHandler(private val repository: PersonRepository) {

    // ...

    suspend fun listPeople(request: ServerRequest): ServerResponse {
        // ...
    }

    suspend fun createPerson(request: ServerRequest): ServerResponse {
        // ...
    }

    suspend fun getPerson(request: ServerRequest): ServerResponse {
        // ...
    }
}


RouterFunction을 가동하는 방법 하나는 이를 HttpHandler로 변경하고 내장형 서버 어댑터를 통해 설치하는 것이다.
  • RouterFunctions.toHttpHandler(RouterFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)


대부분의 어플리케이션은 웹플럭스 자바 설정을 통해 실행할 수 있다. Running a Server를 보라.

1.5.2. HandlerFunction

ServerRequest, ServerResponse는 불변형 인터페이스로, JDK 8 에 친숙한 HTTP 요청과 응답 접근법을 제공한다. 요청과 응답은 본문 스트림에 대한 Reactive Streams 백프레셔를 제공한다. 요청 본문은 리액터 Flux 또는 Mono로 표현된다. 응답 본문은 Flux와 Mono를 포함하는 리액티브 스트림 Publisher로 표현된다. 더 자세한 내용은 리액티브 라이브러리를 보라.

ServerRequest



ServerRequest는 HTTP 메서드, URI, 헤더, 쿼리 파라미터에의 접근을 제공하며, 본문에의 접근은 body 메서드를 통해 이루어진다.

다음 예제는 요청 본문을 Mono<String>으로 추출한다:

Java
Mono<String> string = request.bodyToMono(String.class);
Kotlin
val string = request.awaitBody<String>()


다음 예제는 본문을 Flux<Person>(코틀린은 Flow) 로 추출한다. Person 객체는 JSON 또는 XML 처럼 시리얼라이징된 폼으로부터 디코딩된다.

Java
Flux<Person> people = request.bodyToFlux(Person.class);
Kotlin
val people = request.bodyToFlow<Person>()


위의 예제는 더 일반적인 ServerRequest.body(BodyExtractor) 사용 예이다. ServerRequest.body(BodyExtractor) 는 BodyExtractor 함수형 전략(strategy) 인터페이스를 받는다. 유틸리티 클래스 BodyExtractors는 다량의 인스턴스에의 접근을 제공한다. 예로, 위 예제는 아래와 같이 작성될 수 있다:

Java
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
Kotlin
    val string = request.body(BodyExtractors.toMono(String::class.java)).awaitFirst()
    val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()


다음 예제는 폼 데이터에 접근한다:

Java
Mono<MultiValueMap<String, String> map = request.formData();
Kotlin
val map = request.awaitFormData()


다음 예제는 맵 방식으로 멀티파트 데이터에 접근한다:

Java
Mono<MultiValueMap<String, Part> map = request.multipartData();
Kotlin
val map = request.awaitMultipartData()


다음 예제는 멀트파트에 한 번에 하나씩, 스트리밍 방식으로 접근한다:

Java
Flux<Part> parts = request.body(BodyExtractors.toParts());
Kotlin
val parts = request.body(BodyExtractors.toParts()).asFlow()


ServerResponse



ServerResponse는 HTTP 응답에의 접근을 제공한다. 불변형이며, build 메서드로 생성한다. 빌더를 사용하여 응답 상태를 설정하고 응답 헤더를 추가하거나 본문을 작성한다. 다음 예제는 200(OK) 응답을 JSON 컨텐츠로 생성한다:

Java
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
Kotlin
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)


다음 예제는 201(CREATED) 응답을 Location 헤더와 빈 본문으로 생성한다:

Java
URI location = ...
ServerResponse.created(location).build();
Kotlin
val location: URI = ...
ServerResponse.created(location).build()


사용된 코덱에 따라서 힌트 파라미터를 전달하여 응답 본문이 시리얼라이징 혹은 디시리얼라이징되는 방식을 커스터마이징할 수 있다. 예로, Jackson JSON view를 지정한다:

Java
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
Kotlin
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)


핸들러 클래스(Handler Classes)



핸들러 함수를 람다로 작성할 수 있다:

Java
HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().bodyValue("Hello World");
Kotlin
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }


이 방식은 편리하지만 다수의 함수가 필요한 어플리케이션에선 다수의 인라인 람다는 지저분할 수 있다. 때문에 관련된 함수 그룹을 만들어 하나의 핸들러 클래스에 모으는 것이 좋다. 어노테이션 기반 어플리케이션에선 @Controller 클래스가 비슷한 역할을 한다. 예를 들어, 다음 클래스는 리액티브 Person 리파지토리를 노출한다:

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

public class PersonHandler {

    private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }

    public Mono<ServerResponse> listPeople(ServerRequest request) { (1)
        Flux<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people, Person.class);
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) { (2)
        Mono<Person> person = request.bodyToMono(Person.class);
        return ok().build(repository.savePerson(person));
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) { (3)
        int personId = Integer.valueOf(request.pathVariable("id"));
        return repository.getPerson(personId)
            .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
            .switchIfEmpty(ServerResponse.notFound().build());
    }
}
Kotlin
class PersonHandler(private val repository: PersonRepository) {

    suspend fun listPeople(request: ServerRequest): ServerResponse { (1)
        val people: Flow<Person> = repository.allPeople()
        return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
    }

    suspend fun createPerson(request: ServerRequest): ServerResponse { (2)
        val person = request.awaitBody<Person>()
        repository.savePerson(person)
        return ok().buildAndAwait()
    }

    suspend fun getPerson(request: ServerRequest): ServerResponse { (3)
        val personId = request.pathVariable("id").toInt()
        return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
                ?: ServerResponse.notFound().buildAndAwait()

    }
}


(1) listPeople은 리파지토리에서 검색한 모든 Person 객체를 JSON으로 반환하는 핸들러 함수이다. (2) createPerson은 요청 본문에 있는 새 Person을 저장하는 핸들러 함수이다. PersonRepository.savePerson(Person)Mono를 반환한다: 요청에서 Person을 읽어 저장하면 빈 Mono는 완료 시그널을 발생시킨다. 이 완료 시그널을 받으면(Person 저장이 완료되면) build(Publisher) 메서드를 사용하여 응답을 보낼 수 있다. (3) getPersonPerson을 반환하는 핸들러 함수이다. Person은 경로 변수인 id를 통해 검색된다. 리파지토리에서 Person을 검색하고 JSON 응답을 생성한다. 검색된 Person 이 없으면 switchIfEmpty(Mono<T>)를 사용해 404 Not Found 응답을 반환한다.

밸리데이션



함수형 엔드포인트는 스프링의 밸리데이션 기능을 요청 본문에 적용한다. 예로, Person 에 대한 커스텀 스프링 Validator 구현체는 다음과 같이 사용한다:

Java
public class PersonHandler {

    private final Validator validator = new PersonValidator(); (1)

    // ...

    public Mono<ServerResponse> createPerson(ServerRequest request) {
        Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); (2)
        return ok().build(repository.savePerson(person));
    }

    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString()); (3)
        }
    }
}
Kotlin
class PersonHandler(private val repository: PersonRepository) {

    private val validator = PersonValidator() (1)

    // ...

    suspend fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.awaitBody<Person>()
        validate(person) (2)
        repository.savePerson(person)
        return ok().buildAndAwait()
    }

    private fun validate(person: Person) {
        val errors: Errors = BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw ServerWebInputException(errors.toString()) (3)
        }
    }
}


(1) Validator 인스턴스를 생성한다. (2) 밸리데이션을 적용한다. (3) 400 응답을 위한 익셉션을 발생시킨다.

핸들러는 LocalValidatorFactoryBean 에 기반한 전역 Validator 인스턴스를 생성하고 주입하여 표준 빈 밸리데이션 API(JSR-303)를 사용할 수 있다. Spring Validation을 보라.

1.5.3. RouterFunction

라우터 함수를 사용하여 요청을 그에 맞는 HandlerFunction 에 라우팅할 수 있다. 보통 라우터 함수를 직접 작성하지는 않고, RouterFunction 유틸리티 클래스의 메서드로 생성하여 사용한다. RouterFunctions.route()(파라미터 없음) 은 라우터 함수를 생성하기 위한 훌륭한 빌더를 제공하고, RouterFunctions.route(RequestPredicate, HandlerFunction) 는 직접 라우터를 생성하도록 한다.

일반적으로는 route() 빌더 사용을 권장한다. 이 빌더는 전형적인 매핑 시나리오를, 찾기 어려운 정적 임포트 없이 사용할 수 있는 편리하고 간결한 방식으로 제공한다. 예를 들어, 라우터 함수 빌더는 메서드 GET(String, HandlerFunction)으로 GET 요청 매핑을 생성한다. POST 요청 매핑에는 POST(String, HandlerFunction) 가 있다.

HTTP 메서드 기반 매핑 외에도, 이 빌더는 요청에 매핑할 때 추가적인 술어(predicates)를 도입하는 방법을 제공한다. RequestPredicate를 파라미터로 취하는, 각 HTTP 메서드에 대한 과적화된 변종이 존재하지만, 추가 제약조건을 표현할 수 있다.

Predicates



자신만의 RequestPredicate를 작성할 수 있지만, RequestPredicates 유틸리티 클래스는 요청 경로, HTTP 메서드, 컨텐츠 타입, 그리고 그 외의 것들에 근거하여 공통적으로 사용되는 구현체들을 제공한다. 다음은 요청 술어를 사용하여 Accept 헤더에 기반한 제약조건을 생성한다:

Java
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().bodyValue("Hello World")).build();
Kotlin
val route = coRouter {
    GET("/hello-world", accept(TEXT_PLAIN)) {
        ServerResponse.ok().bodyValueAndAwait("Hello World")
    }
}


다음을 사용하여 다수의 요청 술어를 함께 구성할 수 있다.
  • RequestPredicate.and(RequestPredicate) - 둘 모두 반드시 매칭.
  • RequestPredicate.or(RequestPredicate) - 하나만 매칭 가능.


RequestPredicates 에는 많은 술어가 구성되어 있다. 예를 들어, RequestPredicates.GET(String) 은 RequestPredicates.method(HttpMethod) 와 RequestPredicates.path(String)으로부터 구성되었다. 위 예제는 또한 두 요청 술어를 사용한다. 빌더는 RequestPredicates.GET을 사용하고, accept 술어를 함께 구성한다.

Routes



라우터 함수는 순서에 따라 평가된다: 첫 번째 라우터에 매칭되지 않으면 두 번째 라우터가 평가되고, 같은 과정을 밟는다. 따라서, 더 구체적인 라우터가 일반적인 라우터에 앞서도록 선언하는 것이 좋다. 유의할 점은, 이 동작은 어노테이션 기반 프로그래밍 모델과는 다르다. 어노테이션 기반 프로그래밍 모델에서는 더 구체적인 컨트롤러 메서드가 자동으로 선택된다.

라우터 함수 빌더를 사용하면, 정의된 모든 라우팅은 하나의 RouterFunction 안에 구성되고, build() 로부터 반환된다. 또한 다수의 라우터 함수를 함께 구성하는 다른 방법들이 있다:
  • RouterFunctions.route() 빌더의 add(RouterFunction)
  • RouterFunction.and(RouterFunction)
  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) - RouterFunction.and() 와 중첩된 RouterFunctions.route() 의 간결한 형태


다음은 네 가지 라우팅의 구성을 보여준다:

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> otherRoute = ...

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    .POST("/person", handler::createPerson) (3)
    .add(otherRoute) (4)
    .build();
Kotlin
import org.springframework.http.MediaType.APPLICATION_JSON

val repository: PersonRepository = ...
val handler = PersonHandler(repository);

val otherRoute: RouterFunction<ServerResponse> = coRouter {  }

val route = coRouter {
    GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)


1) GET /person/{id}와 함께 Accept 헤더가 JSON에 매칭되면 PersonHandler.getPerson으로 라우팅한다 2) GET /person와 함께 Accept 헤더가 JSON에 매칭되면 PersonHandler.listPeople으로 라우팅한다. 3) POST /person이 매칭되면 PersonHandler.createPerson으로 라우팅한다, 그리고 4) otherRoute는 다른 곳에서 생성된 라우터 함수이다. 라우팅에 추가된다.

중첩 라우팅(Nested Routes)



라우터 함수 그룹은 술어를 공유하는 것이 일반적이다. 예를 들어 경로를 공유할 수 있다. 위의 예제에서, 공유된 술어는 /person 에 매칭되는 경로 술어가 된다. 이 술어는 세 가지 라우팅에 사용되었다. 어노테이션을 사용할 때는 타입 레벨 @RequestMapping 어노테이션을 /person 과 매핑하여 이런 중복을 제거했다. WebFlux.fn 에서는 경로 술어는 라우터 함수 빌더의 path 메서드를 통해 공유될 수 있다. 예를 들어, 위 예제의 마지막 몇 줄은 중첩 라우팅을 사용하면 다음과 같이 개선된다:

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", builder -> builder (1)
        .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        .GET("", accept(APPLICATION_JSON), handler::listPeople)
        .POST("/person", handler::createPerson))
    .build();
Kotlin
val route = coRouter {
    "/person".nest {
        GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        GET("", accept(APPLICATION_JSON), handler::listPeople)
        POST("/person", handler::createPerson)
    }
}


(1) path의 두 번째 파라미터는 라우터 빌더를 소비한다.

경로 기반의 중첩이 가장 일반적이기는 하나, 빌더의 nest 메서드를 사용하여 어떠한 종류의 술어든 중첩할 수 있다. 위 예제는 Accept 헤더 술어를 공유하는 형태에서 여전히 중복이 있다. nest 메서드와 accept를 함께 사용하여 더욱 개선할 수 있다:

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople))
        .POST("/person", handler::createPerson))
    .build();
Kotlin
val route = coRouter {
    "/person".nest {
        accept(APPLICATION_JSON).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
            POST("/person", handler::createPerson)
        }
    }
}


1.5.4. 서버 가동하기

어떻게 HTTP 서버에서 라우터 함수를 가동할까? 다음 중 하나를 사용하여 라우터 함수를 HttpHandler로 컨버팅하는 간단한 옵션이 있다:
  • RouterFunctions.toHttpHandler(RoutherFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)


반환된 HttpHandler와 서버 어댑터를 서버 특징적 명령을 위한 HttpHandler에 따라 사용할 수 있다.

더 일반적인, 또한 스프링 부트에서 사용하는 옵션은 WebFlux Config를 통한 DispatcherHandler 기반 설정을 사용하는 것이다. 이 방법은 스프링 설정을 사용하여 요청 처리에 필요한 컴포넌트를 선언한다. 웹플럭스 자바 설정은 다음의 기반 컴포넌트를 선언하여 함수형 엔드포인트를 지원한다:
  • RouterFunctionMapping: 스프링 설정에서 하나 이상의 RouterFunction 빈을 감지하고, RouterFunction.andOther를 통해 이 빈들을 결합한다. 그리고 이 결과로 구성된 RouterFunction 에 요청을 라우팅한다.
  • HandlerFunctionAdapter: DispatcherHandler가 요청에 매핑된 HandlerFunction을 실행하도록 하는 단순한 어댑터이다.
  • ServerResponseResultHandler: ServerResponse의 writeTo 메서드를 통해 HandlerFunction을 실행한 결과를 핸들링한다.


위의 컴포넌트들은 함수형 엔드포인트가 DispatcherHandler 요청 처리 라이프싸이클 안에서 적절하게 작동하도록 한다. 그리고(잠재적으로) 어노테이티드 컨트롤러가 선언되어 있다면 여기에서도 함께 동작하도록 한다. 이것이 스프링 부트 웹플럭스 스타터가 함수형 엔드포인트를 적용하는 방식이다.

다음 예제는 웹플럭스 자바 설정을 보여준다(동작 방식에 관하여는 DispatcherHandler를 보라):

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Bean
    public RouterFunction<?> routerFunctionA() {
        // ...
    }

    @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }

    // ...

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // configure message conversion...
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // configure CORS...
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // configure view resolution for HTML rendering...
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Bean
    fun routerFunctionA(): RouterFunction<*> {
        // ...
    }

    @Bean
    fun routerFunctionB(): RouterFunction<*> {
        // ...
    }

    // ...

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        // configure message conversion...
    }

    override fun addCorsMappings(registry: CorsRegistry) {
        // configure CORS...
    }

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // configure view resolution for HTML rendering...
    }
}


1.5.5. 핸들러 함수 필터링(Filtering Handler Functions)

라우팅 함수 빌더의 before, after, filter 메서드를 사용하여 핸들러 함수를 필터링할 수 있다. 어노테이션을 사용할 때는 @ControllerAdvice 또는 @ServerFilter, 혹은 둘 모두를 통해 비슷한 기능을 구현할 수 있다. 필터는 해당 빌더가 생성한 모든 라우팅에 적용된다. 이는 중첩 라우팅 안에 선언된 필터는 최상위 레벨 라우팅에 적용되지 않음을 의미한다. 예를 들어, 다음 예제를 보자:

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople)
            .before(request -> ServerRequest.from(request) (1)
                .header("X-RequestHeader", "Value")
                .build()))
        .POST("/person", handler::createPerson))
    .after((request, response) -> logResponse(response)) (2)
    .build();
Kotlin
val route = router {
    "/person".nest {
        GET("/{id}", handler::getPerson)
        GET("", handler::listPeople)
        before { (1)
            ServerRequest.from(it)
                    .header("X-RequestHeader", "Value").build()
        }
        POST("/person", handler::createPerson)
        after { _, response -> (2)
            logResponse(response)
        }
    }
}


(1) 커스텀 요청 헤더를 추가하는 before 필터는 두 GET 라우팅에만 적용된다. (2) 응답 로깅을 수행하는 after 필터는 중첩 라우팅을 포함한 모든 라우팅에 적용된다.

라우터 빌더의 filter 메서드는 HandlerFilterFunction을 아규먼트로 갖는다: HandlerFilterFunction은 ServerRequest와 HandlerFunction을 가지고 ServerResponse를 반환한다. 핸들러 함수 파라미터는 체인의 다음 요소를 나타낸다. 일반적으로 다음 요소는 라우팅 대상 핸들러가 되지만, 다수의 필터를 적용할 때는 다른 필터가 될 수도 있다.

다음 예제는 간단한 보안 필터를 라우팅에 추가한다. SecurityManager는 특정 경로에의 접근 허용 여부를 결정한다:

Java
SecurityManager securityManager = ...

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople))
        .POST("/person", handler::createPerson))
    .filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
    .build();
Kotlin
val securityManager: SecurityManager = ...

val route = router {
        ("/person" and accept(APPLICATION_JSON)).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
            POST("/person", handler::createPerson)
            filter { request, next ->
                if (securityManager.allowAccessTo(request.path())) {
                    next(request)
                }
                else {
                    status(UNAUTHORIZED).build();
                }
            }
        }
    }


위 예제는 next.handle(ServerRequest) 실행이 선택적임을 보여준다. 핸들러 함수는 접근이 허용된 경우에만 실행된다.

라우터 함수 빌더의 filter 함수 사용 외에도, RouterFunction.filter(HandlerFunction) 을 통해 기존 라우터 함수에 필터링을 적용할 수 있다.

# 함수형 엔드포인트에의 CORS 지원은 여기에 특화된 CorsWebFilter를 통해 제공한다.

1.6.1. URI 링크

이 섹션은 스프링 프레임워크의 URI 에 대한 다양한 옵션에 대해 다룬다.

1.6.1. UriComponents

UriComponentsBuilder는 변수를 사용한 URI 템플릿으로부터의 URI 빌드를 제공한다. 다음은 그 예제이다:

Java
UriComponents uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build(); (4)

URI uri = uriComponents.expand("Westin", "123").toUri();  (5)
Kotlin
val uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build() (4)

val uri = uriComponents.expand("Westin", "123").toUri()  (5)


(1) URI 템플릿을 지정하는 스태틱 팩토리 메서드이다. (2) URI 컴포넌트를 추가하거나 교체한다. (3) URI 템플릿과 URI 변수를 인코딩한다. (4) UriComponents를 빌드한다. (5) 변수를 추가하고 URI를 얻는다.

위 예제는 buildAndExpand로 하나의 체인으로 통합하여 더 간결하게 만들 수 있다:

Java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri();
Kotlin
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri()


(인코딩이 적용된) URI로 직접 이동하여 더 단축할 수 있다:

Java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
Kotlin
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123")


완전한 URI 템플릿으로 여기서 더 단축할 수 있다:

Java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123");
Kotlin
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123")


1.6.2. UriBuilder

UriComponentsBuilder는 UriBuilder를 구현한다. UriBuilderFactory를 사용하여 UriBuilder를 생성할 수 있다. 그리고 UriBuilderFactory와 UriBuilder는 URI 템플릿으로부터 베이스 URL, 인코딩, 기타 세부적인 사항과 같은 공유되는 설정을 바탕으로 URI를 빌드하는 장착형(pluggable) 메커니즘을 제공한다.

UriBuilderFactory를 사용하여 RestTemplate, WebClient를 설정하고 URI 준비 과정을 커스터마이징할 수 있다. DefaultUriBuilderFactory는 내부적으로 UriComponentsBuilder를 사용하고 공유되는 설정 옵션을 노출하는 UriBuilderFactory의 기본 구현체이다.

다음은 RestTemplate 설정 방법이다:

Java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
Kotlin
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = factory


다음 예제는 WebClient 설정 방법이다:

Java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
Kotlin
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val client = WebClient.builder().uriBuilderFactory(factory).build()


추가로, DefaultBuilderFactory를 직접 사용할 수 있다: UriComponentsBuilder를 사용하는 것과 비슷하지만, 이는 스태틱 팩토리 메서드 대신, 설정을 담은 실제 인스턴스이다. 다음은 그 예제이다:

Java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
Kotlin
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)

val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123")


1.6.3. URI 인코딩(URI Encoding)

UriComponentsBuilder는 두 가지 레벨의 인코딩 옵션을 노출한다:

이 두 옵션은 아스키가 아닌 잘못된 문자를 이스케이핑한 8진수로 대체한다. 그런데 첫 번째 옵션은 문자를 URI 변수가 의미하는 예약 문자로 대체한다.

# ":"를 생각해보자. 경로 문자열에서는 유효한 문자이지만, 동시에 예약 문자이기도 하다. 첫 번째 옵션은 URI 변수의 ":"를 "%3B" 로 대체하지만, URI 템플릿에선 대체하지 않는다. 이와 반대로 두 번째 옵션은 ":"를 절대 대체하지 않는다. 왜냐하면 이 문자는 경로 문자열에서 유효한 문자이기 때문이다.

대부분의 경우, 기대하는 결과를 가져오는 쪽은 첫 번째 옵션이다. 왜냐하면 이 옵션은 URI 변수를 불분명한 데이터 완전한 인코딩 대상으로 취급하기 때문이다. 두 번째 옵션이 유용한 경우는 URI 변수에 의도적으로 예약 문자가 포함된 경우 뿐이다.

다음은 첫 번째 옵션 예제이다:

Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("New York", "foo+bar")
        .toUri();

// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
Kotlin
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("New York", "foo+bar")
        .toUri()

// Result is "/hotel%20list/New%20York?q=foo%2Bbar"


(인코딩이 적용된) URI로 직접 이동하여 더 단축할 수 있다:

You can shorten the preceding example by going directly to the URI (which implies encoding), as the following example shows:

Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar")
Kotlin
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar")


완전한 URI 템플릿으로 여기서 더 단축할 수 있다:

Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar")
Kotlin
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar")


WebClient와 RestTemplate는 UriBuilderFactory 전략을 통해 URI 템플릿을 내부적으로 적용하고 인코딩한다. 이 둘 모두 커스터마이징 전략으로 설정할 수 있다. 다음은 그 예제이다:

Java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
Kotlin
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
    encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}

// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
    uriTemplateHandler = factory
}

// Customize the WebClient..
val client = WebClient.builder().uriBuilderFactory(factory).build()


DefaultBuilderFactory 구현체는 내부적으로 UriComponentsBuilder를 사용하여 URI 템플릿을 적용하고 인코딩한다. 이 구현체는 팩토리로서 아래 인코딩 모드 중 하나를 선택하여 인코딩 방식을 설정할 수 있다:
  • TEMPLATE_AND_VALUES: 첫 번째 옵션, UriComponentsBuilder#encode()를 사용하여 URI 템플릿을 먼저 인코딩하고 URI 변수를 적용 시에 인코딩한다.
  • VALUES_ONLY: URI 템플릿은 인코딩하지 않고, UriUtils#encodeUriUriVariables를 사용하여 URI 변수를 템플릿에 적용하기 전에 인코딩한다.
  • URI_COMPONENTS: 두 번째 옵션, UriComponents#encode()를 사용하여 URI 변수를 적용한 뒤에 URI 컴포넌트를 인코딩한다.
  • NONE: 아무 인코딩도 적용하지 않는다.


RestTemplate은 히스토릭한 사유로, 그리고 하위 호환성을 위해 EncodingMode.URI_COMPONENTS를 설정한다. WebClient는 기본값으로 DefaultUriBuilderFactory를 갖는다. 인코딩 방식은 버전 5.0.x 에선 Encoding.URI_COMPONENTS 였고, 5.1 에서 EncodingMode.TEMPLATE_AND_VALUES로 변경되었다.

1.7. CORS

스프링 웹플럭스는 CORS(Cross-Origin Resource Sharing) 을 핸들링한다. 이 섹션은 그 방법을 주제로 한다.

1.7.1. 소개

보안상의 이유로, 브라우저는 현재 origin 에서 벗어난 자원에의 AJAX 호출을 금지한다. 예를 들어, 한 탭에 은행 계좌를 열어두고 다른 탭에 evil.com로 접속했다. evil.com의 스크립트는 당신의 인증서로 은행 API 에 AJAX 요청을 보낼 수 없어야 한다 - 잘못된 예로 당신의 계좌에서 돈을 인출할 수 있다!

Cross-Origin Resource Sharing(CORS) 는 대부분의 브라우저가 구현하는 W3C 명세로, IFRAME 또는 JSONP을 사용한 방법이 아닌, 어떤 종류의 크로스도메인 요청을 허용할 것인지 지정하는 더 안전하고 강력한 방법을 사용한다.

1.7.2. 처리(Processing)

CORS 명세는 예비, 단순, 실제 요청을 구분한다. CORS의 작동 방식을 배우려면 이 글이나 명세에서 더 자세한 내용을 볼 수 있다.

스프링 웹플럭스 HandlerMapping 구현체는 CORS를 내장형으로 지원한다. 요청을 핸들러에 성공적으로 매핑한 뒤, HandlerMapping은 주어진 요청과 핸들러에 대한 CORS 설정을 체크하고 다음 동작을 수행한다. 예비 요청은 직접 핸들링하고, 단순 그리고 실제 CORS 요청은 인터셉팅하고, 검증하고, 필요한 CORS 응답 헤더를 설정한다.

크로스오리진 요청(Origin 헤더 존재하며 요청의 host 헤더와 다른 요청) 을 활성화하기 위해서는, 명시적 CORS 설정을 선언해야 한다. 매칭되는 CORS 설정이 존재하지 않으면 예비 요청은 거부된다. CORS 헤더가 추가되지 않은 단순, 실제 CORS 요청은 브라우저에 의해 거부된다.

URL 패턴 기반 CorsConfiguration 매핑을 통해 각 HandlerMapping을 개별적으로 설정할 수 있다. 대부분의 경우 어플리케이션은 웹플럭스 자바 설정을 사용하여 이런 매핑을 선언한다. 설정 결과는 단일의, 전역 맵으로 모든 HandlerMapping 구현체에 전달된다.

HandlerMapping 레벨의 전역 CORS 설정과 보다 잘 정돈된 핸들러 레벨 CORS 설정을 결합할 수 있다. 예를 들어, 어노테이티드 컨트롤러는 클래스 또는 메서드 레벨 @CrossOrigin을 사용할 수 있다(다른 핸들러는 CorsConfigurationSource를 구현한다).

전역과 지역 설정을 결합하는 규칙은 보통 추가적이다 - 예: 모든 전역 과 지역 설정. allowCredentials와 maxAge 처럼 단일 값만을 받아들이는 어트리뷰트들은 지역 설정이 전역 설정보다 우선한다. 더 자세한 내용은 CorsConfiguration#combine(CorsConfiguration)을 보라.

# 소스 혹은 고급 커스터마이징은 아래를 찾아보자:
  • CorsConfiguraion
  • CorsProcessor, DefaultCorsProcessor
  • AbstractHandlerMapping


1.7.3. @CrossOrigin

@CrossOrigin은 어노테이티드 컨트롤러 요청에 크로스오리진을 활성화한다. 다음은 그 예제이다:

Java
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}
Kotlin
@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}


기본적으로 @CrossOrigin은 다음을 허용한다:
  • 모든 오리진
  • 모든 헤더
  • 컨트롤러 메서드에 매핑되는 모든 HTTP 메서드


allowedCredentials는 비활성화가 기본값이다. 왜냐하면 민감한 사용자 특징적 정보(쿠키와 CSRF 토큰과 같은)를 노출하는 신뢰 수준을 결정하기 때문이다. 반드시 적재적소에만 사용되어야 한다.

maxAge를 30분으로 설정한다.

@CrossOrigin을 클래스 레벨에 적용하면 컨트롤러의 모든 메서드에 적용된다. 다음 예제는 특정 도메인을 지정하고 maxAge를 한시간으로 설정한다:

Java
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}
Kotlin
@CrossOrigin("https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}


@CrossOrigin을 클래스와 메서드 레벨 모두에 적용할 수 있다:

Java
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin("https://domain2.com") (2)
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}
Kotlin
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin("https://domain2.com") (2)
    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}


(1) @CrossOrigin을 클래스 레벨에 사용한다. (2) @CrossOrigin을 메서드 레벨에 사용한다.

1.7.4. 전역 설정

잘 정돈된 컨트롤러 메서드 레벨 설정에 더하여, 전역 CORS 설정이 필요할 수 있다. URL 기반 CorsConfiguration 매핑을 어떠한 HandlerMapping 에나 독립적으로 설정할 수 있다. 그러나 대부분의 어플리케이션에선 웹플럭스 자바 설정을 사용하여 이를 처리한다.

전역 설정은 다음을 활성화한다:
  • 모든 오리진
  • 모든 헤더
  • GET, HEAD, POST 메서드


allowedCredentials는 비활성화가 기본값이다. 왜냐하면 민감한 사용자 특징적 정보(쿠키와 CSRF 토큰과 같은)를 노출하는 신뢰 수준을 결정하기 때문이다. 반드시 적재적소에만 사용되어야 한다.

maxAge를 30분으로 설정한다.

CorsRegistry 콜백을 사용하여 웹플럭스 자바 설정에서 CORS를 활성화할 수 있다. 다음은 그 예제이다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
            .allowedOrigins("https://domain2.com")
            .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
            .exposedHeaders("header1", "header2")
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {

        registry.addMapping("/api/**")
                .allowedOrigins("https://domain2.com")
                .allowedMethods("PUT", "DELETE")
                .allowedHeaders("header1", "header2", "header3")
                .exposedHeaders("header1", "header2")
                .allowCredentials(true).maxAge(3600)

        // Add more mappings...
    }
}


1.7.5. CORS WebFilter

내장된 CorsWebFilter 사용하여 CORS 지원을 활성화할 수 있다. 이 방식은 함수형 엔드포인트와 함께 사용하기 적합하다.

# CorsFilter를 스프링 시큐리티와 사용한다면 스프링 시큐리티의 내장형 CORS 지원을 유념하라.

CorsWebFilter 빈을 선언하고 CorsConfigurationSource를 빈의 생성자로 전달하여 필터를 설정할 수 있다. 다음은 그 예제이다:

Java
@Bean
CorsWebFilter corsFilter() {

    CorsConfiguration config = new CorsConfiguration();

    // Possibly...
    // config.applyPermitDefaultValues()

    config.setAllowCredentials(true);
    config.addAllowedOrigin("https://domain1.com");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    return new CorsWebFilter(source);
}
Kotlin
@Bean
fun corsFilter(): CorsWebFilter {

    val config = CorsConfiguration()

    // Possibly...
    // config.applyPermitDefaultValues()

    config.allowCredentials = true
    config.addAllowedOrigin("https://domain1.com")
    config.addAllowedHeader("*")
    config.addAllowedMethod("*")

    val source = UrlBasedCorsConfigurationSource().apply {
        registerCorsConfiguration("/**", config)
    }
    return CorsWebFilter(source)
}


1.8. 웹 보안(Web Security)

스프링 시큐리티 프로젝트는 웹 어플리케이션을 악의적인 행위로부터 보호하기 위한 방어책을 지원한다. 스프링 시큐리티 레퍼런스 문서를 보라:

1.9. 뷰 기술(View Technologies)

스프링 웹플럭스의 뷰 기술은 장착형(pluggable)으로 사용할 수 있다. 뷰 기술로 타임리프(Thymeleaf), 프리마커(FreeMarker) 또는 이 외 다른 어떤 것을 사용하느냐는 주로 설정 변경의 문제가 된다. 이 챕터는 이러한 뷰 기술들과 스프링 웹플럭스의 통합에 대해 다루며, 독자는 뷰 리솔루션에 대해 숙지하고 있음을 전제로 한다.

1.9.1. 타임리프(Thymeleaf)

타임리프는 모던 서버 사이드 자바 템플릿 엔진으로, 더블 체킹을 통해 브라우저에서 미리 보여지는 플레인 HTML 템플릿을 강조한다. 이는 서버 구동 없이 작동할 수 있어, 독립적인 UI 템플릿 작업에 매우 유용하다. 타임리프는 광범위한 기능을 제공하고, 활발하게 개발되고 유지보수된다. 타임리프 프로젝트 홈페이지에서 보다 완전한 소개를 볼 수 있다.

타임리프와 스프링 웹플럭스의 통합은 타임리프 프로젝트가 관리한다. 설정은 몇 가지 빈 선언으로 이루어진다. SpringResourceTemplateResolver, SpringWebFluxTemplateEngine, ThymeleafReactiveViewResolver가 그 예이다. 더 자세한 정보는 타임리프+스프링웹플럭스 통합 선언문을 통해 찾아볼 수 있다.

1.9.2. 프리마커(FreeMarker)

아파치 프리마커는 HTML 부터 이메일이나 기타 다른 어떤 종류의 텍스트 아웃풋이든 생성 가능한 템플릿 엔진이다. 스프링 프레임워크는 스프링 웹플럭스와 프리마커 템플릿을 함께 사용하기 위한 내장형 통합을 제공한다.

뷰 설정



다음 예제는 프리마커를 뷰 기술로 설정하는 방법을 보여준다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
    }

    // Configure FreeMarker...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
        return configurer;
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()
    }

    // Configure FreeMarker...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates/freemarker")
    }
}


위 예제에서와 같이, 템플릿이 저장된 경로를 FreeMarkerConfigurer 에 지정해야 한다. 이런 설정을 바탕으로, 컨트롤러가 뷰 이름 welcome을 반환하면 리졸버는 classpath:/templates/freemarker/welcome.ftl 템플릿을 찾는다.

프리마커 설정



FreeMarkerConfigurer 빈에 적절한 프로퍼티를 설정하여 프리마커 'Settings' 와 'SharedVariables' 디렉토리를 프리마커 Configuration 객체(스프링이 관리한다) 에 전달할 수 있다. freemarkerSettings 프로퍼티에는 java.util.Properties 객체를 사용한다. 그리고 freemarkerVariables 프로퍼티에는 java.util.Map을 사용한다. 다음은 FreeMarkerConfigurer 사용 예제이다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // ...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        Map<String, Object> variables = new HashMap<>();
        variables.put("xml_escape", new XmlEscape());

        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        configurer.setFreemarkerVariables(variables);
        return configurer;
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    // ...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates")
        setFreemarkerVariables(mapOf("xml_escape" to XmlEscape()))
    }
}


변수 설정과 Configuration 객체로의 적용에 대한 더 자세한 내용은 프리마커 문서를 보라.

폼 핸들링



스프링은 JSP 에서의 사용을 위한 태그 라이브러리를 제공한다. 태그 라이브러리에는 엘리먼트가 있다. 이 엘리먼트는 폼 지원 객체(form-backing objects)로부터 폼에 값을 보여주도록 하고, 웹 혹은 비즈니즈 계층의 Validator의 밸리데이션 실패 결과를 보여준다. 스프링은 프리마커에 대해서도 이와 동일한 기능과 함께, 폼 인풋 엘리먼트를 스스로 생성하는 편리한 매크로를 제공한다.

매크로 바인딩(The Bind Macros)



프리마커 매크로의 표준 집합은 spring-webflux.jar 파일에서 관리한다. 때문에 적절하게 설정된 어플리케이션에 언제든 유효하게 사용할 수 있다.

스프링 템플릿 라이브러리(Spring templating libraries)에 정의된 몇몇 매크로는 내부적으로(private) 취급되지만, 매크로 정의에는 이러한 범위 설정(scoping)은 존재하지 않는다. 모든 매크로는 호출 모드와 사용자 템플릿에서 접근할 수 있다(visible). 다음 섹션은 사용자 템플릿 안에서의 직접 매크로 호출에 관하여만 집중적으로 다룬다. 매크로 코드를 직접 보려면 org.springframework.web.reactive.result.view.freemarker 패키지의 spring.ftl 파일을 보자.

바인딩 지원에 관한 추가적인 정보는 스프링 MVC의 Simple Binding에서 찾아볼 수 있다.

폼 매크로



스프링의 프리마커 템플릿 폼 매크로 지원에 대한 자세한 내용은 스프링 MVC 문서의 다음 섹션들에서 다룬다.

1.9.3. 스크립트 뷰

스프링 프레임워크는 스프링 웹플럭스와 JSR-223 자바 스트립팅 엔진 위에서 작동하는 템플릿 라이브러리를 함께 사용하기 위한 내장형 통합을 제공한다. 다음 테이블은 여러 스트립트 엔진 테스트를 거친 템플릿 라이브러리를 보여준다:

스크립팅 라이브러리 스크립팅 엔진
Handlebars Nashorn
Mustache Nashorn
React Nashorn
EJS Nashorn
ERB JRuby
String template Jython
Kotlin Scripting templating Kotlin


# 다른 스트립트 엔진과 통합하는 기본 규칙은, 반드시 ScriptEngine와 Invocable 인터페이스를 구현하는 것이다.

요건



스크립트 엔진이 클래스패스에 위치해야 한다. 다음은 스크립트 엔진 종류에 따라 달라지는 상세 내용이다:
  • Nashorn 자바스크립트 엔진은 자바 8+ 을 요구한다. 가장 최근의 업데이트 릴리즈를 사용할 것을 강력히 권고한다.
  • 루비 지원을 위해 JRuby 의존성을 추가해야 한다.
  • 파이썬 지원을 위해 Jython 의존성을 추가해야 한다.
  • 코틀린 지원을 위해 org.jetbrains.kotlin:kotlin-script-util의존성과 org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory 라인을 포함하는 META-INF/services/javax.script.ScriptEngineFactory 파일을 추가해야 한다. 이 예제에서 자세히 알아볼 수 있다.
스크립트 템플릿 라이브러리가 필요하다. 자바스크립트를 위한 한 가지 방법으로 WebJars가 있다.

스크립트 템플릿



ScriptTemplateConfigurer를 선언하여 사용할 스트립트 엔진, 불러올 스크립트 파일, 어떤 함수를 호출하여 템플릿을 렌더링할지, 그리고 그 외의 것들을 지정할 수 있다. 다음 예제는 Mustache 템플릿 엔진과 Nashorn 자바스크립트 엔진들 사용한다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
    }

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("mustache.js");
        configurer.setRenderObject("Mustache");
        configurer.setRenderFunction("render");
        return configurer;
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("mustache.js")
        renderObject = "Mustache"
        renderFunction = "render"
    }
}


render 함수는 다음의 파라미터로 호출한다:
  • String template: 템플릿 컨텐츠
  • Map model: 뷰 모델
  • RenderingContext renderingContext: RenderingContext는 어플리케이션 컨텍스트, 로케일, 템플릿 로더, URL(버전 5.0 부터) 에의 접근을 제공한다.


Mustache.render() 는 이 시그니처와 선천적인 호환성을 지니기 때문에, 직접 호출할 수도 있다.

사용할 뷰 기술에 커스터마이징이 필요할 경우, 커스텀 render 함수를 구현하는 스크립트를 제공할 수 있다. 예를 들어, Handlerbars는 사용하기에 앞서 템플릿을 컴파일해야 하고, 서버 사이드 스크립트 엔진에서 사용할 수 없는 브라우저 기능을 에뮬레이팅하기 위해 polyfill을 필요로 한다. 다음 예제는 커스텀 render 함수를 설정한다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
    }

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
        configurer.setRenderFunction("render");
        configurer.setSharedEngine(false);
        return configurer;
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("polyfill.js", "handlebars.js", "render.js")
        renderFunction = "render"
        isSharedEngine = false
    }
}


# 쓰레드세이프하지 않은 스크립트 엔진과 동시성을 고려하여 제작되지 않은 템플릿 라이브러리를 함께 사용할 때는 sharedEngine 프로퍼티를 false로 설정해야 한다. Nashorn 위에서 구동되는 Handlebars, React가 그렇다. 이런 경우에 대해서는 Java SE 8 update 60 이 필요하다. 이 버그가 이유인데, 사실 일반적으로 어떤 경우이든 최신 Java SE 패치 릴리즈를 사용하는 것을 권장한다.

polyfill.js은 Handlebars가 제대로 작동하기 위해 필요한 window 객체만을 정의한다. 다음은 그 스니펫이다:

var window = {};


이 기본 render.js 구현체는 템플릿 엔진을 컴파일하여 사용한다. 준비된 구현체 생성물은 캐싱된 템플릿이나 사전에 컴파일된 템플릿으로 저장하고 재사용하여야 한다. 이 작업은 스크립트사이드에서 이루어지며, 또한 필요에 따라 어떠한 커스터마이징이든 가능하다(템플릿 엔진 설정 관리를 예로 들 수 있다). 다음 예제는 템플릿을 컴파일한다:

function render(template, model) {
    var compiledTemplate = Handlebars.compile(template);
    return compiledTemplate(model);
}


더 많은 설정 예제는 스프링 프레임워크 유닛 테스트, Java 그리고 resources를 체크아웃하여 얻을 수 있다.

1.9.4. JSON 과 XML

컨텐츠 협상을 고려할 때, 클라이언트 요청 컨텐츠 타입에 따라 모델 렌더링을 HTML 템플릿이나 다른 포맷(JSON 또는 XML 과 같은) 사이에서 다르게 처리할 수 있도록 하는 것이 좋다. 이런 기능을 지원하기 위해서 스프링 웹플럭스는 HttpMessageWriterView를 제공한다. 이 컴포넌트는 Jackson2JsonEncoder, Jackson2SmileEncoder, 또는 Jaxb2XmlEncoder와 같은, spring-web 에서 유효한 어떠한 코덱에든 추가하여 사용할 수 있다.

다른 뷰 기술과는 다르게, HttpMessageWriterView는 ViewResolver를 필요로 하지 않고, 대신 기본 뷰로 설정한다. 여러 종류의 HttpMessageWriter 또는 Encoder 인스턴스를 래핑하여 하나 이상의 기본 뷰를 설정할 수 있다. 런타임에 요청 컨텐츠 타입에 매칭되는 뷰가 사용된다.

대부분의 경우 모델은 다수의 어트리뷰트를 갖는다. HttpMessageWriterView를 렌더링에 사용할 모델 어트리뷰트 이름과 함께 설정하여 시리얼라이징 대상 어트리뷰트를 결정할 수 있다. 모델이 가진 어트리뷰트가 하나라면, 그 하나를 사용한다.

1.10. HTTP 캐싱(HTTP Caching)

HTTP 캐싱은 웹 어플리케이션의 성능을 대폭 향상시킬 수 있다. HTTP 캐싱은 Cache-Control 응답 헤더와 Last-Modified, ETag 과 같은, 이어지는 조건 요청 헤더를 중심으로 작동한다. Cache-Control은 프라이빗(예: 브라우저), 퍼블릭(예: 프록시) 캐시를 어떻게 캐싱하고 응답에 재사용할지 권고한다. ETag 헤더를 사용하여 변경되지 않은 컨텐츠에 대해 304(NOT_MODIFIED) 본문 없는 응답을 내보내는 조건을 만든다. ETag는 Last-Modified 헤더의 보다 정교한 대체제가 된다.

이 섹션은 스프링 웹플럭스에서 사용 가능한 HTTP 캐싱과 관련된 옵션에 대해 다룬다.

1.10.1. CacheControl

CacheControl은 Cache-Control 헤더와 관련된 설정을 제공하고, 다양한 곳에서 아규먼트로 사용할 수 있다:

RFC 7234는 Cache-Control 응답 헤더를 위한 모든 가능한 디렉티브를 기술한다. CacheControl 타입은 다음과 같이 공통된 시나리오에 맞춘, 유스케이스 지향적인(use case-oriented) 방법을 취한다:

Java
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);

// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();

// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
Kotlin
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)

// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()

// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()


1.10.2. 컨트롤러

컨트롤러는 HTTP 캐싱 지원 요소를 명시적으로 추가할 수 있다. 자원의 lastModified 혹은 ETag 값은 조건부 요청 해더와 비교하기 전에 계산되어야 하기 때문에, 이 방법을 사용할 것을 권한다. 컨트롤러는 ETag와 Cache-Control 설정을 ResponseEntit 에 추가할 수 있다:

Java
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {

    Book book = findBook(id);
    String version = book.getVersion();

    return ResponseEntity
            .ok()
            .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
            .eTag(version) // lastModified is also available
            .body(book);
}
Kotlin
@GetMapping("/book/{id}")
fun showBook(@PathVariable id: Long): ResponseEntity<Book> {

    val book = findBook(id)
    val version = book.getVersion()

    return ResponseEntity
            .ok()
            .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
            .eTag(version) // lastModified is also available
            .body(book)
}


위 예제는 조건부 요청 헤더와의 비교 결과 컨텐츠가 변경되지 않았음이 확인되면 304(NOT_MODIFIED) 응답과 빈 본문을 전송한다. 그렇지 않으면 ETag와 Cache-Control 헤더를 응답에 추가해 보낸다.

컨트롤러의 조건부 요청 헤더 체크를 다음과 같이 만들 수도 있다:

Java
@RequestMapping
public String myHandleMethod(ServerWebExchange exchange, Model model) {

    long eTag = ... (1)

    if (exchange.checkNotModified(eTag)) {
        return null; (2)
    }

    model.addAttribute(...); (3)
    return "myViewName";
}
Kotlin
@RequestMapping
fun myHandleMethod(exchange: ServerWebExchange, model: Model): String? {

    val eTag: Long = ... (1)

    if (exchange.checkNotModified(eTag)) {
        return null(2)
    }

    model.addAttribute(...) (3)
    return "myViewName"
}


(1) 어플리케이션 특징적 계산. (2) 304(NOT_MODIFIED) 응답 설정. 더 이상의 처리는 없다. (3) 요청 처리.

조건부 요청에 대한 eTag, lastModified 값 체크에는 세 가지 변형이 있다. 조건부 GET 과 HEAD 요청에는 304(NOT_MODIFIED)를 설정한다. POST, PUT, DELETE 에는 409(PRECONDITION_FAILED)를 설정하여 동시 변경을 방지한다.

1.10.3. 정적 자원

정적 자원을 Cache-Control 과 조건부 응답 헤더와 함께 제공하여 성능 최적화를 도모할 수 있다. 정적 자원 설정하기 센셕을 보라.

1.11 웹플럭스 설정(WebFlux Config)

웹플럭스 자바 설정은 어노테이티드 컨트롤러 혹은 함수형 엔드로인트로 요청을 처리하기 위해 필요한 컴포넌트를 선언하고, 설정을 커스터마이징하기 위한 API를 제공한다. 이는 자바 설정으로 생성되는 기반 빈을 이해할 필요가 없다는 의미이다. 하지만 기반 빈을 이해하고 싶다면 WebFluxConfigurationSupport을 보거나, 혹은 스페셜 빈 타입에서 필요한 내용을 읽어보자.

더 고급의 커스터마이징을 위해서는 설정 API가 아닌 고급 설정 모드를 통한 전체 설정 관리가 필요하다.

1.11.1. 웹플럭스 설정 활성화(Enabling WebFlux Config)

@EnableWebFlux를 자바 설정에 추가한다:

Java
@Configuration
@EnableWebFlux
public class WebConfig {
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig


위 예제는 스프링 웹플럭스의 다양한 인프라스트럭처 빈을 등록하고 클래스에서 유효한 의존성을 묶는다 - JSON, XML, 기타 등등.

1.11.2. 웹플럭스 설정 API(WebFlux config API)

자바 설정에 WebFluxConfigurer 인터페이스를 구현할 수 있다. 다음은 그 예제이다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // Implement configuration methods...
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    // Implement configuration methods...
}


1.11.3. 컨버젼, 포맷팅(Conversion, formatting)

@NumberFormat, @DateTimeFormat을 통해 Number, Date 타입 포맷터는 기본적으로 지원한다. 클래스패스에 Joda-Time 이 존재할 경우 Joda-Time 포맷팅 라이브러리를 위한 완전한 지원도 제공한다.

다음 예제는 커스텀 포맷터와 컨버터 등록 방법을 보여준다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // ...
    }

}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        // ...
    }
}


# FormatterRegistrar 구현체를 언제 사용할지에 관한 더 자세한 내용은 FormatterRegistrar SPI와 FormattingConversionServiceFactoryBean을 보라.

1.11.4. 밸리데이션(Validation)

빈 벨리데이션이 클래스패스에 존재할 경우(예: 하이버네이트 밸리데이터), 기본적으로 LocalValidatorFactoryBean 이 전역 밸리데이터로 등록되어 @Valid, Validated를 @Controller 메서드의 아규먼트로 사용할 수 있다.

자바 설정에서 전역 Validator 인스턴스를 커스터마이징할 수 있다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public Validator getValidator(); {
        // ...
    }

}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun getValidator(): Validator {
        // ...
    }

}


Validator 구현체를 지역적으로 등록하는 것 또한 가능하다:

Java
@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }

}
Kotlin
@Controller
class MyController {

    @InitBinder
    protected fun initBinder(binder: WebDataBinder) {
        binder.addValidators(FooValidator())
    }
}


# 어딘가에 LocalValidatorFactoryBean을 주입해야 한다면, 빈을 생성하고 @Primary를 사용하면 MVC 설정에서 선언된 빈과의 충돌을 회피할 수 있다.

1.11.5. 컨텐츠 타입 리졸버(Content Type Resolvers)

스프링 웹플럭스가 @Controller 인스턴스로 요청된 미디어 타입을 결정하는 방법을 설정할 수 있다. 기본적으로 Accept 헤더만을 체크하지만, 쿼리 파라미터 기반의 체크도 가능하다.

다음은 요청된 컨텐츠 타입 리솔루션 커스터마이징 예제이다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
        // ...
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) {
        // ...
    }
}


1.11.6. HTTP 메시지 코덱(HTTP message codecs)

다음 예제는 요청과 응답 본문을 읽고 작성하는 방법을 커스터마이징하는 방법을 보여준다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // ...
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        // ...
    }
}


ServerCodecConfigurer는 디폴트 readers와 writers 집합을 제공한다. ServerCodecConfigurer를 사용하여 reader와 writer를 추가하고 디폴트를 커스터마이징하거나 다른 것으로 교체할 수 있다.

Jackson JSON, XML 사용에 대해서는 Jackson2ObjectMapperBuilder 사용을 고려하라. Jackson의 디폴트 프로퍼티를 다음 중 하나로 커스터마이징할 수 있다:

또한, 다음의 잘 알려진 모듈들 중, 클래스패스에 존재하는 것을 자동으로 감지하여 등록한다:

1.11.7. 뷰 리졸버(View Resolvers)

다음 예제는 뷰 리졸버 설정을 보여준다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // ...
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // ...
    }
}


ViewResolverRegistry는 스프링 프레임워크와 통합된 뷰 기술을 위한 간단한 등록 방법을 제공한다. 다음 예제는 프리마커를 사용한다(기반 프리마커 뷰 기술 설정이 요구됨).

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {


    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
    }

    // Configure Freemarker...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        return configurer;
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()
    }

    // Configure Freemarker...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates")
    }
}


또한 다음과 같이 어떠한 ViewResolver 구현체든 등록할 수 있다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {


    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        ViewResolver resolver = ... ;
        registry.viewResolver(resolver);
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        val resolver: ViewResolver = ...
        registry.viewResolver(resolver
    }
}


컨텐츠 협상과 뷰 리솔루션을 통한(HTML 이 아닌) 다른 포맷 렌더링 지원을 위해, HttpMessageWriterView 구현체에 기반한 하나 이상의 디폴트 뷰를 설정할 수 있다. HttpMessageWriterView 구현체는 spring-web으로부터 유효한 어느 코덱이든 받아들인다. 다음 예제는 그 설정을 보여준다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {


    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();

        Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
        registry.defaultViews(new HttpMessageWriterView(encoder));
    }

    // ...
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {


    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()

        val encoder = Jackson2JsonEncoder()
        registry.defaultViews(HttpMessageWriterView(encoder))
    }

    // ...
}


뷰 기술에서 스프링 웹플럭스와 통합된 뷰 기술에 대한 더 자세한 내용을 찾아볼 수 있다.

1.11.8. 정적 자원(Static Resources)

이 옵션은 정적 자원을 Resource 기반 위치 목록으로부터 서비스하기 위한 편리한 방법을 제공한다.

다음 예제에서 상대경로 /resources로 시작하는 요청은 클래스패스의 /static 경로의 정적 자원을 찾아 서비스한다. 자원의 브라우저 캐싱 유지 기간을 1년으로 설정한다. Last-Modified 헤더가 있을 경우 그 값을 평가하고 304 상태 코드를 반환한다. 다음은 그 예제이다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
            .addResourceLocations("/public", "classpath:/static/")
            .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
    }

}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public", "classpath:/static/")
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
    }
}


자원 핸들러는 ResourceResolver, ResourceTransformer 구현체의 체인을 지원한다. 이 구현체들은 최적화된 자원으로 서비스하기 위한 툴체인을 생성하기 위해 사용된다.

컨텐츠, 어플리케이션 버전 또는 다른 정보로부터 계산된 MD5 해시에 기반하여 버저닝된(versioned) 자원 URL을 위해 VersionResourceResolver를 사용할 수 있다. ContentVersionStrategy(MD5 해시) 는(모듈 로더와 함께 사용되는 자바스크립트 자원과 같은) 몇몇 중요한 예외를 제외하고 좋은 선택이 된다.

다음 예제는 자바 설정에서 VersionResourceResolver를 사용하는 예제이다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }

}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
    }

}


ResourceUrlProvider를 사용하여 URL 재작성(rewrite) 및 리졸버와 트랜스포머 의 완전 체이닝을 적용할 수 있다(예: 버전 입력을 위해). 웹플럭스 설정은 ResourceUrlProvider를 제공하여 이런 컴포넌트들을 다른 곳으로 주입할 수 있도록 한다.

스프링 MVC 와는 달리, 현재 웹플럭스에서는 정적 자원 URL을 투명하게 재작성하는 방법은 없다. 왜냐하면 논 블로킹 리졸버와 트랜스포머 체인을 사용하는 뷰 기술이 존재하지 않기 때문이다. 로컬 자원만을 서비스할 때는 ResourceUrlProvider를 직접 사용하고(예: 커스텀 엘리먼트를 통해) 블로킹 방식을 취하는 것이 대안이 된다.

EncodedResourceResolver(예: Gzip, Brotli) 와 VersionedResourceResolver를 사용할 때는 컨텐츠 기반 버전은 언제나 인코딩된 파일을 기반으로 하도록 반드시 해당 순서로 등록해야 한다.

WebJars는 WebJarsResourceResolver를 통해 지원된다. WebJarsResourceResolver는 org.webjars:webjars-locator-core 라이브러리가 클래스패스에 존재할 경우 자동으로 등록된다. 이 리졸버는 URL을 재작성하여 jar의 버전을 포함하도록 하고, 버전 없이 들어오는 요청 URL 과 매칭한다. 예를 들어, /jquery/jquery.min.js를 /jquery/1.2.0/jquery.min.js와 매칭한다.

1.11.9. 경로 매칭(path Matching)

경로 매칭 관련 옵션을 커스터마이징할 수 있다. 각각의 옵션에 관한 자세한 내용은 PathMatchConfigurer 자바독을 보라. 다음은 PathMatchConfigurer 예제이다:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer
            .setUseCaseSensitiveMatch(true)
            .setUseTrailingSlashMatch(false)
            .addPathPrefix("/api",
                    HandlerTypePredicate.forAnnotation(RestController.class));
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Override
    fun configurePathMatch(configurer: PathMatchConfigurer) {
        configurer
            .setUseCaseSensitiveMatch(true)
            .setUseTrailingSlashMatch(false)
            .addPathPrefix("/api",
                    HandlerTypePredicate.forAnnotation(RestController::class.java))
    }
}


# 스프링 웹플럭스는 세미콜론이 제거된 상태에서(경로 혹은 매트릭스 변수) 디코딩된 경로 세그먼트에의 접근을 위해 RequestPath 라 불리는 파싱된 요청 경로 표기에 의존한다. 이는 스프링 MVC 와는 다르게, 경로 매칭 목적으로 요청 경로의 디코딩 여부나 세미콜론의 제거 여부를 표시할 필요가 없음을 의미한다.

스프링 MVC 와는 달리, 스프링 웹플럭스는 접미어 패턴 매칭을 지원하지 않는다. 접미어 패턴에 의존하지 않는 다른 방법을 권한다.

1.11.10. 고급 설정 모드(Advanced Configutaion Mode)

@EnableWebFlux는 DelegatingWebFluxConfiguration을 임포트한다:
  • 웹플럭스 어플리케이션을 위한 디폴트 스프링 설정을 제공한다.
  • WebFluxConfigurer 구현체를 감지하고 위임을 통해 설정을 커스터마이징한다.


고급 모드 사용을 위해 @EnableWebFlux를 제거하고 DelegatingWebFluxConfiguration을 직접 확장할 수 있다. WebFluxConfigurer는 구현하지 않는다. 다음은 그 예제이다:

Java
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

    // ...
}
Kotlin
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {

    // ...
}


기존 WebConfig의 메서드를 유지할 수 있지만, 기본 클래스로부터 빈 선언을 오버라이딩할 수도 있으며, 여전히 클래스패스에 WebMvcConfigurer 구현체를 몇 개든 가질 수 있다.

1.12. HTTP/2

서블릿 4 컨테이너는 HTTP/2를 지원해야 하고, 스프링 프레임워크 5 는 서블릿 API 4 와 호환된다. 프로그래밍 모델 관점에서는 어플리케이션에 더 해야할 일은 없다. 그러나 서버 설정에 관한 고려사항이 있다. HTTP/2 위키 페이지에서 더 자세한 내용을 찾아볼 수 있다.

스프링 웹플럭스는 현재 네티를 사용한 HTTP/2를 지원하지 않는다. 그리고 프로그래밍 방식 자원 푸싱(pushing)도 지원하지 않는다.

2. 웹클라이언트(WebClient)

스프링 웹플럭스는 HTTP 요청을 위한 리액티브한 논 블로킹 WebClient를 포함한다. 이 클라이언트는 선언적 구성을 위한 리액티브 타입을 사용하는 함수형의 능수능란한 API를 가지고 있다. 리액티브 라이브러리를 보라. 웹플럭스 클라이언트와 서버는 동일한 논 블로킹 코덱에 의존하여 요청와 응답 내용을 인코딩 및 디코딩한다.

WebClient는 내부적으로 HTTP 클라이언트 라이브러리에게 위임한다. 기본적으로 Reactor Netty(이하 리액터 네티)를 사용하고, 제티 리액티브 HttpClient를 위한 내장형 지원이 있다. 그리고 ClientHttpConnector를 통해 다른 라이브러리도 사용 가능하다.

2.1. 설정(Configuration)

WebClient를 생성하는 가장 단순한 방법은 스태틱 팩터리 메서드를 사용하는 것이다:
  • WebClient.create()
  • WebClient.create(String baseUrl)


위의 두 메서드는 리액터 네티를 디폴트 세팅으로 사용한다. 클래스패스에 io.projectreactor.netty:reactor-netty가 존재해야 한다.

WebClient.builder()를 몇 가지 옵션과 함께 사용할 수도 있다:
  • uriBuilderFactory: 기본 URL으로 사용하기 위한 커스터마이징된 UriBuilderFactory.
  • defaultHeader: 모든 요청에 대한 헤더.
  • defaultCookie: 모든 요청에 대한 쿠키.
  • defaultRequest: 모든 요청을 커스터마이징하기 위한 Consumer.
  • filter: 모든 요청에 대한 클라이언트 필터.
  • exchangeStrategies: HTTP 메시지 reader/writer 커스터마이징.
  • clientConnector: HTTP 클라이언트 라이브러리 세팅.


다음 예제는 HTTP 코덱을 설정한다:

Java
WebClient client = WebClient.builder()
        .exchangeStrategies(builder -> {
                return builder.codecs(codecConfigurer -> {
                    //...
                });
        })
        .build();
Kotlin
val webClient = WebClient.builder()
        .exchangeStrategies { strategies ->
            strategies.codecs {
                //...
            }
        }
        .build()


한 번 만들어진 WebClient 인스턴스는 불변형이다. 하지만 원본 인스턴스에 영향을 주지 않으면서 인스턴스를 복제하고 변경된 복제복을 만드는 일이 가능하다. 다음은 그 예제이다:

Java
WebClient client1 = WebClient.builder()
        .filter(filterA).filter(filterB).build();

WebClient client2 = client1.mutate()
        .filter(filterC).filter(filterD).build();

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD
Kotlin
val client1 = WebClient.builder()
        .filter(filterA).filter(filterB).build()

val client2 = client1.mutate()
        .filter(filterC).filter(filterD).build()

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD


2.1.1. MaxInMemorySize

스프링 웹플럭스는 어플리케이션의 메모리 이슈를 피하기 위해 코덱의 데이터 인 메모리 버퍼링 사이즈에 제한을 둘 수 있다. 이 제한의 디폴트 값은 256KB 이며, 이 값으로 충분하지 못할 때는 다음 메시지를 만나게 된다:

# org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer

모든 디폴트 코덱에 적용되는 제한값을 설정할 수 있다. 다음 그 예제이다:

Java
WebClient webClient = WebClient.builder()
        .exchangeStrategies(builder ->
            builder.codecs(codecs ->
                codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
            )
        )
        .build();
Kotlin
val webClient = WebClient.builder()
    .exchangeStrategies { builder ->
            builder.codecs {
                it.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
            }
    }
    .build()


2.1.2. 리액터 네티(Reactor Netty)

리액터 네티 설정 커스터마이징을 위한 미리 설정된 HttpClient를 제공한다:

Java
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);

WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
Kotlin
val httpClient = HttpClient.create().secure { ... }

val webClient = WebClient.builder()
    .clientConnector(ReactorClientHttpConnector(httpClient))
    .build()


Resources



기본적으로, HttpClient는 이벤트 루프 쓰레드와 커넥션 풀을 포함하여 reactor.netty.http.HttpResources 에 있는 전역 리액터 네티에 속에 있다. 이벤트 루프 동시성에는 고정된 공유 자원들이 선호되므로, 이 모드가 권고된다. 이 모드에서는 전역적 자원은 프로세스가 종료될 때까지 유효하다.

서버가 프로세스에 맞추어져 있다면, 일반적으로 명시적 셧다운이 필요하지 않다. 하지만 만약 서버가 프로세스 안에서 시작되고 정지될 수 있다면(예를 들어, WAR로 배포된 스프링 MVC 어플리케이션), 여기에는 ReactorResourceFactory를 globalResources=true(디폴트) 로 설정한 스프링 관리 빈을 선언하여 리액터 네티 전역 자원이 스프링 ApplicationCotext의 클로징 시점에 셧다운되도록 할 수 있다. 다음은 그 예제이다:

Java
@Bean
public ReactorResourceFactory reactorResourceFactory() {
    return new ReactorResourceFactory();
}
Kotlin
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()


또한 글로벌 리액터 네티 자원에 속하지 않도록 할 수도 있는데, 이 모드에서는 리액터 네티 클라이언트와 서버 인스턴스가 공유된 자원을 사용하도록 하는 것은 당신의 몫이 된다. 다음은 그 예제이다:

Java
@Bean
public ReactorResourceFactory resourceFactory() {
    ReactorResourceFactory factory = new ReactorResourceFactory();
    factory.setUseGlobalResources(false); (1)
    return factory;
}

@Bean
public WebClient webClient() {

    Function<HttpClient, HttpClient> mapper = client -> {
        // Further customizations...
    };

    ClientHttpConnector connector =
            new ReactorClientHttpConnector(resourceFactory(), mapper); (2)

    return WebClient.builder().clientConnector(connector).build(); (3)
}
Kotlin
@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
    isUseGlobalResources = false (1)
}

@Bean
fun webClient(): WebClient {

    val mapper: (HttpClient) -> HttpClient = {
        // Further customizations...
    }

    val connector = ReactorClientHttpConnector(resourceFactory(), mapper) (2)

    return WebClient.builder().clientConnector(connector).build() (3)
}


(1) 전역 자원과 독립된 자원을 생성한다. (2) 자원 팩토리로 ReactorClientHttpConnector생성자를 사용한다. (3) 커넥터를 WebClient.Builder에 연결한다.

타임아웃(Timeouts)



커넥션 타임아웃은 다음과 같이 설정한다:

Java
import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));
Kotlin
import io.netty.channel.ChannelOption

val httpClient = HttpClient.create()
        .tcpConfiguration { it.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)}


read/write 타임아웃은 다음과 같이 설정한다:

Java
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;

HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(10))
                        .addHandlerLast(new WriteTimeoutHandler(10))));
Kotlin
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler

val httpClient = HttpClient.create().tcpConfiguration {
    it.doOnConnected { conn -> conn
            .addHandlerLast(ReadTimeoutHandler(10))
            .addHandlerLast(WriteTimeoutHandler(10))
    }
}


2.1.3. 제티(Jetty)

다음 예제는 제티 HttpClient 설정 커스터마이징을 보여준다:

Java
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
ClientHttpConnector connector = new JettyClientHttpConnector(httpClient);

WebClient webClient = WebClient.builder().clientConnector(connector).build();
Kotlin
val httpClient = HttpClient()
httpClient.cookieStore = ...
val connector = JettyClientHttpConnector(httpClient)

val webClient = WebClient.builder().clientConnector(connector).build();


기본적으로 HttpClient는 자신의 리소스(Executor, ByteBufferPool, Scheduler)를 사용한다. 이 리소스들은 프로세스가 종료되거나 stop() 이 호출될 때까지 유효하다.

리소스를 다수의 제티 클라이언트(그리고 서버) 인스턴스 사이에서 공유할 수 있고, JettyResourceFactory를 스프링 관리 빈으로 선언함으로써 스프링 ApplicationContext가 클로징될 때 셧다운되도록 할 수 있다. 다음은 그 예제이다:

Java
@Bean
public JettyResourceFactory resourceFactory() {
    return new JettyResourceFactory();
}

@Bean
public WebClient webClient() {

    HttpClient httpClient = new HttpClient();
    // Further customizations...

    ClientHttpConnector connector =
            new JettyClientHttpConnector(httpClient, resourceFactory()); (1)

    return WebClient.builder().clientConnector(connector).build(); (2)
}
Kotlin
@Bean
fun resourceFactory() = JettyResourceFactory()

@Bean
fun webClient(): WebClient {

    val httpClient = HttpClient()
    // Further customizations...

    val connector = JettyClientHttpConnector(httpClient, resourceFactory()) (1)

    return WebClient.builder().clientConnector(connector).build() (2)
}


(1) JettyClientHttpConnector 생성자에 리소스 팩터리를 사용한다. (2) 커넥터를 WebClient.Builder에 연결한다.

2.2. retrieve()

retrieve() 메서드는 응답 본문을 얻고 디코딩하기 위한 가장 쉬운 방법이다. 다음은 그 예제이다:

Java
WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono(Person.class);
Kotlin
val client = WebClient.create("https://example.org")

val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .awaitBody<Person>()


응답으로부터 디코딩된 객체의 스트림을 얻을 수도 있다:

Java
Flux<Quote> result = client.get()
        .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
        .retrieve()
        .bodyToFlux(Quote.class);
Kotlin
val result = client.get()
        .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
        .retrieve()
        .bodyToFlow<Quote>()


기본적으로 상태 코드 4xx 혹은 5xx 응답의 결과는 WebClientResponseException 혹은 WebClientResponseException.BadRequest, WebClientResponseException.NotFound, 기타 등등과 같은 HTTP 상태 특징적 서브클래스가 된다. onStatus 메서드를 사용하여 결과 익셉션을 커스터마이징할 수 있다. 다음은 그 예제이다:

Java
Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError, response -> ...)
        .onStatus(HttpStatus::is5xxServerError, response -> ...)
        .bodyToMono(Person.class);
Kotlin
val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError) { ... }
        .onStatus(HttpStatus::is5xxServerError) { ... }
        .awaitBody<Person>()


onStatus가 사용될 때, 그 응답이 내용을 가지고 있다고 예상된다면 onStatus 콜백은 이를 소비해야 한다. 그렇지 않으면 응답 내용은 리소스를 릴리즈하기 위해 자동으로 비워진다.

2.3. exchange()

exchange() 메서드는 retrieve 메서드보다 더 많은 기능을 제공한다. 다음 예제는 retrieve() 와 동등하면서도 ClientResponse 에의 접근을 제공한다:

Java
Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .exchange()
        .flatMap(response -> response.bodyToMono(Person.class));
Kotlin
val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .awaitExchange()
        .awaitBody<Person>()


이 레벨에서는 완전한 ResponseEntity를 생성할 수도 있다:

Java
Mono<ResponseEntity<Person>> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .exchange()
        .flatMap(response -> response.toEntity(Person.class));
Kotlin
val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .awaitExchange()
        .toEntity<Person>()


retrieve() 와는 달리, exchange()를 사용하면 4xx, 5xx 응답에 대한 자동적인 에러 시그널이 존재하지 않는다. 직접 상태 코드를 체크하여 그 뒤의 동작을 결정해야 한다.

# exchange() 사용 시 응답 본문은 언제나 소비되거나 릴리즈되어야 한다. 익셉션이 발생하더라도 마찬가지이다(DataBuffer 사용하기를 보라). 보통, 본문을 원하는 타입의 객체로 컨버팅하기 위해 ClientResponse의 bodyTo* 혹은 toEntity*를 실행하는데, releaseBody()를 사용해서 본문 컨텐츠를 소비하지 않고 버리는 일도 가능하다. 아니면 toBodilessEntity()를 사용해서 상태와 헤더만을 얻을 수도 있다(본문은 버린다).

그리고, bodyToMono(Void.class) 가 있다. 이 방법은 오직 응답 컨텐츠가 없다고 예상되는 경우에 한하여 사용되어야 한다. 만일 응답이 컨텐츠를 가지고 있다면 그 커넥션은 닫히고, 커넥션 풀로 반환되지 않는다. 왜냐하면 이 커넥션은 재사용 가능한 상태로 남지 않기 때문이다.

2.4. 요청 본문(Request Body)

요청 본문은 Mono 혹은 코틀린 코루틴 Deferred와 같이, ReactiveAdapterRegistry가 핸들링하는 어떠한 비동기 타입으로부터든 인코딩 가능하다. 다음은 그 예제이다:

Java
Mono<Person> personMono = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body(personMono, Person.class)
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
val personDeferred: Deferred<Person> = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body<Person>(personDeferred)
        .retrieve()
        .awaitBody<Unit>()


또한 인코딩된 객체의 스트림을 가질 수도 있다:

Java
Flux<Person> personFlux = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_STREAM_JSON)
        .body(personFlux, Person.class)
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
val people: Flow<Person> = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body(people)
        .retrieve()
        .awaitBody<Unit>()


아니면, 실제 값을 가진 경우 bodyValue 메서드를 사용할 수도 있다:

Java
Person person = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(person)
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
val person: Person = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(person)
        .retrieve()
        .awaitBody<Unit>()


2.4.1. 폼 데이터(Form Data)

폼 데이터를 전송하기 위해 요청 본문으로 MultiValueMap<String, String> 을 사용할 수 있다. 본문 컨텐츠는 FormHttpMessageWriter 에 의해 자동으로 application/x-www-form-urlencoded로 설정된다. 다음 예제는 MultiValueMap<String, String> 을 사용한다:

Java
MultiValueMap<String, String> formData = ... ;

Mono<Void> result = client.post()
        .uri("/path", id)
        .bodyValue(formData)
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
val formData: MultiValueMap<String, String> = ...

client.post()
        .uri("/path", id)
        .bodyValue(formData)
        .retrieve()
        .awaitBody<Unit>()


BodyInserters를 사용하여 폼 데이터 인라인으로 생성할 수도 있다:

Java
import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(fromFormData("k1", "v1").with("k2", "v2"))
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
import org.springframework.web.reactive.function.BodyInserters.*

client.post()
        .uri("/path", id)
        .body(fromFormData("k1", "v1").with("k2", "v2"))
        .retrieve()
        .awaitBody<Unit>()


2.4.2. 멀티파트 데이터(Multipart Data)

멀티파트 데이터를 전송하기 위해 파트 컨텐츠 혹은 파트에 대한 컨텐츠나 헤더를 나타내는 HttpEntity 인스턴스를 나타내는 Object 인스턴스를 값으로 가진 MultiValueMap<String, ?> 을 사용할 수 있다. MultipartBodyBulder는 멀티파트 요청을 준비하기 위한 편리한 API를 제공한다. 다음 예제는 MultiValueMap<String, ?> 을 생성한다:

Java
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request

MultiValueMap<String, HttpEntity<?>> parts = builder.build();
Kotlin
val builder = MultipartBodyBuilder().apply {
    part("fieldPart", "fieldValue")
    part("filePart1", new FileSystemResource("...logo.png"))
    part("jsonPart", new Person("Jason"))
    part("myPart", part) // Part from a server request
}

val parts = builder.build()


대부분의 경우, 각 파트에 대한 Content-Type을 지정할 필요는 없다. 컨텐츠 타입은 시리얼라이징을 위해 선택된 HttpMessageWriter, 혹은 파일 확장자에 기반한 Resource를 바탕으로 자동적으로 결정된다. 필요에 따라서 오버로딩된 빌더 part 메소드를 통해 각 파트가 사용할 MediaType을 명시적으로 제공할 수 있다.

한 번 MultiValueMap 이 준비되면, 이를 WebClient에게 전달하는 가장 쉬운 방법은 body 메서드를 사용하는 것이다. 다음은 그 예제이다:

Java
MultipartBodyBuilder builder = ...;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(builder.build())
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
val builder: MultipartBodyBuilder = ...

client.post()
        .uri("/path", id)
        .body(builder.build())
        .retrieve()
        .awaitBody<Unit>()


MultiValueMap 이 최소 하나의 non-String 값을 포함한다면 Content-Type을 multipart/form-data로 세팅할 필요가 없다. 이 때의 값은 일반 폼 데이터(application/x-www-form-urlencoded)를 나타낼 수도 있다. 이는 언제나 MultipartBodyBuilder를 사용하는 경우가 된다. 이 빌더는 HttpEntity 래퍼를 보장한다.

MultipartBodyBuilder의 대안으로, 내장형 BodyInserter를 통한 인라인 스타일의 멀티파트 컨텐츠를 제공할 수도 있다. 다음은 그 예제이다:

Java
import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
import org.springframework.web.reactive.function.BodyInserters.*

client.post()
        .uri("/path", id)
        .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
        .retrieve()
        .awaitBody<Unit>()


2.5. 클라이언트 필터(Client Filters)

요청을 인터셉팅하고 변경하기 위해 WebClient.Builder를 사용해서 클라이언트 필터(ExchangeFilterFunction)를 등록할 수 있다. 다음은 그 예제이다:

Java
WebClient client = WebClient.builder()
        .filter((request, next) -> {

            ClientRequest filtered = ClientRequest.from(request)
                    .header("foo", "bar")
                    .build();

            return next.exchange(filtered);
        })
        .build();
Kotlin
val client = WebClient.builder()
        .filter { request, next ->

            val filtered = ClientRequest.from(request)
                    .header("foo", "bar")
                    .build()

            next.exchange(filtered)
        }
        .build()


필터는 인증과 같은 크로스커팅 관심사를 다루기 위해 사용될 수 있다. 다음 예제는 필터를 사용하여 스태틱 팩토리 메서드를 통해 기본 인증을 처리한다:

Java
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

WebClient client = WebClient.builder()
        .filter(basicAuthentication("user", "password"))
        .build();
Kotlin
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication

val client = WebClient.builder()
        .filter(basicAuthentication("user", "password"))
        .build()


필터는 모든 요청에 글로벌하게 적용된다. 특정 요청에 대한 필터의 동작을 변경하려면, 체인 안의 모든 필터가 접근하는 ClientRequest 에 요청 어트리뷰트를 추가할 수 있다. 다음은 그 예제이다:

Java
WebClient client = WebClient.builder()
        .filter((request, next) -> {
            Optional<Object> usr = request.attribute("myAttribute");
            // ...
        })
        .build();

client.get().uri("https://example.org/")
        .attribute("myAttribute", "...")
        .retrieve()
        .bodyToMono(Void.class);

    }
Kotlin
val client = WebClient.builder()
            .filter { request, _ ->
        val usr = request.attributes()["myAttribute"];
        // ...
    }.build()

    client.get().uri("https://example.org/")
            .attribute("myAttribute", "...")
            .retrieve()
            .awaitBody<Unit>()


기존 WebClient를 복제하거나, 새 필터를 추가하거나, 이미 등록된 필터를 제거하는 것도 가능하다. 다음은 인덱스 0 에 기본 인증 필터를 추가한다:

Java
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

WebClient client = webClient.mutate()
        .filters(filterList -> {
            filterList.add(0, basicAuthentication("user", "password"));
        })
        .build();
Kotlin
val client = webClient.mutate()
        .filters { it.add(0, basicAuthentication("user", "password")) }
        .build()


2.6. 동기식 사용(Synchronous Use)

WebClient를 결과를 위한 끝에 블로킹하는 방법으로 동기 방식으로 사용할 수 있다:

Java
Person person = client.get().uri("/person/{id}", i).retrieve()
    .bodyToMono(Person.class)
    .block();

List<Person> persons = client.get().uri("/persons").retrieve()
    .bodyToFlux(Person.class)
    .collectList()
    .block();
Kotlin
val person = runBlocking {
    client.get().uri("/person/{id}", i).retrieve()
            .awaitBody<Person>()
}

val persons = runBlocking {
    client.get().uri("/persons").retrieve()
            .bodyToFlow<Person>()
            .toList()
}


하지만 다수의 호출이 필요할 경우, 각 응답에 대한 개별적인 블로킹은 피하는 것이 더 효율적이다. 대신, 결합된 결과를 기다리도록 한다:

Java
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
        .retrieve().bodyToMono(Person.class);

Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
        .retrieve().bodyToFlux(Hobby.class).collectList();

Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
            Map<String, String> map = new LinkedHashMap<>();
            map.put("person", person);
            map.put("hobbies", hobbies);
            return map;
        })
        .block();
Kotlin
val data = runBlocking {
        val personDeferred = async {
            client.get().uri("/person/{id}", personId)
                    .retrieve().awaitBody<Person>()
        }

        val hobbiesDeferred = async {
            client.get().uri("/person/{id}/hobbies", personId)
                    .retrieve().bodyToFlow<Hobby>().toList()
        }

        mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await())
    }


위 코드는 한밭 예시에 불과하다. 많은 수의 원격 호출, 잠재적으로 중첩된, 상호 의존적이면서 끝까지 블로킹하지 않는 리액티브 파이프라인을 만드는 수많은 패턴과 오퍼레이터가 존재한다.

# Flux 또는 Mono를 사용하여, 스프링 MVC 혹은 스프링 웹플럭스 컨트롤러에서 절대로 블로킹하지 않을 수 있다. 간단히 컨트롤러 메서드로부터 리액티브 타입을 반환하는 것이다. 같은 원칙은 코틀린 코루틴과 스프링 웹플럭스에 동일하게 적용된다. 서스펜딩 함수를 사용하거나 컨트롤러 메서드에서 Flow를 반환하라.

2.7. 테스팅(Testing)

WebClient를 사용하는 코드를 테스트하기 위해, OkHttp MockWebServer와 같은 목 웹서버를 사용할 수 있다. 이 서버의 사용 예제를 보려면 스프링 프레임워크 테스트 슈트의 WebClientIntergrationTests, 혹은 OkHttp 리파지토리의 static-server 샘플을 체크아웃하자.

3. 웹소켓(WebSockets)

이 부분은 리액티브 스택 웹소켓 메시징 지원에 대해 다룬다.

3.1. 웹소켓이란(Introduction to WebSocket)

웹소켓 프로토콜, RFC 6455는 단일 TCP 커넥션 위로 클라이언트와 서버 사이의 전 양방의, 양방향 통신 채널을 설정하는 표준적인 방법을 제공한다. 이는 HTTP 와는 다른 TCP 프로토콜이지만, HTTP를 근간으로 동작하도록 설계되었다. 80 과 443 포트를 사용하며, 기존 방화벽 규칙을 재사용한다.

웹소켓 상호작용은 업그레이드를, 혹은 웹소켓 프로토콜로의 스위칭을 위한 HTTP Upgrade 헤더를 사용하는 HTTP 요청과 함께 시작된다. 다음 예제는 이러한 상호작용을 보여준다:

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080


(1) Upgrade 헤더. (2) Upgrade 커넥션 사용.

보통의 200 상태 코드 대신, 웹소켓 서버는 다음과 비슷한 아웃풋을 반환한다.

HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp


1) 프로토콜 스위치

핸드셰이크 성공 후, HTTP 업그레이드 요청의 기반 TCP 소켓은 메시지 송수신을 위해 클라이언트와 서버 양쪽으로 열려있게 된다.

웹소켓 동작에 대한 완전한 소개는 이 문서의 범위를 넘어선다. 더 자세한 내용은 RFC 6455, HTML5 의 웹소켓 챕터, 혹은 웹에 있는 수많은 인트로와 튜토리얼을 보도록 하자.

웹소켓 서버가 웹서버 뒤에서 동작할 때는(예: nginx), 웹소켓 업그레이드 요청을 웹소켓 서버로 보내도록 설정해야 한다. 마찬가지로, 어플리케이션이 클라우드 환경에서 구동된다면 웹소켓 지원에 관련된 클라우드 제공자의 지시를 확인해야 한다.

3.1.1. HTTP와 웹소켓(HTTP Versus WebSocket)

웹소켓이 HTTP와 호환되도록 만들어지고 또 HTTP 요청으로 시작된다고 할지라도, 이 두 프로토콜이 매우 다른 아키텍처와 어플리케이션 프로그래밍 모델을 가지고 있음을 이해할 필요가 있다.

HTTP와 REST 에서는 어플리케이션이 많은 URL으로 모델링된다. 어플리케이션과의 상호작용을 위해서 클라이언트는 이런 URL 에 요청-응답 방식으로 접근해야 한다. 서버는 요청을 HTTP URL, 메서드, 헤더를 바탕으로 적절한 핸들러에게 라우팅한다.

반대로, 웹소켓에서는 주로 초기 커넥션을 위한 오직 하나의 URL 만이 존재한다. 이어서, 모든 어플리케이션 메시지는 같은 TCP 커넥션 위로 흐른다. 이는 완전하게 다른 비동기적, 이벤트 드리븐 메시징 아키텍처를 시사한다.

웹소켓은 또한 로우레벨 전송 프로토콜이다. HTTP 와는 다르게, 메시지의 컨텐츠에 대한 어떠한 시맨틱스도 규정하지 않는다. 이는 클라이언트와 서버가 메시지 시맨틱스에 동의하지 않으면 메시지를 라우팅하고 처리하기 위한 방법이 없다는 의미이다.

웹소켓 클라이언트와 서버는 고수준 메시징 프로토콜(예: STOMP) 의 사용을 HTTP 핸드셰이크 요청의 Sec-WebSocket-Protocol 헤더를 통해 협상할 수 있다. 이 헤더가 없으면 클라이언트와 서버는 자신들의 협상 방식을 스스로 규정해야 한다.

3.1.2. 웹소켓을 언제 사용할 것인가(When to Use WebSocket)

웹소켓은 웹페이지를 동적이고 상호적으로 만든다. 하지만 많은 경우에 있어 Ajax와 HTTP 스트리밍, 혹은 롱 폴링(Ajax and HTTP streaming or long polling)의 결합은 간단하면서 효과적인 솔루션이 된다.

예를 들어, 뉴스, 메일, 소셜 피드는 동적으로 갱신될 필요가 있지만, 몇 분 마다 갱신하는 정도로도 완벽할지 모른다. 반면에 콜라보레이션, 게임, 금융 어플리케이션은 훨씬 더 실시간으로 동작해야 한다.

대기시간만으로는 결정적인 요인이 되지 못한다. 메시지의 볼륨이 상대적으로 작다면(예로, 네트워크 실패 모니터링), HTTP 스트리밍이나 폴링이 효과적인 솔루션이 될 수 있다. 웹소켓을 사용하기 위한 최적의 케이스는 적은 대기시간, 높은 빈도, 큰 볼륨이 함께 요구되는 곳이다.

또한 인터넷에서는 사용자의 통제 밖에 있는 프록시의 제한이 웹소켓의 상호동작을 방해할 수 있음을 유념해야 한다. 왜냐하면 프록시는 Upgrade 헤더를 전달하도록 설정되어 있지 않거나, 오랜 시간 열려 있는, 휴게 중으로 보이는 커넥션은 닫아버리기 때문이다. 이는 웹소켓 사용에 있어 방화벽이 작동하는 환경에서의 인터넷 어플리케이션에 대한 사용이, 자유롭게 접근 가능한 어플리케이션에 대한 사용보다 쉽다는 뜻이다.

3.2. 웹소켓 API(WebSocket API)

스프링 프레임워크는 웹소켓 메시지를 핸들링하는 클라이언트와 서버 사이드 어플리케이션을 작성하기 위한 웹소켓 API를 제공한다.

3.2.1. 서버(Server)

웹소켓 서버를 생성하기 위해, 먼저 WebSocketHandler를 생성할 수 있다. 다음은 그 예제이다:

Java
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;

public class MyWebSocketHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        // ...
    }
}
Kotlin
import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession

class MyWebSocketHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {
        // ...
    }
}


다음은 이 핸들러를 URL 에 매핑하고 WebSocketHandlerAdapter를 추가한다:

Java
@Configuration
class WebConfig {

    @Bean
    public HandlerMapping handlerMapping() {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/path", new MyWebSocketHandler());
        int order = -1; // before annotated controllers

        return new SimpleUrlHandlerMapping(map, order);
    }

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}
Kotlin
@Configuration
class WebConfig {

    @Bean
    fun handlerMapping(): HandlerMapping {
        val map = mapOf("/path" to MyWebSocketHandler())
        val order = -1 // before annotated controllers

        return SimpleUrlHandlerMapping(map, order)
    }

    @Bean
    fun handlerAdapter() =  WebSocketHandlerAdapter()
}


3.2.2. webSocketHandler

WebSocketHandler의 handle 메서드는 WebSocketSession을 아규먼트로 가지며, Mono<Void>를 반환하여 어플리케이션의 세션 핸들링이 완료되었음을 알린다. 세션은 두 스크림을 통해 핸들링되는데, 하나는 인바운드, 다른 하나는 아웃바운드 메시지를 위한 것이다. 다음 테이블은 이 스트림을 핸들링하는 두 메서드에 대해 설명한다:

WebSocketSession 메서드 설명
Flux receive() 인바운드 메시지 스트림에 접근하고 커넥션이 닫혔을 때 완료된다.
Mono send(Publisher) 발신 메시지를 위한 소스를 취하고, 메시지를 작성하고, 소스가 완료되고 메시지 작성이 끝났을 때 완료되는 Mono<Void>를 반환한다.


WebSocketHandler는 인바운드와 아웃바운드 스트림을 반드시 통일환 하나의 흐름으로 구성하고, 이 흐름의 완료를 나타내는 Mono<Void>를 반환해야 한다. 어플리케이션 요건에 따라서, 통일된 흐름은 다음의 경우에 완료된다:
  • 인바운드나 아웃바운드 메시지 스트림 완료 시.
  • 인바운드 스트림 완료되고(커넥션 닫힘), 아웃바운드 스트림이 무한할 시.
  • 선택된 시점에, WebSocketSession의 close 메서드를 통해.


인바운드와 아웃바운드 메시지 스트림이 함께 구성되어 있을 때는 커넥션이 열려 있는지 확인할 필요가 없다. 리액티브 스트림 시그널이 활성화 상태를 종료하기 때문이다. 인바운드 스트림은 완료 혹은 에러 시그널을 수신하고, 아웃바운드 스트림은 취소 시그널을 수신한다.

핸들러의 가장 기본적인 구현체는 인바운드 스트림을 핸들링한다. 다음 예제는 이러한 구현체를 보여준다:

Java
class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        return session.receive()            (1)
                .doOnNext(message -> {
                    // ...                  (2)
                })
                .concatMap(message -> {
                    // ...                  (3)
                })
                .then();                    (4)
    }
}
Kotlin
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {
        return session.receive()            (1)
                .doOnNext {
                    // ...                  (2)
                }
                .concatMap {
                    // ...                  (3)
                }
                .then()                     (4)
    }
}


(1) 인바운트 메시지 스트림에 접근한다. (2) 각 메시지에 대한 동작을 수행한다. (3) 메시지 컨텐츠를 사용하는 비동기 중첩 명령을 수행한다. (4) 수신 완료 시 완료되는 Mono를 반환한다.

# 중첩된 비동기 명령에 대해서는, 풀링된(pooled) 데이터 버퍼를 사용하는 서버(예: 네티)에서는 message.retain()를 호출할 필요가 있을 수 있다. 그렇지 않으면 데이터를 읽기 전에 버퍼가 비워질 수 있다. 더 자세한 배경 설명은 데이터 버퍼와 코덱을 보자.

다음 구현체는 인바운드와 아웃바운드 스트림을 결합한다.

Java
class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {

        Flux<WebSocketMessage> output = session.receive()               (1)
                .doOnNext(message -> {
                    // ...
                })
                .concatMap(message -> {
                    // ...
                })
                .map(value -> session.textMessage("Echo " + value));    (2)

        return session.send(output);                                    (3)
    }
}
Kotlin
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {

        val output = session.receive()                      (1)
                .doOnNext {
                    // ...
                }
                .concatMap {
                    // ...
                }
                .map { session.textMessage("Echo $it") }    (2)

        return session.send(output)                         (3)
    }
}


(1) 인바운드 메시지 스트림을 핸들링한다. (2) 아웃바운드 메시지를 생성하면서 결합된 흐름을 만든다. (3) 수신 중엔 완료되지 않는 Mono를 반환한다.

인바운드와 아운바운드 스트림은 독립적으로 존재할 수 있고, 완료 시에만 결합할 수 있다. 다음은 그 예제이다:

Java
class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {

        Mono<Void> input = session.receive()                                (1)
                .doOnNext(message -> {
                    // ...
                })
                .concatMap(message -> {
                    // ...
                })
                .then();

        Flux<String> source = ... ;
        Mono<Void> output = session.send(source.map(session::textMessage)); (2)

        return Mono.zip(input, output).then();                              (3)
    }
}
Kotlin
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {

        val input = session.receive()                                   (1)
                .doOnNext {
                    // ...
                }
                .concatMap {
                    // ...
                }
                .then()

        val source: Flux<String> = ...
        val output = session.send(source.map(session::textMessage))     (2)

        return Mono.zip(input, output).then()                           (3)
    }
}


(1) 인바운드 메시지 스트림을 핸들링한다. (2) 발신 메시지를 전송한다. (3) 스트림을 결합하고, 두 스트림 중 하나가 끝나면 완료되는 Mono를 반환한다.

3.2.3. DataBuffer

DataBuffer는 웹플럭스의 바이트 버퍼이다. 스프링 코어 레퍼런스에 이 버퍼에 대해 다루는 섹션 데이터 버퍼와 코덱이 있다. DataBuffer의 키 포인트가 되는 부분은, 네티와 같은 서버에서는 바이트 버퍼는 풀링되고(pooled) 레퍼런트가 카운팅된다는 점, 그리고 메모리 누수를 피하기 위해 소비한 뒤에는 반드시 릴리즈해야 한다는 점이다.

네티에서 구동되는 어플리케이션은 인풋 데이터가 릴리즈되지 않게 묶어두려면 반드시 DataBufferUtils.retain(dataBuffer)를 사용해야 한다. 이어서 버퍼를 소비하고 DataBufferUtil.release(dataBuffer)를 호출해야 한다.

3.2.4. 핸드셰이크(Handshake)

WebSocketHandlerAdapter는 WebSocketService에게 동작을 위임한다. 디폴트 인스턴스는 HandshakeWebSocketService이다. 이 구현체는 웹소켓 요청에 대한 기본적인 확인 작업을 수행한 다음, 구동 서버에 대해 RequestUpgradeStrategy를 사용한다. 현재는 리액터 네티, 톰캣, 제티, 언더토를 내장형으로 지원하고 있다.

HandshakeWebSocketService는 sessionAttributePredicate 프로퍼티를 제공한다. 이 프로퍼티는 Predicate<String> 을 세팅하여 WebSession으로부터 어트리뷰트를 추출해서 WebSocketSession의 어트리뷰트로 입력한다.

3.2.5. 서버 설정(Server Configuration)

RequestUpgradeStrategy는 각 서버에 대한, 기반 웹소켓 엔진에 대해 유효한 웹소켓 관련 설정 옵션을 제공한다. 다음 예제는 톰캣에서 구동되는 웹소켓 옵션을 설정한다.

Java
@Configuration
class WebConfig {

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter(webSocketService());
    }

    @Bean
    public WebSocketService webSocketService() {
        TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
        strategy.setMaxSessionIdleTimeout(0L);
        return new HandshakeWebSocketService(strategy);
    }
}
Kotlin
@Configuration
class WebConfig {

    @Bean
    fun handlerAdapter() =
            WebSocketHandlerAdapter(webSocketService())

    @Bean
    fun webSocketService(): WebSocketService {
        val strategy = TomcatRequestUpgradeStrategy().apply {
            setMaxSessionIdleTimeout(0L)
        }
        return HandshakeWebSocketService(strategy)
    }
}


Upgrade strategy를 체크하여 자신이 구동하는 서버에 대해 어떤 옵션을 사용할 수 있는지 알아보자. 현재는 톰캣과 제티만이 이 옵션을 제공한다.

3.2.6. CORS

CORS를 설정하고 웹소켓 엔드포인트로의 접근을 제한하는 가장 쉬운 방법은 WebSocketHandler가 CorsConfigurationSource를 구현하여 CorsConfiguration을 반환하도록 하는 것이다. CorsConfiguration 에는 허용 오리진, 헤더, 그리고 다른 상세 요소를 담는다. 이 방법이 어렵다면 SImpleUrlHandler의 corsConfigurations 프로퍼티를 설정하는 방법도 있다. 이 방법은 CORS를 URL 패턴 기준으로 설정할 수 있다. 만약 이 두 방법을 모두 사용한다면, CorsConfiguration의 combine 메서드를 통해 두 설정이 결합되어 적용된다.

3.2.7. 클라이언트(Client)

스프링 웹플럭스는 WebSocketClient 추상화에 리액터 네티, 톰캣, 제티, 언더토, 그리고 표준 자바(JSR-356) 에 대한 구현체를 제공한다.

# 톰캣 클라이언트는 WebSocketSession의 몇몇 추가적인 기능을 함께 제공하는 표준 자바의 효과적인 확장판으로, 톰캣 특징적 API의 이점을 취하여 백프레셔를 위해 수신 메시지를 일시 중단한다.

웹소켓 세션을 시작하기 위해 클라이언트의 인스턴스를 생성하고 execute 메서드를 사용한다.

Java
WebSocketClient client = new ReactorNettyWebSocketClient();

URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
        session.receive()
                .doOnNext(System.out::println)
                .then());
Kotlin
val client = ReactorNettyWebSocketClient()

        val url = URI("ws://localhost:8080/path")
        client.execute(url) { session ->
            session.receive()
                    .doOnNext(::println)
            .then()
        }


제티와 같은 몇몇 클라이언트는 Lifecycle을 구현하고, 사용 전에 중지 및 시작해야 한다. 모든 클라이언트는 기반 웹소켓 클라이언트의 설정 관련 옵션을 생성자로 제공한다.

4. 테스팅(Testing)

spring-test 모듈은 ServerHttpRequest, ServerHttpResponse, ServerWebExchange의 mock 구현체를 제공한다. 스프링 웹 리액티브에서 mock 객체에 대해 다룬다.

WebTestClient는 이러한 mock 요청과 응답 객체를 기반으로 한, HTTP 서버 없는 웹플럭스 어플리케이션 테스트를 지원한다. WebTestClient를 사용하여 end-to-end 통합 테스트를 할 수 있다.

5. RSocket

이 섹션은 스프림 프레임워크의 RSocket 프로토콜 지원에 대해 다룬다.

5.1. 개요

RSocket은 TCP, 웹소켓, 그리고 다른 바이트 스트림 전송을 바탕으로 다중화된 양방향 통신을 지원하는 어플리케이션 프로토콜이다. 다음 중 하나를 상호작용 모델로 따른다:
  • Request-Response: 메시지 하나를 송신하고 응답 하나를 수신한다.
  • Request-Stream: 메시지 하나를 송신하고 응답 하나를 스트림으로 수신한다.
  • Channel - 양방향으로 스트림 메시지를 송수신한다.
  • Fire-and-Forget: 단방향 메시지를 송신한다.


초기 커넥션이 맺어지면 "클라이언트" vs "서버" 구분은 무의미하다. 양쪽은 대칭적이며, 한쪽은 위 상호작용 모델 중 하나를 시작할 수 있다. 이것이 통신에 참여하는 양쪽을 "요청자" 와 "응답자" 로 칭하는 이유이다. 위의 상호작용 모델은 "요청 스트림" 혹은 간단히 "요청" 이라 칭한다.

다음은 RSocket 프로토콜의 핵심 기능과 이점이다:
  • 네트워크 경계를 아우르는 리액티브 스트림 시맨틱스: Request-Stream, Channel 과 같은 스트리밍 요청을 위해. 백프레셔는 요청자와 응답자 사이를 오가는 시그널을 보내서 요청자는 응답자의 속도를 근본적으로 늦추도록 한다. 그리하여 네트워크 계층의 혼잡 제어에의 의존과, 네트워크 레벨 혹은 어떤 레벨에서든 버퍼링의 필요성을 감소시킨다.
  • 요청 쓰로틀링: 통신의 한쪽은 LEASE 프레임을 송신하여 주어진 시간 동안 상대쪽에서의 요청의 총량에 제한을 걸 수 있다. 이 기능은 "Leasing" 이라 부른다.
  • 세션 재개: 이 기능은 요청 손실, 상태 보존 요건을 위해 만들어졌다. 상태 관리는 어플리케이션에 대해 명료하게 이루어지며, 가능 여부에 따라 프로듀서를 정지하여 필요한 상태 보존 분량을 감소시키는 백프레셔와 함께 훌륭하게 결합하여 작동한다.
  • 큰 메시지의 단편화(fragmentation) 및 재조립(re-assemply).
  • Keepalive(heartbeats).


RSocket 에는 다양항 언어로 작성된 구현체가 있다. 자바 라이브러리Project Reactor를 기반으로 하며, 데이터 수송을 위해 리액터 네티를 사용한다. 어플리케이션의 리액티브 스트림 퍼블리셔가 내보낸 시그널은 RSocket을 통해 네트워크를 가로질러 투명하게 전파된다.

5.1.1. 프로토콜(The Protocol)

RSocket의 이점 중 하나는 와이어에서의 동작이 잘 정의되어 있고 몇몇 프로토콜 확장에 따르는 읽기 쉬운 스펙이다. 따라서 구현체의 언어와 고수준 프레임워크 API 에 독립적으로 스펙을 읽어보는 것이 좋다. 이 섹션은 간결한 개요를 제공하여 몇몇 문맥을 확립하고자 한다.

연결하기(Connecting)

기본적으로 한 클라이언트는 TCP 혹은 웹소켓과 같은 몇몇 로우 레벨 스트리밍 송신을 통해 한 서버에 연결한다. 그리고 SETUP 프레임을 서버로 전송하여 커넥션을 위한 파라미터를 설정한다.

이 서버는 SETUP 프레임을 거부할 수도 있지만, 보통은 이 프레임이 전송되고 수신되면, SETUP 프레임이 요청 수 제한을 위한 leasing 시맨틱스 사용을 표시하지 않는 이상, 양쪽은 요청을 시작할 수 있다. SETUP 프레임이 leasing 시맨틱스 사용을 표시하면 양쪽은 요청 허가를 위해 반드시 서로의 LEASE 프레임을 기다려야 한다.

요청하기(Making Requests)

한 번 커넥션이 맺어지면, 양쪽은 다음 중 한 프레임을 통해 요청을 시작할 수 있다: REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL, REQUEST_FNF. 이들 각 프레임은 요청자로부터 응답에게 한 메시지를 실어 나른다.

응답자는 응답 메시지와 함께 PAYLOAD 프레임을 반환할 수 있다. 그리고 REQUEST_CHANNEL 요청인 경우, 요청자는 PAYLOAD 프레임과 함께 요청 메시지를 더 보낼 수도 있다.

한 요청이 Request-Stream 과 Channel 과 같은 한 메시지 스트림에 속에 있을 때, 응답자는 반드시 각 시그널의 요구(demand)를 준수해야한다. 요구사항은 메시지의 수로 표현된다. 초기 요구는 REQUEST_STEAM 과 REQUEST_CHANNEL 프레임에 지정되어 있다. 이어지는 요구는 REQUEST_N 프레임을 통해 수행된다.

각 통신측은 메타데이터 알림을 송신할 수도 있다. 여기에는 METADATA_PUSH 프레임이 사용된다. 이 프레임은 어떠한 개별 요청에도 관련되지 않고, 커넥션 전체에 관련된다.

메시지 포맷(Message Format)

RSocket 메시지는 데이터와 메타데이터를 포함한다. 메타데이터는 라우팅, 보안 토큰 및 기타 등등을 전송할 때 사용된다. 데이터와 메타데이터는 각각 다르게 포맷팅된다. 각 데이터에 대한 마임타입은 SETUP 프레임에 선언되어 주어진 커넥션을 통하는 모든 요청에 적용된다.

모든 메시지는 메타데이터를 가질 수 있기 때문에, 보통 라우팅과 같은 메타데이터는 예비 요청이 되고, 따라서 요청의 최초 메시지 안에 포함된다. 예를 들어, 다음 프레임들 중 하나에 포함된다: REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL, REQUEST_FNF.

프로토콜 확장은 어플리케이션에서의 사용을 위해 공통 메타데이터 포맷을 정의한다:

5.1.2. 자바 구현체(Java Implementation)

RSocket 자바 구현체프로젝트 리액터를 기반으로 한다. TCP와 웹소켓 송신은 리액터 네티를 기반으로 한다. 리액티브 스트림 라이브러리로서, 리액터는 프로토콜 구현 역할을 단순화한다. 어플리케이션은 Flux와 Mono를 선언적인 명령과 투명한 백프레셔 지원과 함께 사용하는 것이 자연스럽다.

RSocket 자바 API는 의도적으로 최소형의 기본적인 형태로 만들어졌다. 프로토콜 기능에 집중하며 어플리케이션 프로그래밍 모델(RPC codegen vs others)은 고수준의 독립된 관심사로 남겨두었다.

io.rsocket.RSocket 주요 계약은 네 가지 요청 상호동작 유형을 만든다. 단일 메시지를 위한 약속을 나타내는 Mono, Flux 메시지 스트림, 데이터와 메타데이터로의 바이트 버퍼 접근과 함께하는 실제 메시지 io.rsocket.Payload가 있다. RSocket 계약은 대칭적으로 사용된다. 요청에 대해서는 어플리케이션에 요청을 위한 RSocket 이 주어진다. 응답에 대해서는 어플리케이션은 RSocket을 구현하여 요청을 핸들링한다.

이 내용은 완전한 소개가 되지 못한다. 보통, 스프링 어플리케이션은 이 API를 직접 사용할 필요가 없다. 하지만 RSocket을 스프링과 독립적으로 바라보고 실험하는 것은 중요할 수 있다. RSocket 자바 리파지토리에서 RSocket API와 프로토콜 기능을 보여주는 많은 샘플 앱을 볼 수 있다.

5.1.3. 스프링 지원(Spring Support)

spring-messaging 모델은 다음을 포함한다:
  • RSocketRequester: io.rsocket.RSocket 과 데이터와 메타데이터 인코딩/디코딩을 통해 요청을 생성하는 숙련된 API
  • Annotated Responders: 응답을 위한 @MessageMapping 이 적용된 핸들러 메서드


spring-web 모듈은 Jackson CBOR/JSON 와, RSocket 어플리케이션이 필요로 할 Protobuf와 같은 Encoder와 Decoder 구현체를 포함한다. 그리고 효과적인 라우팅 매칭을 위해 연결하여 사용할 수 있는 PathPatternParser를 제공한다.

스프링 부트 2.2 는 TCP 또는 웹소켓을 통해 RSocket 서버를 지원한다. 웹플럭스 서버에서 웹소켓을 통한 RSocket을 노출하기 위한 옵션을 포함한다. 또한 RSocketRequester.Builder와 RSocketStrategies 에 대한 클라이언트 지원과 자동 설정이 있다. 스프링 부트 레퍼런스의 RSocket 섹션에서 더 자세한 정보를 찾을 수 있다.

스프링 시큐리티 5.2 에서 RSocket을 지원한다.

스프링 인티그레이션 5.2 는 RSocket 클라이언트와 서버의 상호작용을 위한 인바운드와 아웃바운드 게이트웨이를 제공한다. 스프링 인티그레이션 레퍼런스 메뉴얼에서 더 자세한 정보를 찾을 수 있다.

스프링 클라우드 게이트웨이는 RSocket 커넥션을 지원한다.

5.2. RSocketRequester

RSocketRequester는 능숙한 API를 제공하여 RSocket 요청을 수행하고, 데이터와 메타데이터를 로우 레벨 데이터 버퍼대신 객체로 받고 반환한다. 대칭적으로 사용하여 클라이언트나 서버의 요청을 생성할 수 있다.

5.2.1. Client Requester

클라이언트측의 RSocketRequester 를 얻기 위해서는 초기 RSocket SETUP 프레임을 준비하고 전송하는 방법을 통해 서버와 커넥션을 맺어야 한다. RSocketRequester 는 이를 위한 빌더를 제공한다. 내부적으로 RSocket 자바의 RSocketFactory 를 사용한다.

​ 아래는 디폴트 세팅으로 커넥션을 맺는 가장 기본적인 예제이다:

Java
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .connectTcp("localhost", 7000);

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .connectWebSocket(URI.create("https://example.org:8080/rsocket"));
Kotlin
import org.springframework.messaging.rsocket.connectTcpAndAwait
import org.springframework.messaging.rsocket.connectWebSocketAndAwait

val requester = RSocketRequester.builder()
        .connectTcpAndAwait("localhost", 7000)

val requester = RSocketRequester.builder()
        .connectWebSocketAndAwait(URI.create("https://example.org:8080/rsocket"))


​ 위 예제 코드는 커넥션을 유예한다. 실제로 커넥션을 맺고 requester 를 사용하려면 다음과 같이 한다:

Java
// Connect asynchronously
RSocketRequester.builder().connectTcp("localhost", 7000)
    .subscribe(requester -> {
        // ...
    });

// Or block
RSocketRequester requester = RSocketRequester.builder()
    .connectTcp("localhost", 7000)
    .block(Duration.ofSeconds(5));
Kotlin
// Connect asynchronously
import org.springframework.messaging.rsocket.connectTcpAndAwait

class MyService {

    private var requester: RSocketRequester? = null

    private suspend fun requester() = requester ?:
        RSocketRequester.builder().connectTcpAndAwait("localhost", 7000).also { requester = it }

    suspend fun doSomething() = requester().route(...)
}

// Or block
import org.springframework.messaging.rsocket.connectTcpAndAwait

class MyService {

    private val requester = runBlocking {
        RSocketRequester.builder().connectTcpAndAwait("localhost", 7000)
    }

    suspend fun doSomething() = requester.route(...)
}


커넥션 설정(Connection Setup)



RSocketRequester.Builder는 다음 요소를 통해 초기 SETUP 프레임을 커스터마이징한다:
  • dataMimeType(MimeType): 커넥션을 통하는 데이터의 마임 타입을 설정한다.
  • metadataMimeType(MimeType): 커넥션을 통하는 메타데이터의 마임 타입을 설정한다.
  • setupData(Object): SETPUP 프레임에 포함될 데이터를 설정한다.
  • setupRoute(String, Object...): SETUP 프레임에 포함될 메타데이터에 라우팅한다.
  • setupMetaData(Object, MimeType): SETPUP 프레임에 포함될 다른 메타데이터를 설정한다.


데이터에 대한 디폴트 마임 타입은 처음 설정된 Decoder 로부터 얻는다. 메타데이터에 대한 디폴트 마임 타입은 요청 당 다수의 메타데이터 값과 마임 타입 쌍을 허용하는 컴포짓 메타데이터가 된다. 보통 이 둘은 변경할 필요가 없다.

SETUP 프레임의 데이터와 메타데이터는 선택적이다. 서버측에서는 @ConnectMapping 메서드를 사용하여 커넥션의 시작과 SETUP 프레임의 컨텐츠를 핸들링한다. 메타데이터는 커넥션 레벨 보안을 위해 사용할 수도 있다.

전락(Strategies)



RSocketRequester.Builder는 RSocketStrategies를 받아들여 요청자를 설정한다. 이를 사용하여 데이터와 메타데이터 값의 시리얼라이징/디시리얼라이징을 위한 인코더와 디코더를 제공한다. spring-core 로부터 String, byte[], ByteBuffer 에 대한 코덱만이 기본으로 등록된다. spring-web 추가하여 다음과 같이 다른 코덱을 더 추가할 수 있다:

Java
RSocketStrategies strategies = RSocketStrategies.builder()
    .encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
    .decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
    .build();

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .rsocketStrategies(strategies)
    .connectTcp("localhost", 7000);
Kotlin
import org.springframework.messaging.rsocket.connectTcpAndAwait

val strategies = RSocketStrategies.builder()
        .encoders { it.add(Jackson2CborEncoder()) }
        .decoders { it.add(Jackson2CborDecoder()) }
        .build()

val requester = RSocketRequester.builder()
        .rsocketStrategies(strategies)
        .connectTcpAndAwait("localhost", 7000)


RSocketStrategies는 재사용을 고려하여 만들어졌다. 몇몇 시나리오에서는, 예를 들어 클라이언트와 서버가 같은 어플리케이션에서 동작할 경우, RSocketStrategies를 스프링 설정에 선언하는 쪽이 좋을 수 있다.

Client Responders



RSocketRequester.Builder를 사용하여 서버로부터의 요청에 대한 응답자의 동작을 설정할 수 있다.

서버에서 사용되는 것과 같은 인프라스트럭처에 기반하지만 클라이언트측의 응답을 위해 어노테이티드 핸들러를 사용할 수 있다. 다음과 같이 프로그래밍 방식으로 등록한다.

Java
RSocketStrategies strategies = RSocketStrategies.builder()
    .routeMatcher(new PathPatternRouteMatcher())  (1)
    .build();

ClientHandler handler = new ClientHandler(); (2)

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .rsocketFactory(RSocketMessageHandler.clientResponder(strategies, handler)) (3)
    .connectTcp("localhost", 7000);
Kotlin
import org.springframework.messaging.rsocket.connectTcpAndAwait

val strategies = RSocketStrategies.builder()
        .routeMatcher(PathPatternRouteMatcher())  (1)
        .build()

val handler = ClientHandler() (2)

val requester = RSocketRequester.builder()
        .rsocketFactory(RSocketMessageHandler.clientResponder(strategies, handler)) (3)
        .connectTcpAndAwait("localhost", 7000)

(1) spring-web 이 존재한다면 효과적인 라우팅 매칭을 위해 PathPatternRouteMatcher를 사용한다. (2) @MessageMapping 또는 @ConnectMapping 메서드를 포함하는 응답자를 생성한다. (3) RSocketMessageHandler의 스태틱 팩토리 메서드를 사용하여 하나 이상의 응답자를 등록한다.

위 코드는 프로그래밍 방식 클라이언트 응답자 등록을 보여주기 위해 간단하게 작성된 예제일 뿐이다. 다른 시나리오에 대해서는, 클라이언트 응답자가 스프링 설정에 존재할 경우 RSocketMessageHandler를 스프링 빈으로 선언하고 다음과 같이 적용할 수 있다:

Java
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .rsocketFactory(factory -> factory.acceptor(handler.responder()))
    .connectTcp("localhost", 7000);
Kotlin
import org.springframework.beans.factory.getBean
import org.springframework.messaging.rsocket.connectTcpAndAwait

val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()

val requester = RSocketRequester.builder()
        .rsocketFactory { it.acceptor(handler.responder()) }
        .connectTcpAndAwait("localhost", 7000)


위 예제에 대해서, 클라이언트 응답자를 감지하기 위한 다른 전략으로의 스위칭을 위해 RSocketMessageHandler의 setHandlerPredicate를 사용해야 할 수도 있다. 예를 들어, @RSocketCllientResponder와 같은 커스텀 어노테이션과 디폴트 @Controller를 비교할 수 있다. 클라이언트와 서버, 또는 다수의 클라이언트가 같은 어플리케이션에서 동작하는 시나리오에서는 이런 스위칭이 필요하다.

어노테이티드 응답자에서 프로그래밍 모델에 대한 정보를 더 찾아볼 수 있다.

고급(Advanced)



RSocketRequesterBulder는 기반 RSocket 자바의 ClientRSocketFactory를 노출하기 위한 콜백을 제공한다. ClientRSocketFactor는 keepalive intervals, 세션 재개, 인터셉터, 그리고 기타 등등의, 더 나아간 설정 옵션을 위해 사용된다. 이런 수준의 옵션을 설정하려면 다음과 같이 할 수 있다:

Java
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .rsocketFactory(factory -> {
        // ...
    })
    .connectTcp("localhost", 7000);
Kotlin
import org.springframework.messaging.rsocket.connectTcpAndAwait

val requester = RSocketRequester.builder()
        .rsocketFactory {
            //...
        }.connectTcpAndAwait("localhost", 7000)


5.2.2. Server Requester

서버에서 클라이언트로의 요청을 생성하기란, 연결된 클라이언트에 대한 요청자를 서버로부터 획득함의 문제이다.

어노테이티드 응답자에서는 @ConnectMapping 과 @MessageMapping 메서드는 RSocketRequester 아규먼트를 지원한다. 이 아규먼트를 사용하여 커넥션에 대한 요청자에 접근한다. @ConnectMapping 메서드는 요청 시작 전에 반드시 핸들링되어야하는 SETUP 프레임의 기본적인 핸들러이다. 그러므로 아주 초기의 요청자는 반드시 핸들링과 분리되어야 한다. 다음은 그 예제이다:

Java
@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
    requester.route("status").data("5")
        .retrieveFlux(StatusReport.class)
        .subscribe(bar -> { (1)
            // ...
        });
    return ... (2)
}
Kotlin
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
    GlobalScope.launch {
        requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1)
            // ...
        }
    }
    /// ... (2)
}


(1) 요청을 비동기로, 핸들링과 독립적으로 시작한다. (2) 핸들링을 수행하고, 완료된 Mono를 반환한다.

5.2.3. Requests

한 번 클라이언트 또는 서버 요청자를 가지면, 다음과 같이 요청을 생성할 수 있다:

Java
ViewBox viewBox = ... ;

Flux<AirportLocation> locations = requester.route("locate.radars.within") (1)
        .data(viewBox) (2)
        .retrieveFlux(AirportLocation.class); (3)
Kotlin
val viewBox: ViewBox = ...

val locations = requester.route("locate.radars.within") (1)
        .data(viewBox) (2)
        .retrieveFlow<AirportLocation>() (3)


(1) 요청 메시지의 메타데이터에 포함하기 위한 라우팅을 지정한다. (2) 요청 메시지를 위한 데이터를 제공한다. (3) 예상되는 응답을 선언한다.

상호동작 유형은 인풋과 아웃풋의 카디널리티로부터 암시적으로 결정된다. 위 예제는 Request-Stream이다. 왜냐하면 하나의 값이 전송되고 하나의 스트림 값을 수신하기 때문이다. 대부분의 경우 인풋과 아웃풋 선택이 RSocket 상호동작 유형 및 응답자가 기대하는 인풋과 아웃풋 타입에 매칭되는 한 이를 생각할 필요는 없다. 유효하지 않은 결합의 유일한 예는 many-to-one이다.

data(Object) 메서드는 또한 어떠한 리액티브 스트림 Publisher 든 받아들인다. 이 Publisher는 Flux와 Mono를 포함하며, ReactiveAdapterRegistry 에 등록된 값의 프로듀서는 무엇이든 포함한다. Flux와 같이 같은 타입의 값을 생산하는 다수 값 Publisher 에 대해서는 오버로딩된 data 메서드 중 하나를 사용하여 타입 체크 및 모든 엘리먼트에 대한 Encoder 검색 방지를 고려하자.

data(Object producer, Class<?> elementClass);
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);


data(Object) 단계는 선택적이다. 데이터를 전송하지 않는 요청에서는 생략하자:

Java
Mono<AirportLocation> location = requester.route("find.radar.EWR"))
    .retrieveMono(AirportLocation.class);
Kotlin
import org.springframework.messaging.rsocket.retrieveAndAwait

val location = requester.route("find.radar.EWR")
    .retrieveAndAwait<AirportLocation>()


컴포짓 메타데이터(디폴트)를 사용하면서, 등록된 Encoder가 지원하는 값이라면 메타데이터 값을 추가할 수 있다. 다음은 그 예제이다:

Java
String securityToken = ... ;
ViewBox viewBox = ... ;
MimeType mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0");

Flux<AirportLocation> locations = requester.route("locate.radars.within")
        .metadata(securityToken, mimeType)
        .data(viewBox)
        .retrieveFlux(AirportLocation.class);
Kotlin
import org.springframework.messaging.rsocket.retrieveFlow

val requester: RSocketRequester = ...

val securityToken: String = ...
val viewBox: ViewBox = ...
val mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0")

val locations = requester.route("locate.radars.within")
        .metadata(securityToken, mimeType)
        .data(viewBox)
        .retrieveFlow<AirportLocation>()


Fire-and-Forget 에 대해서는 Mono<Void>를 반환하는 send() 메서드를 사용한다. 이 Mono는 메시지가 성공적으로 전송되었음을 알릴 뿐, 핸들링 대상이 되진 않는다.

5.3. 어노테이티드 응답자(Annotated Responders)

RSocket 응답자는 @MessageMapping 및 @ConnectMapping 메서드로서 구현될 수 있다. @MessageMapping 메서드는 각 요청을 개별적으로 핸들링하고, 반면 @ConnectMapping 메서드는 커넥션 레벨 이벤트(setup, 메타데이터 push)를 핸들링한다. 어노테이티드 응답자는 서버 및 클라이언트 측으로부터의 응답에 대해 대칭적으로 지원된다.

5.3.1. 서버 응답자(Server Responders)

서버 측에 어노테이티드 응답자를 사용하기 위해서는 RSockerMessageHandler를 스프링 설정에 추가하여 @Controller 빈과 @MessageMapping 및 @ConnectMapping 메서드를 함게 감지하도록 한다:

Java
@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.routeMatcher(new PathPatternRouteMatcher());
        return handler;
    }
}
Kotlin
@Configuration
class ServerConfig {

    @Bean
    fun rsocketMessageHandler() = RSocketMessageHandler().apply {
        routeMatcher = PathPatternRouteMatcher()
    }
}


다음으로 자바 RSocket API를 통해 RSocket 서버를 시작하고, 응답자를 위해 RSocketMessageHandler를 연결한다:

Java
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

CloseableChannel server =
    RSocketFactory.receive()
        .acceptor(handler.responder())
        .transport(TcpServerTransport.create("localhost", 7000))
        .start()
        .block();
Kotlin
import org.springframework.beans.factory.getBean

val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()

val server = RSocketFactory.receive()
        .acceptor(handler.responder())
        .transport(TcpServerTransport.create("localhost", 7000))
        .start().awaitFirst()


RSocketMessageHandler는 메타데이터 구성라우팅을 기본으로 지원한다. 다른 마임 타입으로 스위칭하거나 추가적인 메타데이터 마임 타입을 등록해야 한다면 핸들러의 MetadataExtractor을 세팅할 수 있다.

메타데이터와 데이터 포맷팅 지원을 위한 Encoder와 Decoder를 세팅할 필요가 있다. 그리고 코덱 구현체를 위해 spring-web 모듈이 필요할 수 있다.

디폴트로 SimpleRouteMatcher를 사용하여 AntPathMatcher를 통한 라우팅 매칭을 수행한다. 효과적인 라우팅 매칭을 위해 spring-web의 PathPatternRouteMatcher 사용을 추천한다. RSocket 라우팅은 계층적으로 동작할 수 있지만, URL 경로는 아니다. 두 라우팅 매칭은 기본적으로 "."를 구분자로 사용하도록 설정된다. 그리고 HTTP URL 과 마찬가지로 URL 디코딩은 없다.

RSocketMessageHandler는 RSocketStrategies를 통해 설정된다. RSocketStrategies는 같은 프로세스 안에서 클라이언트와 서버 사이의 설정을 공유할 때 유용할 수 있다:

Java
@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.setRSocketStrategies(rsocketStrategies());
        return handler;
    }

    @Bean
    public RSocketStrategies rsocketStrategies() {
        return RSocketStrategies.builder()
            .encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
            .decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
            .routeMatcher(new PathPatternRouteMatcher())
            .build();
    }
}
Kotlin
@Configuration
class ServerConfig {

    @Bean
    fun rsocketMessageHandler() = RSocketMessageHandler().apply {
        rSocketStrategies = rsocketStrategies()
    }

    @Bean
    fun rsocketStrategies() = RSocketStrategies.builder()
            .encoders { it.add(Jackson2CborEncoder()) }
            .decoders { it.add(Jackson2CborDecoder()) }
            .routeMatcher(PathPatternRouteMatcher())
            .build()
}


5.3.2. 클라이언트 응답자(Client Responders)

클라이은트 측 어노테이티드 응답자는 RSocketRequester.Builder 에서 설정되어야 한다. 자세한 내용은 클라이언트 응답자를 보라.

5.3.3. @MssageMapping

서버 또는 클라이언트 응답자가 한 번 설정되면, @MessageMapping 메서드는 다음과 같이 사용된다:

Java
@Controller
public class RadarsController {

    @MessageMapping("locate.radars.within")
    public Flux<AirportLocation> radars(MapRequest request) {
        // ...
    }
}
Kotlin
@Controller
class RadarsController {

    @MessageMapping("locate.radars.within")
    fun radars(request: MapRequest): Flow<AirportLocation> {
        // ...
    }
}


위의 @MessageMapping 메서드는 "locate.radars.within" 라우팅을 가진 Request-Stream 상호동작에 응답한다. 이 메서드는 다음 메서드 아규먼트를 선택적으로 사용하는 유연한 메서드 시그니처를 지원한다:

메서드 아규먼트 설명
@Payload 요청의 페이로드. Mono 또는 Flux와 같이 비동기 타입의 구체적인 값이 될 수 있다. 유의사항: 이 어노테이션 사용은 선택적이다. 메서드 아규먼트가 단순 타입이 아니면서, 다른 지원되는 아규먼트도 아닌 경우에는 예상된 페이로드로 상정한다.
RSocketRequester 원격 종료 요청을 위한 요청자.
@DestinationVariable 매핑 패턴 안의 변수에 기반한 라우팅으로부터 추출된 값. 예: @MessageMapping("find.radar.{id}")
@Header MetadataExtractor에 설명된 추출물을 위해 등록된 메타데이터 값.
@Headers Map<String, Object> MetadataExtractor에 설명된 추출물을 위해 등록된 모든 메타데이터 값.


반환값은 응답 페이로드로 시리얼라이징될 하나 이상의 객체로 예상된다. Mono 또는 Flux와 같이 비동기 타입이 될 수 있으며, 구체적인 값 또는 void 또는 no-value 비동기 타입이 될 수 있다(Mono).

@MessageMapping 메서드가 지원하는 RSocket 상호동작 유형은 인풋(예: @Payload 아규먼트)과 아웃풋의 카디널리티로부터 결정된다. 여기서 카디널리티란 다음을 의미한다:

카디널리티 설명
1 명시적인 값, 혹은 Mono<T> 와 같은 비동기 단일 값.
Many Flux<T> 와 같은 다수 값 비동기 타입.
0 인풋에 대해서는 이 메서드가 @Payload 아규먼트를 가지지 않았음을 의미한다. 아웃풋에 대해서는 void 또는 no-value 비동기 타입을 의마한다(Mono<Void>).


아래 테이블은 모든 인풋과 아웃풋의 결합 그리고 그에 대응하는 상호동작 유형을 보여준다:

인풋 카디널리티 아웃풋 카디널리티 상호동작 유형
0, 1 0 Fire-and-Forget, Request-Response
0, 1 1 Request-Response
0, 1 Many Request-Stream
Many 0, 1, Many Request-Channel


5.3.4. @ConnectMapping

@ConnectMapping은 RSocket 커넥션 시작 시점의 SETUP 프레임을 핸들링한다. 그리고 METADATA_PUSH 프레임을 통해 이어지는 메타데이터 푸시 알림을 핸들링한다. 예: io.rsocket.RSocket의 metadataPush(Payload).

@ConnetMapping 메서드는 @MessageMapping와 동일한 아규먼트를 지원하지만, SETUP, METADATA_PUSH 프레임의 메타데이터 및 데이터에 기반한다.@ConnetMapping은 핸들링 대상 커넥션의 범위를 좁히는 패턴을 가질 수 있다. 이 커넥션은 메타데이터의 라우팅을 가질수 있고, 혹은 패턴이 없는 경우라면 모든 커넥션이 매칭된다.

@ConnetMapping 메서드는 데이터를 반환할 수 없고, 반드시 void 또는 Mono<Void>를 반환해야 한다. 만약 신규 커넥션 핸들링에서 에러를 반환하면 그 커넥션은 거부된다. RSocketRequester 에 커넥션을 위한 요청을 보내기 위해 처리를 보류해서는 안된다. 서버 요청자에서 더 자세한 내용을 찾아볼 수 있다.

5.4. MetadataExtractor

응답자는 반드시 메타데이터를 해석해야 한다. 컴포짓 메타데이터는 각각 자신의 마임 타입으로 독립적으로 포맷팅된 메타데이터 값(예: 라우팅, 보안, 추적) 을 허용한다. 마임 타입 지원 및 추출된 값에 접근하기 위한 어플리케이션 설정 방법이 필요하다.

MetadataExtractor는 시리얼라이징된 메타데이터에 접근하고 디코딩된 이름-값 쌍을 반환하는 계약이다. 이 이름-값 쌍은 헤더처럼 이름을 사용해 접근할 수 있다. 예를 들어 핸들러 메서드에 적용된 @Header와 같다.

DefaultMetadataExtractor는 메타데이터 디코딩을 위한 Decoder 인스턴스를 가질 수 있다. DefaultMetadataExtractor는 특별한 "message/x.rsocket.routing.v0"을 내장형으로 지원한다. String으로 디코딩하고 "route" 을 키로 저장한다. 다른 마임 타입에 대해서는 Decoder를 제공하고 다음과 같이 마임 타입을 등록해야 한다:

Java
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");
Kotlin
import org.springframework.messaging.rsocket.metadataToExtract

val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Foo>(fooMimeType, "foo")


컴포짓 메타데이터는 독립된 메타데이터 값을 잘 결합한다. 하지만 요청자가 컴포짓 메타데이터를 지원하지 않을 수 있고, 혹은 이를 사용하지 않을 수도 있다. 이런 경우에 대해 DefaultMetadataExractor는 디코딩된 값을 아웃풋 맵에 매핑하지 위한 커스텀 로직이 필요할 수 있다. 아래는 메타데이터로 JSON을 사용하는 예제이다:

Java
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(
    MimeType.valueOf("application/vnd.myapp.metadata+json"),
    new ParameterizedTypeReference<Map<String,String>>() {},
    (jsonMap, outputMap) -> {
        outputMap.putAll(jsonMap);
    });
Kotlin
import org.springframework.messaging.rsocket.metadataToExtract

val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Map<String, String>>(MimeType.valueOf("application/vnd.myapp.metadata+json")) { jsonMap, outputMap ->
    outputMap.putAll(jsonMap)
}


RSocketStrategies를 사용하여 MetadataExtractor를 설정할 때는 RSocketStrategies.Builder에게 설정된 디코더와 함께 추출자를 생성하도록 할 수 있다. 그리고 다음과 같이 콜백을 사용하여 등록 작업은 커스터마이징한다:

Java
RSocketStrategies strategies = RSocketStrategies.builder()
    .metadataExtractorRegistry(registry -> {
        registry.metadataToExtract(fooMimeType, Foo.class, "foo");
        // ...
    })
    .build();
Kotlin
import org.springframework.messaging.rsocket.metadataToExtract

val strategies = RSocketStrategies.builder()
        .metadataExtractorRegistry { registry: MetadataExtractorRegistry ->
            registry.metadataToExtract<Foo>(fooMimeType, "foo")
            // ...
        }
        .build()


6. 리액티브 라이브러리(Reactive Libraries)

spring-webflux는 reactor-core 에 의존하여 내부적으로 이를 사용해 비동기 로직을 구성하고 리액티브 스트림 지원을 제공한다. 일반적으로, 웹플럭스 API는 Flux 또는 Mono를 반환한다(내부적으로 이 둘이 사용되기에). 그리고 어떠한 리액티브 스트림 Publisher 구현체든 인풋으로 받아들인다. Flux의 사용이 Mono 에 비해 더 중요하다. Flux는 카디널리티 표현을 돕기 때문이다. 예를 들어, 예상되는 비동기 값이 단일 값일 수도, 다수 값일 수도 있다. 그리고 이는 어떤 결정을 내리는 데에 기본이 될 수 있다(예로, HTTP 메시지 인코딩 또는 디코딩 시).

어노테이티드 컨트롤러에 대해, 웹플럭스는 어플리케이션에 선택된 리액티브 라이브러리로 투명하게 연결된다. 여기에는 ReactiveAdapterRegistry의 도움을 받는다. 이 레지스트리는 리액티브 라이브러리와 다른 비동기 타입을 위한 장착형 지원을 제공한다. 그리고 RxJava 및 CompletableFuture를 내장형으로 지원하면서, 다른 것들도 추가로 등록할 수 있다.

함수형 API 에 대해(함수형 엔드포인트, 웹클라이언트 그리고 기타 등등), 웹플럭스 API를 위한 일반 규칙을 적용한다 - 반환값으로는 Flux 및 Mono를, 인풋으로는 리액티브 스트림 Publisher를 취한다. Publisher가 주어지면, Publisher는 밝혀지지 않은 시맨틱스(0..N)를 가진 스트림으로 취급된다. 만약 시맨틱스을 알고 있다면 그냥 Publisher를 전달하는 대신, 이를 Flux 또는 Mono.from(Publisher) 로 래핑할 수 있다.

예를 들어서, Mono가 아닌 Publisher가 주어지면, Jackson JSON message writer는 다수 값을 예상한다. 만약 미디어 타입이 무한정한 스트림을 나타낸다면(예: application/json+stream), 값들은 개별적으로 작성되고 플러싱된다. 그렇지 않으면 값들은 리스트 안에 버퍼링되고 JSON 배열로 렌더링된다.

=== 원문 정보: Version 5.2.2.RELEASE 최종 수정 2019-12-03 08:48:18 UTC

람다와 익명클래스

 

람다와 익명클래스의 this 바인딩은 다르다. 람다의 this는 자신을 둘러싼 인스턴스를 참조하고, 익명클래스는 자기 자신의 인스턴스를 참조한다.

 

익명클래스의 생성과 동작 방식은 컴파일타임에 결정되지만, 람다는 런타임에 실제 호출 시 결정된다(invokedynamic).

 

컴파일타임에서, 익명클래스는 '클래스명$1' 의 형태로 개별 클래스파일이 생성되지만, 람다의 클래스파일이 따로 생성되지 않는다. 이는 람다의 경우 JVM이 클래스파일을 로드하기 위한 동작이 필요치 않음을 의미한다(-Djdk.internal.lambda.dumpProxyClasses=<dir> 을 통해 람다로 생성되는 동적 클래스를 파일로 저장할 수는 있다).

 

런타임에서, 익명클래스는 실행 시 마다 익명클래스의 인스턴스를 생성하는 과정을 거친다. 람다는 이 과정을 거치치 않는다. 람다는 JVM의 invokedynamic 연산을 통해 동적으로 생성된다(후에 private static 메서드로 풀어지고, 실행된다).

 

 

람다와 클로저

 

모든 클로저는 람다이지만, 모든 람다가 클로저는 아니다.

 

람다가 자신의 블록 밖의 변수(외부 변수)에 접근하면(capture), 이는 클로저이다(클로저는 자바 1 에서부터 익명클래스의 형태로 이미 존재해왔다).

 

'Java > Core' 카테고리의 다른 글

Java 13 간략히 정리  (0) 2020.07.16
Java 12 간략히 정리  (0) 2020.07.15
Java 11 간략히 정리  (0) 2020.07.15
Java 10 간략히 정리  (0) 2020.07.15
IO, NIO, NIO2  (0) 2020.02.26


스프링부트 테스트를 실행할 때 다음과 같은 에러를 만날 수 있다.


java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test


이 에러는 @SpringBootTest 클래스가 실행되면서 필요한 스프링부트 설정 클래스를 찾지 못할 경우 발생한다.


흔히, 테스트 클래스를 작성하면서 테스트 클래스 패키지명을 메인 클래스 패키지와 차이가 생기면서 이 에러를 만나는데, 해결 방법은 간단하다. 


프로젝트 레이아웃이 아래와 같다고 하자:


src

+--main

+--com

+--demo

+--demo

+--DemoApplication.java (@SpringBootApplication)

+--test

+--com

+--demo

+--demo

+--DemoApplicationTest.java (@SpringBootTest)

+--demo

+--others

+--MyCustomTest.java (@SpringBootTest)



DemoApplicationTest 는 정상 실행 될 것이다. MyCusTomTest 는 위의 에러를 만날 것이다. MyCustomTest 가 찾을 수 있는 스프링부트 설정 클래스가 없기 때문이다.


스프링부트 실행 클래스는 자신의 패키지에서부터 스프링부트 설정 클래스를 찾기 시작하여, 찾을 때 까지 상위 패키지로 계속 찾아나간다.


위의 경우, MyCustomTest 클래스가 실행되면서 스프링부트 설정 클래스를 찾는 순서는 다음과 같다:


1. com.demo.others

2. com.demo

3. com


그런데 위 프로젝트에서 유일한 스프링부트 설정 클래스인 DemoApplication 은 MyCustomTest 의 상위 패키지가 아닌, 같은 레벨의 다른 이름을 가진 패키지에 존재하고, 이 경우 MyCustomTest 는 설정 클래스를 찾을 수 없다.


해결 방법은 다음과 같다.


1. 경로 똑같이 맞추기1 (MyCustomTest 클래스를 com.demo.demo 로 이동)

2. 경로 똑같이 맞추기2 (com.demo.others 에도 스프링부트 설정 패키지 생성)

3. 상위 경로에 설정 클래스 하나 두기 (com 또는 com.demo 에 에러 방지용 디폴트 설정 클래스 생성)




어쨌든 테스트 실행 클래스가 찾을 수 있는 설정 파일이 있기만 하면 된다.







아주 좋은 글이다.



Spring Interceptor(혹은 Servlet Filter)에서 POST 방식으로 전달된 JSON 데이터 처리하기

https://meetup.toast.com/posts/44

'Java > Spring' 카테고리의 다른 글

Spring OSIV 작동 방식 (OpenEntityManagerInViewFilter)  (0) 2022.05.19
스프링의 트랜잭션 관리  (0) 2017.06.30

이클립스 메이븐 플러그인 m2e 는 의존성 설정된 프로젝트가 로컬에 존재하는 경우 로컬 프로젝트를 참조하고, 존재하지 않는 경우 원격 저장소(넥서스) 에서 jar 를 받아와 참조한다.


그런데 로컬에 존재하는 프로젝트의 jar 파일이 톰캣의 server path lib 경로로 배포(복사)되지 않아서 서버 동작 시 ClassNotFoundException 이 던져지는 경우가 있다.


이 경우 다음과 같은 확인 절차를 따른다.


1. 먼저 web 프로젝트의 pom.xml 재확인

-> 의존성 설정이 정확히 되어있는지 다시 확인한다.



2. 의존 프로젝트의 의존성 트리 확인

-> web 프로젝트 -> 의존 프로젝트가 간접 의존 관계인 경우 그 연결고리가 제대로 맺어져 있는지 확인한다.



3. web 프로젝트의 Java Build path 확인

-> 프로젝트 properties 의 Java Build Path 에서 Maven Dependencies 에 의존 프로젝트가 제대로 떠 있는지 확인한다. 로컬 프로젝트의 경우 로컬 프로젝트 아이콘으로 존재해야 한다.



4. 의존 프로젝트의 Deployment Assembly 확인

-> 여기가 관건이다. 위 1,2,3 은 보통 제대로 설정이 되어 있다. 그럼에도 불구하고 이 문제가 발생하는 경우, 의존 프로젝트의 properties 의 Deployment Assembly 를 본다. 여기의 설정이 다른 정상적인 프로젝트의 설정과 일치하는지 확인한다. 프로젝트 설정이 틀어진 경우 이 메뉴를 클릭하면

...contains invalid values 라는 오류 안내를 내뱉고 아예 메뉴가 열리지 않을 수 있는데, 이 경우 다른 정상적인 프로젝트의 다음 경로의 파일들을 가져와 덮어 쓴다:


  • .settings
  • .classpath
  • .project


그리고 Deployment Assembly 메뉴가 정상적으로 열리는지 확인한다.


확인이 되었으면 프로젝트를 clean-install 후 web 프로젝트를 clean-install, 톰캣 clean 을 한 번 해주자.


com.ibm.db2.jcc.am.SqlSyntaxErrorException:

[jcc][10145][10844][3.62.56] Invalid parameter 1: Parameter

index is out of range. ERRORCODE=-4461, SQLSTATE=42815

com.ibm.db2.jcc.am.SqlSyntaxErrorException:

[jcc][10145][10844][3.62.123] 유효하지 않은 매개변수 1: 매개변수 인덱스가 범위를 벗어납니다.

ERRORCODE=-4461, SQLSTATE=42815



이 오류는 기본적으로 db2 jdbc type 4 드라이버가 named parameter setting 처리 도중 파라미터의 타입과 실제 컬럼의 데이터 타입이 맞지 않는 데서 발생한다. 그런데 2대 이상의 호스트에서 컨테이너를 띄울 때, 간혹 정확히 같은 request 내용을 처리 시 어떤 서버에서는 오류가 발생하고, 어떤 서버에서는 오류가 발생하지 않는 현상이 있다.


이는 db2 jdbc 드라이버 버전의 차이에서 발생하는 현상으로, 제우스의 datasource path 에서 db2 드라이버 버전을 맞춰 주어 해결할 수 있다.


아래는 내 호스트의 jdbc 드라이버 jar 위치이다. 제우스의 webinf-first 설정을 false 로 한다면 이 곳의 드라이버 클래스를 로드 할태니 참고.


.../jeus/lib/datasource/



아래는 ibm 의 관련 내용. JCC driver version 을 3.64.141 이상으로 올릴 것을 권하고 있다.


http://www-01.ibm.com/support/docview.wss?uid=swg1IC86612

MyClass.class.getProtectionDomain().getCodeSource().getLocation().getPath();


현재 JVM에 로드된 클래스의 물리적 파일 위치를 반환.

스프링의 트랜잭션 관리


종합적인 트랜잭션 관리는 스프링 프레임워크를 사용하는 가장 중요한 이유 중 하나이다. 스프링 프레임워크는 다음과 같은 장점을 지닌 트랜잭션 관리를 위한 일관된 추상화를 제공한다.


  • 자바 트랜잭션 API(JTA), JDBC, 하이버네이트, 자바 영속성 API(JPA), 자바 데이터 객체(JDO) 와 같은 서로 다른 트랜잭션을 아우르는 일관된 프로그래밍 모델
  • 선언적 트랜잭션 관리
  • JTA 와 같은 복잡한 트랜잭션 API 보다 간단한 프로그래밍 방식의 트랜잭션 관리 API
  • 스프링 데이터 접근 추상화를 통한 훌륭한 통합

다음 섹션은 스프링 프레임워크릐 부가 가치와 기술에 대하여 설명한다. (또한 가장 실용적인 예제와 어플리케이션 서버 통합, 그리고 공통된 문제들에 대한 솔루션에 대한 논의를 포함한다.)


  • 스프링 프레임워크의 트랜잭션 관리 모델의 장점 에서는 왜 당신이 EJB 컨테이너 관리 트랜잭션(CMT) 이나 하이버네이트와 같은 상용 로컬 트랜잭션 관리 대신 스프링 프레임워크의 트랜잭션 관리를 사용해야 하는지 설명한다.
  • 스프링 프레임워크 추상화의 이해 에서는 핵심 클래스를 선보이며 다양한 자원으로부터 DataSource 인스턴스를 획득하고 설정하는 방법에 대해 설명한다.
  • 트랜잭션을 통한 동기화 에서는 어플리케이션 코드가 어떻게 자원의 생성, 재사용, 제거를 안전하게 수행하는지 설명한다.
  • 프로그래밍 방식 트랜잭션 관리 에서는 프로그래밍 방식(프로그램 코드를 통한) 트랜잭션 관리에 대해 알아본다.
  • 트랜잭션 바운드 이벤트 에서는 당신이 트랜잭션을 이용해 어떻게 어플리케이션 이벤트를 사용할 수 있는지 설명한다.


스프링 프레임워크의 트랜잭션 관리 모델의 장점

전통적으로 자바 EE 개발자들은 트랜잭션 관리를 위한 두 가지 선택지를 가지고 있었다. 글로벌 트랜잭션과 로컬 트랜잭션이 그것인데, 둘 모두 큰 한계를 가지고 있다. 글로벌와 로컬 트랜잭션 관리에 대해서는 다음 두 섹션에서 논하며 스프링 트랜잭션 관리가 이 두 트랜잭션 관리의 한계점을 다루는 방식을 설명한다.


글로벌 트랜잭션

글로벌 트랜잭션은 다수의 트랜잭션 관련 자원들을 다룰 수 있도록 해준다. 전형적인 트랜잭션 관련 자원은 관계형 데이터베이스와 메시지 큐와 같은 것들이다. 어플케이션 서버는 JTA 라는 다루기 힘든 API 를 통해 글로벌 트랜잭션을 관리한다. 더 나아가서 JTA 의 UserTransaction 은 일반적으로 JNDI 를 통해 얻어져야 하는데, 이는 JTA 를 사용하기 위해서는 JNDI 도 필요하다는 것을 의미한다. JTA 가 보통 어플리케이션 서버 환경에서만 유효하듯이, 확실히 글로벌 트랜잭션을 사용함은 어플리케이션의 잠재적인 재사용성을 제한하게 된다.

이전에는 EJB CMT(컨테이너 관리 트랜잭션) 을 사용하는 것이 글로벌 트랜잭션을 관리하기 위해 선호되는 방법이었다. CMT 는 하나의 선언적 트랜잭션 관리 형식이다. EJB CMT 는 트랜젹선 관련 JNDI 룩업을 사용할 필요가 없었으나, EJB 자체가 JNDI 를 필요로 했다. 자바 코드를 통한 트랜잭션 관리의 필요성을 거의 없앴긴 했지만, 완전히 없애지는 못했다. 중요한 단점은 CMT 는 JTA 와 어플리케이션 서버 환경에 묶여 있었다는 점이고, 또한 비즈니스 로직을 EJB 에 구현하거나, 적어도 EJB 의 외관 뒤에 구현하는 경우에만 사용할 수 있었다. EJB 의 선언적 트랜잭션 관리를 위한 강력한 대안에도 불구하고 이러한 단점들은 너무나 큰 것이어서 EJB 는 매력적인 제안이 되지 못했다.


로컬 트랜잭션

로컬 트랜잭션은 자원 종속적이다. JDBC 커넥션과 관련된 트랜잭션이 그렇다. 로컬 트랜잭션은 사용하지 쉬울 수 있지만, 큰 약점을 가지고 있다. 여러가지 트랜잭션 자원으로 작업할 수 없다. 가령, JDBC 커넥션을 사용하는 트랜잭션 관리 코드는 JTA 글로벌 트랜잭션에서 동작하지 않는다. 왜냐하면 어플리케이션 서버는 트랜잭션 관리와 관련이 없기 때문에, 여러 자원에 대한 정확성을 보장할 수 없다. (대부분의 어플리케이션은 단일 트랜잭션 자원을 사용한다는 점은 알아둬야 한다.) 또다른 단점은 로컬 트랜잭션은 프로그래밍 모델 안으로 급속히 퍼지게 된다.


스프링 프레임워크의 일관된 프로그래밍 모델

스프링은 글로벌와 로컬 트랜잭션의 약점을 해결한다. 스프링은 어플리케이션 개발자로 하여금 어떤 환경에서든 일관된 프로그래밍 모델을 사용할 수 있도록 한다. 한 번 작성된 코드는 다른 환경의 다른 트랜잭션 관리 전략에서도 사용될 수 있다. 스프링 프레임워크는 선언적 트랜잭션 관리와 프로그래밍 방식 트랜잭션 관리를 제공한다. 대부분 선언적 트랜잭션 관리가 선호되는데, 이 방식은 거의 모든 상황에 적합하다.

프로그래밍 방식 트랜잭션 관리를 사용하면 개발자는 어떠한 트랜잭션 기반 환경에서도 동작하는 스프링 프레임워크 트랜잭션 추상화로 작업할 수 있다. 선언적 트랜잭션 관리를 사용하면 개발자는 트랜잭션 관리와 관련된 코드를 아주 조금, 혹은 전혀 작성하지 않아도 되기 때문에, 코드는 스프링 프레임워크 트랜잭션 API 혹은 그 어떤 트랜잭션 API 에도 의존성을 갖지 않는다.

트랜잭션 관리를 위한 어플리케이션 서버가 필요한가?

스프링 프레임워크의 트랜잭션 관리는 엔터프라이즈 자바 어플리케이션이 어플리케이션 서버를 필요로 할 때와 같은 전통적인 규칙을 변경한다. 

특히 EJB 를 통한 선언적 트랜잭션을 위한 어플리케이션 서버는 필요하지 않다. 사실 당신의 어플리케이션 서버가 강력한 JTA 기능을 가지고 있다고 해도, 스프링 프레임워크의 선언적 트랜잭션은 EJB CMT 보다 더 강력하고 더 생산성이 뛰어난 프로그래밍 모델을 제공한다.

보통 어플리케이션 서버의 JTA 기능이 요구되는 경우는 어플리케이션이 여러가지 자원을 다뤄야 할 때 뿐인데, 많은 어플리케이션이 이런 기능을 필요로 하진 않는다. 많은 수의 고급 어플리케이션은 대신 높은 확장성을 지닌 단일한 데이터베이스를 사용한다. (오라클 RAC 와 같은) Atomikos Transactions, JOTM 과 같은 독립형 트랜잭션 관리자는 다른 선택 사항이다. 물론 자바 메시지 서비스(JMS), 자바 EE 컨테이너 아키텍처(JCA) 와 같은 어플리케이션 서버의 다른 기능을 필요로 할 수는 있다.

스프링 프레임워크는 어플리케이션을 언제 완전한 어플리케이션 서버로 확장할지 선택할 수 있게 해준다. JDBC 커넥션과 같은 로컬 트랜잭션으로 코드를 작성하기 위한 선택이 EJB CMT 나 JTA 뿐이던 시절은 지났다. 그리고 그런 코드를 글로벌, 컨테이너 관리 트랜잭션 안에서 실행하기 위해서는 막대한 재작업이 요구된다. 스프링 프레임워크를 사용하면 코드가 아닌 설정 파일 안의 빈 정의를 조금 바꿀 뿐이다.



스프링 프레임워크 트랜잭션 추상화의 이해


스프링 트랜잭션 추상화의 핵심은 트랜잭션 전략의 개념이다. 트랜잭션 전략은 org.springframework.transaction.PlatformTransactionManager 인터페이스에 의해 정의된다.


public interface PlatformTransactionManager {


    TransactionStatus getTransaction(

            TransactionDefinition definition) throws TransactionException;


    void commit(TransactionStatus status) throws TransactionException;


    void rollback(TransactionStatus status) throws TransactionException;

}


이 인터페이스는 어플리케이션 코드에 프로그래밍 방식으로도 사용 가능하긴 하지만, 기본적으로 서비스 제공 인터페이스(SPI)이다. PlatformTransactionManager 는 하나의 인터페이스이기 때문에 필요에 따라 손쉽게 모의화 또는 스텁 될 수 있다. 이 인터페이스는 JNDI 와 같은 룩업 전략에 구애받지 않는다. PlatformTransactionManager 구현체는 스프링 프레임워크 IoC 컨테이너 안의 다른 객체들(또는 빈)과 같은 방식으로 정의된다. 이런 장점은 스프링 프레임워크 트랜잭션 추상화를 가치있게 만들고, JTA 를 사용할 때조차도 이 장점은 유효하다. 트랜잭션 관련 코드의 테스트는 JTA 를 직접 사용할 때보다 훨씬 쉬워진다.


다시 스프링의 철학을 따라서, PlatformTransactionManager 의 모든 메서드에서 발생할 수 있는 예외인 TransactionException 은 unchecked(런타임 예외)이다. 트랜잭션 인프라의 에러는 거의 대부분 치명적이다. 어플리케이션 코드가 실제로 트랜잭션 실패를 복구할 수 있는 경우에는 TransactionException 을 캐치할 것인지 선택할 수 있다. 여기서 중요한 점은 개발자에게 트랜잭션 예외 캐치를 강제하지 않고 선택권을 준다는 것이다.


getTransaction(..) 메서드는 TransactionDefinition 파라미터에 따른 TransactionStatue 객체를 반환한다. 반환된 TransactionStatus 객체는 새로운 트랜잭션을 나타내거나, 혹은 현재 호출 스택에 매칭되는 기존 트랜잭션을 나타낼 수 있다. 후자의 경우는 자바 EE 트랜잭션 컨텍스트와 마찬가지로 TransactionStatus 가 실행 쓰레드와 연관되어 있음을 의미한다.


TransactionDefinition 인터페이스는 다음 사항들을 명시한다.


  • 격리: 이 트랜잭션이 다른 트랜잭션으로부터 격리되는 정도. 예로, 이 트랜잭션이 다른 트랜잭션의 커밋되지 않은 쓰기 작업을 볼 수 있는가?
  • 전파: 보통 한 트랜잭션 범위 안에서 실행된 코드는 그 트랜잭션 안에서 동작한다. 그러나 이미 트랜잭션이 존재할 때 트랜잭션 관련 메서드가 실행될 경우 트랜잭션을 어떻게 묶을 것인지 선택할 수 있다. 예로, 기존 트랜잭션 안에서 실행할 것인가, 혹은 생성된 새로운 트랜잭션으로 실행하여 기존 트랜잭션과 분리할 것인가. 스프링은 EJB CMT 와 유사한 모든 트랜잭션 전파 옵션을 제공한다. 스프링의 트랜잭션 전파의 의미에 대해서는 트랜잭션 전파 에서 읽어볼 수 있다.
  • 타임아웃: 트랜잭션 인프라에 의하여, 이 트랜잭션 수행이 자동 타임아웃-롤백 될 때까지 얼마만큼의 시간이 주어지는가.
  • 읽기 전용 상태: 읽기 전용 트랜잭션은 트랜잭션이 데이터를 읽기는 하지만 수정할 수는 없도록 할 때 사용된다. 읽기 전용 트랜잭션은 하이너베이트를 사용할 때와 같은 몇몇 경우에 유용할 최적화가 될 수 있다.

이 설정들은 표준적인 트랜잭션 개념을 반영한다. 필요하다면 트랜잭션 격리 수준과 다른 핵심적인 트랜잭션 개념을 논한 자료를 참조하라. 스프링 프레임워크나 다른 트랜잭션 관리 솔루션을 사용하기 위해서는 이 개념들을 이해해야 한다.

TransactionStatus 인터페이스는 트랜잭션 코드가 트랜잭션 실행과 쿼리 트랜잭션 상태를 제어하기 위한 쉬운 방법을 제공한다. 그 개념은 모든 트랜잭션 API 에게 공통된 사항인 것처럼 친숙해야 한다.

public interface TransactionStatus extends SavepointManager {

    boolean isNewTransaction();

    boolean hasSavepoint();

    void setRollbackOnly();

    boolean isRollbackOnly();

    void flush();

    boolean isCompleted();

}

스프링의 선언적 트랜잭션 관리와 프로그래밍 방식 트랜잭션 관리 중 어떤 것을 선택하느냐에 상관없이 반드시 알맞는 PlatformTransactionManager 구현체가 정의되어야 한다. 일반적으로 의존성 주입을 통해 구현체를 정의한다.

PlatformTransactionManager 구현체에는 JDBC, JTA, 하이버네이트, 기타 등등의 트랜잭션이 작동할 환경에 대한 지식이 요구된다. 다음 예제는 로컬 PlatformTransactionManager 구현체를 정의하는 방법을 보여준다. (일반적인 JDBC 가 사용되었다.)

JDBC DataSource 를 정의한다.

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>

다음으로 이와 관련된 PlatformTransactionManager 빈 정의는 정의된 DataSource 으로 참조를 갖는다. 

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>


JTA 를 사용하는 자바 EE 컨테이너라면 JNDI 를 통해 얻어지는 DataSource 와 스프링의 JtaTransactionManager 를 함께 사용한다. JTA 와 JNDI 룩업 버전은 다음과 같다.


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:jee="http://www.springframework.org/schema/jee"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/jee

        http://www.springframework.org/schema/jee/spring-jee.xsd">


    <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>


    <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />


    <!-- other <bean/> definitions here -->


</beans>


JtaTransactionManager 는 DataSource 나 다른 어떤 자원에 대해서도 알 필요가 없다. 컨테이너의 글로벌 트랜잭션 관리를 기반으로 하기 때문이다.


다음 예제에서 보이듯, 하이버네이트 로컬 트랜잭션 또한 쉽게 사용할 수 있다. 이 경우 하이버네이트 LocalSessionFactoryBean 을 정의해야 한다. LocalSessionFactoryBean 은 어플리케이션 코드가 하이버네이트 Session 인스턴스를 얻기 위해 사용된다. 


DataSource 빈 정의는 이전의 로컬 JDBC 예제와 유사하다. 따라서 다음 예제에선 생략한다.


non-JTA 트랜잭션 관리자가 사용하는 DataSource 가 자바 EE 컨테이너가 관리하는 JNDI 룩업에 의해 얻어진다면, 이 DataSource 는 트랜잭션과 관련이 없도록 해야한다. 트랜잭션을 관리하는 것은 자바 EE 컨테이너가 아닌 스프링 프레임워크이다.


여기서의 txManager 빈은 HibernatetransactionManager 타입이다. DataSourceTransactionManager 가 DataSource 로의 참조를 필요로 하는 것과 같이, HibernateTransactionManager 도 SessionFactory 참조를 필요도 한다.


<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">

    <property name="dataSource" ref="dataSource"/>

    <property name="mappingResources">

        <list>

            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>

        </list>

    </property>

    <property name="hibernateProperties">

        <value>

            hibernate.dialect=${hibernate.dialect}

        </value>

    </property>

</bean>


<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">

    <property name="sessionFactory" ref="sessionFactory"/>

</bean>


하이버네이트와 자바 EE 컨테이너 관리 JTA 트랜잭션을 사용할 때는 간단히 JDBC JTA 예제에서와 같은 JtaTransactionManager 를 사용할 수 있다.


<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>


JTA 를 사용한다면 데이터 접근에 JDBC, 하이버네이트, JPA 또는 다른 어떤 기술을 사용하든 트랜잭션 매니저 정의는 같아야 한다. JTA 트랜잭션은 어떠한 트랜잭션 관련 자원이든 요청할 수 있는 글로벌 트랜잭션이기 때문이다.


어떤 경우든 어플리케이션 코드는 바뀌지 않는다. 트랜잭션 관리 방식을 변경하려면 그저 설정을 바꾸기만 하면 된다. 심지어 트랜잭션이 로컬에서 글로벌로 바뀌거나 혹은 그 반대일지라도 마찬가지이다.



트랜잭션을 통한 자원 동기화


이제 트랜잭션 관리자가 어떻게 생성되며 트랜잭션으로 동기화가 필요한 관련 자원으로 어떻게 연결되는지 알아본다. (예를 들어 DataSourceTransactionManager 는 JDBC DataSource 로, HibernateTransactionManager 는 하이버네이트 SessionFactory 로, 그리고 기타 등등) 이 섹션에서는 직접적으로든 간접적으로든 JDBC, 하이버네이트, 혹은 JDO 와 같은 영속성 API 를 사용하는 어플리케이션 코드가 어떻게 자원을 생성하고 재사용하고 제거하는지 설명한다. 그리고 관련 PlatformTransactionManager 를 통해 트랜잭션 동기화가 어떻게 (선택적으로)발생하는지에 대해 논한다.



고수준 동기화 처리 방법


선호된는 방법은 스프링의 영속성 통합 API 에 기반하는 가장 높은 수준의 템플릿을 사용하거나, 또는 트랜잭션 인지 팩토리 빈이나 네이티브 자원 팩토리 관리용 프록시를 통한 네이티브 ORM API 를 사용하는 것이다. 트랙잭션 인지 솔루션은 내부적으로 자원의 생성, 재사용, 제거, 선택적 트랜잭션 동기화, 그리고 예외 매핑을 다룬다. 따라서 사용자의 데이터 접근 코드는 이러한 작업을 처리할 필요는 없지만 순수한 비 형식적 영속성 로직에만 초점을 맞출 수 있다. 일반적으로 네이티브 ORM 을 사용하거나 JdbcTemplate 를 사용한 JDBC 접근을 위한 템플릿을 취한다. 이런 솔루션들에 대해서는 이 문서의 다음 챕터들에서 자세히 다룬다.



저수준 동기화 처리 방법


저수준 처리 방법에는 DataSourceUtils (JDBC), EntityManagerFactoryUtils (JPA), SessionFactoryUtil (하이버네이트), PersistenceManagerFactoryUtil (JDO), 기타 등등과 같은 클래스들이 존재한다. 어플리케이션 코드가 네이티브 영속성 API 의 자원 타입을 직접적으로 다루길 원한다면 이런 클래스들을 사용해서 적절한 스프링 프레임워크 관리 인스턴스가 얻어지고, 트랜잭션이 (선택적으로) 동기화되며, 프로세스에서 발생하는 예외가 일관된 API 에 적절하게 매핑되도록 한다.


예를 들어, JDBC 의 경우, DataSource 의 getConnection() 메서드를 호출하는 전통적인 JDBC 접근 방법 대신 다음과 같이 스프링의 org.springframework.jdbc.datasource.DataSourceUtils 클래스를 사용한다.


Connection conn = DataSourceUtils.getConnection(dataSource);


기존 트랜잭션이 이미 동기화된 (링크된) 커넥션을 가지고 있다면, 그 인스턴스가 반환된다. 그렇지 않다면, 이 메서드 호출은 새로운 커넥션이 생성되도록 한다. 이 커넥션은 기존 트랜잭션과 (선택적으로) 동기화되고, 같은 트랜잭션 안에서 재사용된다. 언급된대로 어떠한 SQLException 이든 스프링 프레임워크의 unchecked DataAccessException 중 하나인 CannotGetJdbcConnectionException 으로 래핑된다. 이 처리법은 SQLException 에서 얻을 수 있는 정보보다 많은 정보를 제공하며 다른 지속성 기술에서도 데이터베이스 전반에 걸친 이식성을 보장한다.


이 처리 방법은 스프링 트랜잭션 관리 (트랜잭션 동기화는 선택적이다) 를 사용하지 않아도 유효하다. 때문에 이 방법은 트랜잭션 관리를 위해 스프링을 사용하든 사용하지 않든 사용할 수 있다.


물론 스프링의 JDBC, JPA, 또는 하이버네이트 지원을 한 번 사용해보면 보통 DataSourceUtils 이나 다른 헬퍼 클래스들은 선호하지 않게 된다. 왜냐하면 다른 관련 API 를 직접 사용하는 것보다 스프링 추상화를 통한 작업이 훨씬 편하기 때문이다. 예를 들어 스프링 JdbcTemplate 나 jdbc.object 패키지를 사용해서 JDBC 사용을 단순화하면 알맞는 커넥션 검색은 뒤에서 알아서 처리하기 때문에 이를 위한 특별한 코드를 작성할 필요가 없어진다.



TransactionAwareDataSourceProxy


TransactionAwareDataSourceProxy 클래스는 가장 저수준에 존재한다. 이 클래스는 타겟 DataSource 의 프록시이다. 타겟 DataSource 를 래핑하여 스프링 관리 트랜잭션을 인지하도록 한다. 이 점에 있어서는 자바 EE 서버가 제공하는 전통적인 JNDI DataSource 와 유사하다.


대부분의 경우 이 클래스의 사용은 절대 필요하거나 매력적이지는 않는데, 예외적으로 기존 코드가 반드시 표준 JDBC 인터페이스 구현체로 호출되고 전달되어야 하는 경우는 다르다. 이 경우, 기존 코드를 사용하면서 스프링 관리 트랜잭션에 참여하도록 할 수 있다. 위에서 언급한 더 높은 수준의 추상화를 사용하여 새 코드를 작성하는 편이 좋다.



선언적 트랜잭션 관리


대부분의 스프링 프레임워크 사용자는 선언적 트랜잭션 관리를 사용한다. 이 방법은 어플리케이션 코드로의 영향도가 가장 적고, 때문에 비 침습성 경량 컨테이너의 이상과 가장 일치한다.


스프링 프레임워크의 선언적 트랜잭션 관리는 스프링 관점 지향 프로그래밍 (AOP) 으로 가능하지만, 트랜잭션 측면의 코드는 스프링 프레임워크의 배포와 함께 제공되며 보일러플레이트 방식으로 사용될 수 있기 때문에 AOP 개념이 일반적으로 이 코드의 사용을 효과적으로 만들어준다고 이해할 필요는 없다. 


스프링 프레임워크의 선언적 트랜잭션 관리는 트랜잭션 동작을 독립적인 메서드 단위로 지정할 수 있다는 점에서 EJB CMT 와 유사하다. 필요하다면 setRollbackOnly() 메서드를 트랜잭션 컨텍스트 안에서 호출하는 일도 가능하다. 이 두 트랜잭션 관리의 차이는 다음과 같다.


  • EJB CMT 가 JTA 에 묶여 있었던 것과는 달리, 스프링 프레임워크의 선언적 트랜잭션 관리는 어느 환경에서나 유효하다. 간단히 설정을 바꾸는 것으로 JTA 트랜잭션, JDBC 를 이용한 로컬 트랜잭션, JPA, 하이버네이트 또는 JDO 등의 다양한 트랜잭션 환경에서 작동할 수 있다.
  • 스프링 프레임워크의 선언적 트랜잭션 관리는 어떤 클래스에나 적용할 수 있다. EJB 에서와 같은 특별한 클래스는 필요하지 않다.
  • 스프링 프레임워크는 EJB 에 존재하지 않는 선언적 롤백 규칙을 제공한다. 롤백 규칙은 프로그래밍 방식와 선언적 방식 모두 지원한다.
  • 스프링 프레임워크는 AOP 를 사용하여 트랜잭션 동작에 대한 커스터마이징을 지원한다. 가령 트랜잭션 롤백 수행 안에 특정한 동작을 지정할 수 있다. 트랜잭션 어드바이스와 함께 임의의 어드바이스를 추가할 수 있다. EJB CMT 를 사용하면 setRollbackOnly() 외에는 컨테이너의 트랜잭션 관리에 영향을 줄 수 없다.
  • 스프링 프레임워크는 고수준의 어플리케이션 서버에서 지원하는 원격 호출 간 트랜잭션 컨텍스트 전파를 지원하지 않는다. 이 기능을 원한다면 EJB 를 사용하길 권한다. 하지만 이런 기능을 사용하지 전에 신중히 검토해야 한다. 왜냐하면 보통은 트랜잭션이 원격 호출까지 이어지기를 원하지 않기 때문이다.

TransactionProxyFactoryBean 은 어디에 있는가?

스프링 2.0 버전 이상의 선언적 트랜잭션 설정은 이전 버전에서의 것과는 상당히 다르다. 주 차이점은 더이상 TransactionProxyFactoryBean 빈을 설정할 필요가 없다는 것이다.

스프링 2.0 전의 설정은 여전히 100% 유효하다. 새로운 <tx:tags/> 설정 대신 TransactionProxyFactoryBean 빈을 설정한다고 생각하면 된다.

롤백 규칙의 개념은 중요하다. 이 규칙은 어떤 예외 (그리고 throwables) 에서 자동 롤백이 수행될 것인지 지정하도록 한다. 이 규칙을 자바 코드가 아닌, 설정 안에 선언적으로 지정한다. 여전히 TransactionStatus 객체의 setRollbackOnly() 호출로 현재 트랜잭션을 롤백 할 수는 있지만 대부분은 MyApplicationException 가 던져졌을 때 항상, 반드시 롤백 하도록 지정할 수 있다. 이 옵션의 큰 장점은 비즈니스 객체가 트랜잭션 인프라에 의존하지 않는다는 것이다. 예를 들어, 비즈니스 클래스는 스프링 트랜잭션 API 나 기타 스프링 API 를 임포트 할 필요가 없다.


EJB 컨테이너가 기본적으로 시스템 예외 (주로 런타임 예외) 에 대한 트랜잭션 자동 롤백을 수행하긴 하지만, EJB CMT 는 어플리케이션 예외에 대해 자동 롤백을 수행하진 않는다 (java.rmi.RemoteException 을 제외한 checked 예외들). 스프링의 선언적 트랜잭션 관리의 기본 동작은 EJB 의 관습을 따르기 때문에 (자동 롤백은 unchecked 예외에 대해서만 수행), 롤백 동작 커스터마이징은 유용하게 사용된다.



스프링 프레임워크의 선언적 트랜잭션 구현체의 이해


@Transaction 와 @EnableTransactionManagement 어노테이션을 추가하는 것 만으로 트랜잭션이 어떻게 작동하는지 이해하기를 바랄 순 없다. 이 섹션은 트랜잭션 관련 문제가 발생할 경우 스프링 프레임워크의 선언적 트랜잭션 인프라가 내부적으로 어떻게 작동하는지 설명한다.


스프링 프레임워크의 선언적 트랜잭션 지원에 관한 가장 중요한 개념은 이 기능이 AOP 프록시에 의해 지원되며, 트랜잭션 어드바이스는 메타데이터 (현재 XML 또는 어노테이션 기반) 에 의해 작동한다는 것이다. AOP 와 트랜잭션 메타데이터의 결합은 AOP 프록시를 생성하는데, 이 프록시는 적절한 PlatformTransactionManager 구현체와TransactionInterceptor 를 함께 사용하여 메서드 호출 중심으로 트랜잭션을 구동한다.


개념상으로 트랜잭션 프록시 메서드 호출은 다음과 같다.




선언적 트랜잭션 구현체 예제


다음 인터페이스와 그 구현체를 보자. 이 예제는 특정 도메인 모델에 초점을 두지 않고 트랜잭션 사용법에 집중하기 위해 Foo 와 Bar 클래스를 플레이스홀더로 사용한다. 이 예제의 목적에 부합하기 위해 DefaultFooService 클래스가 각 구현 메서드에서 UnsupportedOperationException 인스턴스를 던지는 것이 좋다. 트랜잭션이 생성되고 UnsupportedOperationException 에 대해 롤백을 수행하는 것을 볼 수 있다.


// the service interface that we want to make transactional


package x.y.service;


public interface FooService {


    Foo getFoo(String fooName);


    Foo getFoo(String fooName, String barName);


    void insertFoo(Foo foo);


    void updateFoo(Foo foo);


}


// an implementation of the above interface


package x.y.service;


public class DefaultFooService implements FooService {


    public Foo getFoo(String fooName) {

        throw new UnsupportedOperationException();

    }


    public Foo getFoo(String fooName, String barName) {

        throw new UnsupportedOperationException();

    }


    public void insertFoo(Foo foo) {

        throw new UnsupportedOperationException();

    }


    public void updateFoo(Foo foo) {

        throw new UnsupportedOperationException();

    }


}


FooService 인터페이스의 처음 두 메서드, getFoo(String), getFoo(String, String) 은 반드시 읽기 전용 트랜잭션 컨텍스트에서 실행되어야 하고, insertFoo(Foo) 와 updateFoo(Foo) 는 반드시 읽기 쓰기 트랜잭션 컨텍스트에서 실행되어야 한다고 가정한다. 아래 설정은 다음 절에서 자세하게 설명한다.


<!-- from the file 'context.xml' -->

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <!-- this is the service object that we want to make transactional -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->

    <tx:advice id="txAdvice" transaction-manager="txManager">

        <!-- the transactional semantics... -->

        <tx:attributes>

            <!-- all methods starting with 'get' are read-only -->

            <tx:method name="get*" read-only="true"/>

            <!-- other methods use the default transaction settings (see below) -->

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <!-- ensure that the above transactional advice runs for any execution

        of an operation defined by the FooService interface -->

    <aop:config>

        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>

        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>

    </aop:config>


    <!-- don't forget the DataSource -->

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>

        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>

        <property name="username" value="scott"/>

        <property name="password" value="tiger"/>

    </bean>


    <!-- similarly, don't forget the PlatformTransactionManager -->

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource"/>

    </bean>


    <!-- other <bean/> definitions here -->


</beans>


위 설정을 보자. 트랜잭션 서비스 객체인 fooService 빈을 만든다. 트랜잭션 적용 사항은 <tx:advice/> 정의에 캡슐화된다. <tx:advice/> 정의는 "get 으로 시작하는 모든 메서드는 읽기 전용 컨텍스트로 실행되고, 다른 모든 메서드는 기본 트랜잭션으로 실행된다" 를 의미한다. transaction-manager 속성은 트랜잭션을 구동하는 PlatformTransactionManager 빈의 이름을 설정한다. 여기서는 txManager 빈이 그것이다.


PlatformTransactionManager 빈의 이름이 transactionManager 라면 <tx:advice/> 의 transaction-manager 속성 설정을 생략할 수 있다. PlatformTransactionManager 빈이 다른 이름을 가질 경우 예제와 같이 transaction-manager 속성을 반드시 설정해야 한다.


<aop:config/> 정의는 txAdvice 빈에서 정의한 트랜잭션이 프로그램의 적절한 지점에서 실행되도록 보장한다. 먼저 FooService 인터페이스 (fooServiceOperation) 에 정의된 모든 메서드의 실행에 매치되는 포인트컷을 정의한다. 다음으로 advisor 를 이용해서 포인트컷과 txAdvice 를 연결한다. 이 설정은 fooServiceOperation 의 실행 시점에 txAdvice 에서 정의한 어드바이스가 구동됨을 의미한다.


공통 요건은 서비스 레이어 전체를 트랜잭션으로 묶는 것이다. 이를 위한 최선의 방법은 포인트컷 표현식을 모든 서비스 레이어에 매치되도록 변경하는 것이다. 예를 들자면 다음과 같다.


<aop:config>

    <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>

    <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>

</aop:config>


이제 설정을 분석했다. 이제 이 모든 설정이 실제로 무엇을 하는지 궁금할 것이다.


위 설정은 fooService 빈 정의로부터 생성된 객체의 트랜잭션 프록시를 생성한다. 이 프록시는 트랜잭션 어드바이스를 통해 설정되어 프록시의 적절한 메서드가 호출됐을 때 트랜잭션이 시작되고, 정지되고, 읽기 전용이 되는 등 트랜잭션 설정에 따른 동작을 수행한다. 위 설정에 대한 테스트를 수행하는 다음 프로그램을 보자.


public final class Boot {


    public static void main(final String[] args) throws Exception {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml", Boot.class);

        FooService fooService = (FooService) ctx.getBean("fooService");

        fooService.insertFoo (new Foo());

    }

}


위 프로그램의 결과는 아래와 유사하다 (Log4J 아웃풋과 DefaultFooService 클래스의 insertFoo(..) 메서드 호출에 의해 던져진 UnsupportedOperationException 의 스택 트레이스. 간단히 보기 위해 중간에 잘랐음.)


<!-- the Spring container is starting up... -->

[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors


<!-- the DefaultFooService is actually proxied -->

[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]


<!-- ... the insertFoo(..) method is now being invoked on the proxy -->

[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo


<!-- the transactional advice kicks in here... -->

[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]

[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction


<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->

[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException

[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]


<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->

[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]

[DataSourceTransactionManager] - Releasing JDBC Connection after transaction

[DataSourceUtils] - Returning JDBC Connection to DataSource


Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)

<!-- AOP infrastructure stack trace elements removed for clarity -->

at $Proxy0.insertFoo(Unknown Source)

at Boot.main(Boot.java:11)



선언적 트랜잭션 롤백


이전 섹션에서 어플리케이션의 클래스, 주로 서비스 레이어 클래스에 적용하는 선언적 트랜잭션 설정 방법의 기초를 선보였다. 이 섹션에서는 간단한 선언적 방식으로 트랜잭션 롤백을 제어하는 방법에 대해 설명한다.


스프링 프레임워크의 트랜잭션 인프라에 트랜잭션이 롤백되어야 함을 알리는 좋은 방법은 현재 트랜잭션 컨텍스트에서 실행중인 코드에서 예외를 던지는 것이다. 스프링 프레임워크의 트랜잭션 인프라 코드는 호출 스택에 차오르는 처리되지 않은 예외를 잡아서 트랜잭션을 롤백할 것인지 결정한다.


기본 설정에 따르면, 스프링 프레임워크의 트랜잭션 인프라 코드는 오직 런타임, unchecked 예외만을 트랜잭션 롤백 대상으로 본다. RuntimeException 의 인스턴스나 이것의 서브클래스가 던져져야 한다는 뜻이다. (Error 또한 기본적으로 롤백 대상이다.) 트랜잭션 메서드에서 발생한 checked 예외는 기본 설정 상 롤백되지 않는다.


checked 예외를 포함하여 어떤 예외 타입에 대해 트랜잭션 롤백을 수행할지 정확히 설정할 수 있다. 다음 XML 스니핏은 어플리케이션에서 정의한 checked 예외 타임에 대한 롤백을 설정하는 방법을 보여준다.


<tx:advice id="txAdvice" transaction-manager="txManager">

    <tx:attributes>

    <tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>

    <tx:method name="*"/>

    </tx:attributes>

</tx:advice>


어떤 예외에 대해서는 트랜잭션 롤백 처리를 원하지 않을 경우 이를 설정할 수도 있다. 다음 예제는 스프링 프레임워크의 트랜잭션 인프라에게 처리되지 않은 InstrumentNotFoundException 이 발생해도 트랜잭션을 롤백하지 않고 커밋할 것을 지정한다.


<tx:advice id="txAdvice">

    <tx:attributes>

    <tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>

    <tx:method name="*"/>

    </tx:attributes>

</tx:advice>


프로그래밍 방식으로 롤백을 처리할 수도 있다. 이 방법은 아주 간단하긴 하지만 상당히 침습적으로, 어플리케이션 코드와 스프링 트랜잭션 인프라 코드의 결합도를 크게 높이게 된다.


public void resolvePosition() {

    try {

        // some business logic...

    } catch (NoProductInStockException ex) {

        // trigger rollback programmatically

        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

    }

}


가능하면 선언적 롤백 설정을 사용하는 편이 좋다. 프로그래밍 방식 롤백은 반드시 필요한 경우에 사용하도록 한다. 이 방식은 깨끗한 POJO 기반 아키텍처를 구현할 때 뛰어난 사용법이 된다.



다른 빈에서의 다른 트랜잭션 설정


이런 시나리오를 생각해보자. 다수의 서비스 레이어 객체를 가지며 각각의 레이어 객체에 대해 서로 완전히 다른 트랜잭션 설정을 적용하고자 한다. 이런 설정은 개별적인 <aop:advisor/> 엘레멘트를 정의하여 다른 pointcut 과 advice-ref 속성 값을 설정하는 방법으로 가능하다.


비교해보자면, 먼저 모든 서비스 레이어 클래스는 루트 x.y.service 패키지에 정의되어 있다고 가정한다. 이 패키지 (혹은 서브패키지) 에 정의된, 이름이 Service 로 끝나는 모든 클래스 인스턴스 빈에 기본 트랜잭션 설정을 적용하기 위해 다음과 같이 작성한다.


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <aop:config>


        <aop:pointcut id="serviceOperation"

                expression="execution(* x.y.service..*Service.*(..))"/>


        <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>


    </aop:config>


    <!-- these two beans will be transactional... -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <bean id="barService" class="x.y.service.extras.SimpleBarService"/>


    <!-- ... and these two beans won't -->

    <bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->

    <bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->


    <tx:advice id="txAdvice">

        <tx:attributes>

            <tx:method name="get*" read-only="true"/>

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <!-- other transaction infrastructure beans such as a PlatformTransactionManager omitted... -->


</beans>


다음으로 두 개의 서로 다른 빈에 각각 완전히 다른 트랜잭션 설정이 적용된 예제를 보자


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <aop:config>


        <aop:pointcut id="defaultServiceOperation"

                expression="execution(* x.y.service.*Service.*(..))"/>


        <aop:pointcut id="noTxServiceOperation"

                expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>


        <aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>


        <aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>


    </aop:config>


    <!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- this bean will also be transactional, but with totally different transactional settings -->

    <bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>


    <tx:advice id="defaultTxAdvice">

        <tx:attributes>

            <tx:method name="get*" read-only="true"/>

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <tx:advice id="noTxAdvice">

        <tx:attributes>

            <tx:method name="*" propagation="NEVER"/>

        </tx:attributes>

    </tx:advice>


    <!-- other transaction infrastructure beans such as a PlatformTransactionManager omitted... -->


</beans>



<tx:advice/> 설정


이 섹션은 <tx:advice/> 태그를 통한 다양한 트랜잭션 설정을 요약해서 설명한다. 기본 <tx:advice/> 설정은 다음과 같다.


  • 전파 설정은 REQUIRED 이다.
  • 격리 수준은 DEFAULT 이다.
  • 트랜잭션은 읽기/쓰기 이다.
  • 트랜잭션 타임아웃은 트랜잭션 기반 시스템의 기본 타임아웃 설정을 따르며, 타임아웃이 지원되지 않는다면 트랜잭션 타임아웃은 없다.
  • 모든 RuntimeException 예외에 대해서 롤백을 수행하며, checked Exception 에는 수행하지 않는다.

이 기본 설정은 변경할 수 있다. 아래의 표는 <tx:advice/> 와 <tx:attributes/> 태그 안에 위치한 <tx:method/> 태그의 다양한 속성을 정리한다.

<tx:method/> 설정

속성 

 필수 여부

 기본값

 설명

 name

 Y

 

트랜잭션 속성이 적용될 메서드 이름. 다수의 메서드에 같은 설정을 적용하기 위해 와일드카드 (*) 를 사용할 수 있다. 예로, get*, handle*, on*Event 등등

 propagation

 N

 REQUIRED

트랜잭션 전파 동작

 isolation

 N

 DEFAULT

트랜잭션 격리 수준

 timeout

 N

 -1

트랜잭션 타임아웃 값

 read-only

 N

 false

읽기 전용 트랜잭션인가?

 rollback-for

 N

 

롤백을 적용할 Exception(s). 콤마로 구분한다. 예로, com.foo.MyBusinessException,ServletException

 no-rollback-for

 N

 

롤백을 적용하지 않을 Exception(s). 콤마로 구분한다. 예로, com.foo.MyBusinessException,ServletException



@Transactional 사용하기


XML 기반 선언적 트랜잭션 설정법에 더해서 어노테이션 기반의 방법을 사용할 수 있다. 트랜잭션을 자바 소스코드에 직접 선언하면 트랜잭션 선언과 트랜잭션이 적용되는 코드가 훨씬 가깝게 된다. 트랜잭션이 적용되는 코드는 거의 항상 그런 방식으로 배포되기 때문에 지나친 결합에 의한 위험성은 크지 않다.


스프링의 어노테이션 대신 표준 javax.transaction.Transactional 어노테이션도 지원된다. 자세한 정보는 JTA 1.2 문서를 참조하라.


@Transactional 어노테이션은 사용하기 쉽다.


// the service class that we want to make transactional

@Transactional

public class DefaultFooService implements FooService {


    Foo getFoo(String fooName);


    Foo getFoo(String fooName, String barName);


    void insertFoo(Foo foo);


    void updateFoo(Foo foo);

}


위 POJO 가 스프링 IoC 컨테이너에 빈으로 정의되었다면 이 빈 인스턴스는 XML 설정을 한 줄 추가하는 것으로 트랜잭션 처리 대상이 된다.


<!-- from the file 'context.xml' -->

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <!-- this is the service object that we want to make transactional -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- enable the configuration of transactional behavior based on annotations -->

    <tx:annotation-driven transaction-manager="txManager"/><!-- a PlatformTransactionManager is still required -->

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <!-- (this dependency is defined somewhere else) -->

        <property name="dataSource" ref="dataSource"/>

    </bean>


    <!-- other <bean/> definitions here -->


</beans>


<tx:annotation-driven/> 태그의 transaction-manager 속성도 PlatformTransactionManager 빈의 이름이 transactionManager 인 경우에 생략될 수 있다. 


자바 기반 설정을 사용한다면 @EnableTransactionManagement 어노테이션을 사용해서 동일한 효과를 얻을 수 있다. 간단히 이 어노테이션을 @Configuration 클래스에 추가하면 된다. 


메서드 가시성과 @Transactional


프록시를 사용할 때, @Transactional 어노테이션은 반드시 public 메서드에 적용되어야 한다. protected, private, package-visible 메서드에 @Transactional 어노테이션이 추가되면 에러가 발생하지는 않지만 해당 메서드에 트랜잭션 설정이 적용되지 않는다. non-public 메서드에 어노테이션을 적용하고 싶다면 어스팩트J 사용을 고려하라.


@Transactional 어노테이션은 인터페이스 정의, 인터페이스의 메서드, 클래스 정의, 클래스의 public 메서드에 적용할 수 있다. 하지만 @Transactional 어노테이션을 추가하는 것 만으로 트랜잭션을 활성화 할 수는 없다. @Transactional 어노테이션은 런타임 인프라가 @Transactional 을 인지하고 적절한 빈과 트랜잭션 동작을 설정하기 위해 사용되는 메타메이터가 된다. <tx:annotation-driven/> 엘레멘트가 트랜잭션 동작의 스위치가 된다.


@Transactional 어노테이션은 구체화된 클래스 (그리고 구체화된 클래스의 메서드) 에 적용할 것을 권한다. 인터페이스나 인터페이스의 메서드에 적용하는 것도 가능하긴 하지만, 이 때는 인터페이스 기반 프록시에서만 유효한 트랜잭션 설정이 된다. 자바 어노테이션은 인터페이스로부터 상속되지 않는다는 사실은, 클래스 기반 프록시 (proxy-target-class="true") 나 위빙 기반 어스팩트 (mode="aspectj") 를 사용할 경우 프록싱과 위빙 인프라가 트랜잭션 설정을 인식할 수 없다는 것을 의미하고, 객체는 트랜잭션 프록시로 래핑되지 않게 된다. 확실히 좋은 설정이 아니다.


기본 프록시 모드에서는 외부로부터의 프록시를 통한 메서드 호출만이 인터셉트된다. 한 객체의 메서드에서 같은 객체 (자신) 의 다른 메서드를 호출할 경우, 이 메서드에 @Transactional 어노테이션이 적용되어 있다 할지라도 실제 트랜잭션 처리는 적용되지 않는다. 또한 프록시가 원하는 대로 작동하기 위해서는 반드시 완전한 초기화가 이루어져야 하므로 초기화 코드 (@PostConstructor) 에 이 기능을 사용하지 않아야 한다.


객체 자신에 대한 호출에도 트랜잭션을 적용하려면 어스팩트J 모드 사용을 고려해야 한다. 이 경우 처음에는 프록시가 없다. 대신 @Transactional 을 모든 종류의 런타임 동작으로 바꾸기 위해서 타겟 클래스를 위빙한다 (클래스의 바이트코드가 수정된다.)


어노테이션 기반 트랜잭션 세팅

XML 속성

 어노테이션 속성

 기본값

 설명

 transaction-manager

 N/A

 transactionManager

 사용할 트랜잭션 매니저의 이름. 이름이 transactionManager 가 아닌 경우에만 필수이다.

 mode

 mode

 proxy

 기본 모드인 "proxy" 는 어노테이션이 적용된, 스프링의 AOP 프레임워크를 사용하여 프록시되기 위한 빈을 처리한다. (여기서의 프록시는 언급한대로 프록시를 통한 외부 메서드 호출에만 적용된다.) 다른 모드인 "aspectj" 는 대상 클래스를 스프링의 어스팩트J 트랜잭션 어스팩트로 위빙하는데, 클래스의 바이트코드를 수정하여 모든 종류의 메서드 호출에 트랜잭션 코드를 적용한다. 어스팩트J 로드타임 위빙이나 컴파일타임 위빙에는 spring-aspects.jar 가 필요하다.

 proxy-target-class

 proxyTargetClass

 false

 프록시 모드를 적용한다. @Transactional 어노테이션이 적용된 클래스에 어떤 타입의 트랜잭션 프록시를 생성할 것인지 설정한다. proxy-target-class 가 true 라면 클래스 기반 프록시가 생성된다. false (또는 생략)라면 표준 JDK 인터페이스 기반 프록시가 생성된다.

 order

 order

 Ordered.LOWEST_PRECEDENCE

@Transactional 어노테이션이 적용된 빈에 적용될 트랜잭션 어드바이스의 순서를 정의한다. 따로 지정하지 않으면 AOP 서브시스템이 순서를 정한다.


@EnableTransactionManagement 와 <tx:annotation-driven/> 은 정의되어 있는 어플리케이션 컨텍스트 안에서 빈의 @Transactional 을 찾는다. 이 설정을 DispatcherServlet 에 대한 WebApplicationContext 에 적용한다면 서비스가 아닌 컨트롤러 안의 빈만을 찾는다.


메서드의 트랜잭션 설정을 평가할 때는 가장 많이 얻어지는 위치가 우선권을 갖는다. 다음 예제의 DefaultFooService 클래스에는 읽기 전용 트랜잭션 설정이 클래스 레벨 어노테이션으로 지정되었다. 하지만 같은 클래스에 있는 updateFoo(Foo) 메서드의 @Transactional 어노테이션의 트랜잭션 설정이 클래스 레벨 어노테이션보다 우선한다.


@Transactional(readOnly = true)

public class DefaultFooService implements FooService {


    public Foo getFoo(String fooName) {

        // do something

    }


    // these settings have precedence for this method

    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)

    public void updateFoo(Foo foo) {

        // do something

    }

}



@Transactional 설정


@Transactional 어노테이션은 인터페이스, 클래스, 메소드 등에게 반드시 트랜잭션 처리를 하도록 지정하는 메타데이터이다. 예를 들어, "이 메소드가 실행되면 모든 기존 트랜잭션을 중단하고 새로운 읽기 전용 트랜잭션을 시작한다." 와 같은 의미를 가질 수 있다. @Transactional 설정의 기본 설정은 다음과 같다.


  • 전파 설정은 PROPAGATION_REQUIRED.
  • 격리 수준은 ISOLATION_DEFAULT.
  • 읽기/쓰기 트랜잭션
  • 타임아웃은 기반 트랜잭션 시스템의 기본 설정을 따른다. 타임아웃이 지원되지 않는다면 타임아웃은 없다.
  • 모든 RuntimeException 에 대해 롤백을 수행하고, checked Exception 에 대해서는 수행하지 않는다.

이 기본 설정은 변경될 수 있다. @Transactional 어노테이션의 다양한 프로퍼티 설정에 대해서는 아래 요약되어 있다.


@Transactional 설정

 프로퍼티

 타입

 설명

 value

 String 

 사용할 트랜잭션 관리자를 지정하는 선택적 구분자

 propagation

 enum: Propagation

 선택적 전파 설정.

 isolation

 enum: Isolation

 선택적 격리 수준.

 readOnly

 boolean

 읽기/쓰기 vs 읽기 전용 트랜잭션.

 timeout

 int (초)

 트잭션 타임아웃.

 rollbackFor

 Throwable 로부터 얻을 수 있는 Class 객체 배열

 롤백이 수행되어야 하는, 선택적인 예외 클래스의 배열.

 rollbackForClassName

 Throwable 로부터 얻을 수 있는 클래스 이름 배열

 롤백이 수행되어야 하는, 선택적인 예외 클래스 이름의 배열.

 noRollbackFor

 Throwable 로부터 얻을 수 있는 Class 객체 배열

 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스의 배열.

 noRollbackForClassName

 Throwable 로부터 얻을 수 있는 클래스 이름 배열

 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스 이름의 배열.


현재는 트랜잭션의 이름을 명시적으로 설정할 수 없다. 여기서 트랜잭션의 '이름' 이란 트랜잭션 모니터와 (적용 가능한 경우) 로깅 출력에 표시되는 트랜잭션 이름을 의미한다. 선언적 트랜잭션에서 트랜잭션 이름은 항상 완전한 클래스 이름 + "." + 트랜잭션 처리 메서드 이름이 된다. 예를 들어, BusinessService 클래스의 handlePayment(..) 메서드에서 트랜잭션이 시작된다면 트랜잭션 이름은 com.foo.BusinessService.handlePayment 가 된다.



@Transactional 을 이용한 다중 트랜잭션 매니저


스프링 어플리케이션은 대부분 단인 트랜잭션 매니저로 구동되지만, 다수의 독립된 트랜잭션 매니저가 필요한 상황도 있다. @Transacional 어노테이션의 value 속성은 사용할 PlatformTransactionManager 를 선택적으로 지정할 수 있다. 이 값은 빈 이름이나 트랜잭션 매니저 빈의 qualifier 값이 지정될 수 있다. 예를 들어 qualifier 표기법을 사용한다면 자바 코드는 다음과 같다.


public class TransactionalService {


    @Transactional("order")

    public void setSomething(String name) { ... }


    @Transactional("account")

    public void doSomething() { ... }

}


위 자바 코드는 어플리케이션 컨텍스트의 다음 트랜잭션 매니저 빈 선언과 함께 설정된다.


<tx:annotation-driven/>


    <bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        ...

        <qualifier value="order"/>

    </bean>


    <bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        ...

        <qualifier value="account"/>

    </bean>


여기서 TransactionalService 의 두 메서드는 각각의 트랜잭션 매니저로 실행된다. 각 트랜잭션 매니저는 "order" 와 "account" 구분자로 구분된다. 기본 <tx:annotation-driven> 대상 빈 이름으로는 여전히 transactionManager 가 사용된다.



커스텀 숏컷 어노테이션


여러 메서드들에 같은 속성이 지정된 @Transactional 어노테이션을 사용하는 일이 많아진다면 스프링의 메타 어노테이션 지원의 커스텀 숏컷 어노테이션이 유용하게 사용될 수 있다. 예제로 다음 어노테이션을 보자.


@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Transactional("order")

public @interface OrderTx {

}


@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Transactional("account")

public @interface AccountTx {

}


그리고 위 어노테이션은 다음과 같이 사용한다.


public class TransactionalService {


    @OrderTx

    public void setSomething(String name) { ... }


    @AccountTx

    public void doSomething() { ... }

}


여기서는 트랜잭션 매니저 구분자를 예로 들었지만, 트랜잭션 전파, 롤백 규칙, 타임아웃 등 다른 설정도 이렇게 사용할 수 있다.



트랜잭션 전파


이 섹션은 스프링의 트랜잭션 전파의 의미를 설명한다. 이 섹션은 트랜잭션 전파에 대한 소개가 아니다. 스프링에서의 트랜잭션 전파에 관하여 자세히 설명한다.


스프링 관리 트랜잭션에서는 물리적 트랜잭션과 논리적 트랜잭션의 차이와 트랜잭션 전파 설정이 이 차이를 어떻게 적용하는지 알아야 한다.



Required



PROPAGATION_REQUIRED


전파 설정이 PROPAGATION_REQUIRED 일 때는 각 트랜잭션 처리 대상 메서드에 논리적 트랜잭션 범위가 생성된다. 이 논리적 트랜잭션 범위는 롤백 전용 상태를 개별적으로 결정할 수 있으며, 외부 트랜잭션 범위는 내부 트랜잭션 범위와 논리적으로 독립적이다. 물론 표준 PROPAGATION_REQUIRED 동작의 경우 이런 모든 범위는 동일한 물리적 트랜잭션으로 매핑된다. 따라서 내부 트랜잭션 범위에 설정된 롤백 전용 표시는 외부 트랜잭션의 실제 커밋에 영향을 준다.


하지만 내부 트랜잭션 범위에 롤백 전용이 설정된 경우에는 외부 트랜잭션은 롤백 여부를 스스로 결정하지 않는다. 때문에 (내부 트랜잭션 범위에 의해 자동적으로 발생한) 롤백은 예상치 못하게 발생하게 된다. 이 시점에 여기에 대응하는 UnexpectedRollbackException 이 발생한다. 이 예외는 트랜잭션 호출자로 하여금 트랜잭션이 실제로 커밋 되지 않았음에도 커밋이 된 것으로 오인하는 것을 방지하기 위한 예상된 동작이다. 그래서 내부 트랜잭션이 (외부 트랜잭션 모르게) 트랜잭션을 롤백 전용으로 표시하면 외부 호출자는 커밋을 호출한다. 외부 호출자는 UnexpectedRollbackException 을 받아서 롤백이 대신 수행되었음을 명확하게 나타내야 한다.



RequiresNew



PROPAGATION_REQUIRES_NEW


PROPAGATION_REQUIRES_NEW 는 PROPAGATION_REQUIRED 와 대조적으로, 각 트랜잭션 범위에 대해 완전하게 독립된 트랜잭션을 사용한다. 기반 물리적 트랜잭션이 서로 다르며, 때문에 독립적인 커밋이나 롤백을 수행할 수 있다. 외부 트랜잭션은 내부 트랜잭션의 롤백 상태의 영향을 받지 않는다.



Nested


PROPAGATION_NESTED 는 롤백 가능한 다수의 세이브포인트를 가진 단일 물리적 트랜잭션을 사용한다. 이런 부분적 롤백은 내부 트랜잭션 범위가 자신의 범위에 대한 롤백을 수행할 수 있도록 한다. 외부 트랜잭션은 몇몇 작업이 롤백되어도 물리적 트랜잭션을 지속할 수 있다. 이 설정은 보통 JDBC 세이브포인트에 매핑된다. 때문에 JDBC 리소스 트랜잭션에서만 유효하다.



트랜잭션 작업 조언


트랜잭션 및 기본 프로파일링 어드바이스를 모두 실행한다고 가정한다. <tx:annotation-driven/> 의 컨텍스트 안에서 이것을 어떻게 적용할까?


updateFoo(Foo) 메서드를 실행할 때 다음 동작들을 보고자 한다.


  • 프로파일링 어스팩트 시작

  • 트랜잭션 어드바이스 실행

  • 어드바이스된 객체의 메서드 실행

  • 트랜잭션 커밋

  • 프로파일링 어스팩트의 정확한 전체 트랜잭션 메서드 실행 시간 보고


이 챕터에서는 AOP 의 자세한 설명을 고려하지 않았다. AOP 설정에 대한 자세한 정보는 AOP 챕터를 참조하기 바란다. 


아래는 위에서 언급한 간단한 프로파일링 어스팩트 코드다. 어드바이스 순서는 Ordered 인터페이스를 통해 설정되었다. 


package x.y;


import org.aspectj.lang.ProceedingJoinPoint;

import org.springframework.util.StopWatch;

import org.springframework.core.Ordered;


public class SimpleProfiler implements Ordered {


    private int order;


    // allows us to control the ordering of advice

    public int getOrder() {

        return this.order;

    }


    public void setOrder(int order) {

        this.order = order;

    }


    // this method is the around advice

    public Object profile(ProceedingJoinPoint call) throws Throwable {

        Object returnValue;

        StopWatch clock = new StopWatch(getClass().getName());

        try {

            clock.start(call.toShortString());

            returnValue = call.proceed();

        } finally {

            clock.stop();

            System.out.println(clock.prettyPrint());

        }

        return returnValue;

    }

}


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- this is the aspect -->

    <bean id="profiler" class="x.y.SimpleProfiler">

        <!-- execute before the transactional advice (hence the lower order number) -->

        <property name="order" __value="1"__/>

    </bean>


    <tx:annotation-driven transaction-manager="txManager" __order="200"__/>


    <aop:config>

            <!-- this advice will execute around the transactional advice -->

            <aop:aspect id="profilingAspect" ref="profiler">

                <aop:pointcut id="serviceMethodWithReturnValue"

                        expression="execution(!void x.y..*Service.*(..))"/>

                <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>

            </aop:aspect>

    </aop:config>


    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>

        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>

        <property name="username" value="scott"/>

        <property name="password" value="tiger"/>

    </bean>


    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource"/>

    </bean>


</beans>


위 설정은 fooService 빈에 프로파일링과 트랜잭션 어스팩트를 원하는 순서대로 적용한다. 이런 방식으로 몇 개의 어스팩트든 추가로 설정할 수 있다.


다음 예제는 위와 같은 설정을 적용하지만 순수 XML 선언법을 사용한다.


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- the profiling advice -->

    <bean id="profiler" class="x.y.SimpleProfiler">

        <!-- execute before the transactional advice (hence the lower order number) -->

        __<property name="order" value="1__"/>

    </bean>


    <aop:config>

        <aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>

        <!-- will execute after the profiling advice (c.f. the order attribute) -->


        <aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" __order="2__"/>

        <!-- order value is higher than the profiling aspect -->


        <aop:aspect id="profilingAspect" ref="profiler">

            <aop:pointcut id="serviceMethodWithReturnValue"

                    expression="execution(!void x.y..*Service.*(..))"/>

            <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>

        </aop:aspect>


    </aop:config>


    <tx:advice id="txAdvice" transaction-manager="txManager">

        <tx:attributes>

            <tx:method name="get*" read-only="true"/>

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <!-- other <bean/> definitions such as a DataSource and a PlatformTransactionManager here -->


</beans>


위 설정은 fooService 빈에 프로파일링과 트랜잭션 어스팩트를 지정된 순서로 적용한다. 프로파일링 어드바이스를 트랜잭션 어드바이스 다음 혹은 이전에 실행하고 싶다면 간단히 order 프로퍼티 값을 바꾸도록 한다.



@Transactional 과 어스팩트J 사용하기


어스팩트J 를 사용해 스프링 프레임워크의 @Transactional 지원을 스프링 컨테이너 외부에서 사용할 수 있다. 먼저 클래스에 (또는 클래스의 메서드에) @Transactional 어노테이션을 설정하고 spring-aspects.jar 에 정의된 org.springframework.transaction.AnnotationTransactionAspect 를 통해 어플리케이션으로 위빙한다. 어스팩트는 반드시 트랜잭션 매니저로 설정되어야 한다. 물론 스프링 프레임워크의 IoC 컨테이너로 어스팩트의 DI 를 제어할 수 있다. 트랜잭션 관리 어스팩트를 설정하는 가장 간단한 방법은 <tx:annotation-driven/> 엘레멘트를 사용하고 mode 속성을 aspectj 로 지정하는 것이다. 여기서는 스프링 컨테이너 외부에서 구동되는 어플리케이션에 포커스를 두기 때문에 프로그래밍 방식으로 보여준다.


// construct an appropriate transaction manager

DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());


// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods

AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);


이 어스팩트를 사용할 때는 반드시 인터페이스가 아닌, 구체화된 클래스 (또는 구체화된 클래스의 메서드) 에 어노테이션을 설정해야 한다. 어스팩트J 는 어노테이션은 인터페이스로부터 상속되지 않는다는 자바의 원칙을 따른다.


클래스의 @Transactional 어노테이션은 클래스의 모든 메서드의 실행에 대한 기본 트랜잭션 처리를 적용한다.


메서드의 @Transactional 어노테이션은 클래스 어노테이션에 의해 주어진 기본 트랜잭션 처리를 적용한다. 메서드 가시성에 상관없이 모든 메서드에 적용 가능하다.


어플리케이션을 AnnotationTransactionAspect 로 위빙하기 위해서는 어플리케이션을 반드시 어스팩트J 와 함께 빌드하거나 로드 타임 위빙을 사용해야 한다. 



프로그래밍 방식 트랜잭션 관리


스프링 프레임워크가 제공하는 프로그래밍 방식 트랜잭션 관리는 두 가지 의미가 있다.


  • TransactionTemplate 사용
  • PlatformTransactionManager 구현체 직접 사용

스프링 팀은 프로그래밍 방식 트랜잭션 관리에 있어 TransactionTemplate 사용을 권장한다. 두 번째 방식은 예외 핸들링이 더 가볍다는 점 외엔 JTA UserTransaction API 를 사용하는 것과 유사하다. 


TransactionTemplate 사용하기

TransactionTemplate 는 JdbcTemplate 와 같은 다른 스프링 템플릿과 같은 접근법을 취한다. 콜백 방식을 사용해서 트랜잭션 리소스 획득, 해제와 같은 보일러플레이트 코드를 어플리케이션 코드에서 분리하고 의도에 맞는, 오로지 개발자가 원하는 동작에 집중하도록 한다.

다음 예제에서 보듯이 TransactionTemplate 사용은 스프링의 트랜잭션 인프라와 API 에 전적으로 묶이게 된다. 프로그래밍 방식이 어플리케이션 개발의 요건에 맞는지 맞지 않는지는 스스로 결정해야 한다.


반드시 트랜잭션 컨텍스트에서 실행하면서 TransactionTemplate 을 사용하는 어플리케이션 코드는 다음과 같다. TransactionCallback 구현체는 (보통 익명 내부 클래스로) 트랜잭션 컨텍스트에서 실행할 코드를 작성하도록 한다. 그리고 커스텀 TransactionCallback 의 인스턴스를 TransactionTemplate 의 execute(..) 메서드로 전달한다.


public class SimpleService implements Service {


    // single TransactionTemplate shared amongst all methods in this instance

    private final TransactionTemplate transactionTemplate;


    // use constructor-injection to supply the PlatformTransactionManager

    public SimpleService(PlatformTransactionManager transactionManager) {

        Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null.");

        this.transactionTemplate = new TransactionTemplate(transactionManager);

    }


    public Object someServiceMethod() {

        return transactionTemplate.execute(new TransactionCallback() {

            // the code in this method executes in a transactional context

            public Object doInTransaction(TransactionStatus status) {

                updateOperation1();

                return resultOfUpdateOperation2();

            }

        });

    }

}


반환값이 없다면 TransactionCallbackWithoutResult 클래스를 사용한다.


transactionTemplate.execute(new TransactionCallbackWithoutResult() {

    protected void doInTransactionWithoutResult(TransactionStatus status) {

        updateOperation1();

        updateOperation2();

    }

});


콜백 안의 코드는 파라미터로 제공된 TransactionStatus 객체의 setRollbackOnly() 메서드를 호출하여 트랜잭션을 롤백할 수 있다.


transactionTemplate.execute(new TransactionCallbackWithoutResult() {


    protected void doInTransactionWithoutResult(TransactionStatus status) {

        try {

            updateOperation1();

            updateOperation2();

        } catch (SomeBusinessExeption ex) {

            status.setRollbackOnly();

        }

    }

});



트랜잭션 설정 지정하기


프로그래밍 방식이나 설정 방식으로 TransactionTemplate 의 전파, 격리 수준, 타임아웃과 같은 트랜잭션 설정을 지정할 수 있다. TransactionTemplate 인스턴스는 기본적으로 기본 트랜잭션 설정을 가진다. 다음 예제는 프로그래밍 방식으로 커스터마이징된 TransactionTemplate 의 트랜잭션 설정을 보여준다.


public class SimpleService implements Service {


    private final TransactionTemplate transactionTemplate;


    public SimpleService(PlatformTransactionManager transactionManager) {

        Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null.");

        this.transactionTemplate = new TransactionTemplate(transactionManager);


        // the transaction settings can be set here explicitly if so desired

        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);

        this.transactionTemplate.setTimeout(30); // 30 seconds

        // and so forth...

    }

}


다음 예제는 스프링 XML 설정을 사용한 TransactionTemplate 의 트랜잭션 설정이다. sharedTransactionTemplate 은 필요에 따라 다수의 서비스로 주입 가능하다.


<bean id="sharedTransactionTemplate"

        class="org.springframework.transaction.support.TransactionTemplate">

    <property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>

    <property name="timeout" value="30"/>

</bean>


마지막으로 TransactionTemplate 클래스의 인스턴스는 쓰레드세이프하다. 인스턴스 안에 어떠한 어플리케이션 작동 상태도 유지하지 않는다. TransactionTemplate 인스턴스는 설정 상태를 유지한다. 따라서 TransactionTemplate 인스턴스는 다수의 클래스에 공유될 수 있고, 만약 다르게 설정된 TransactionTemplate 이 필요하다면 두 개의 구분된 TransactionTemplate 를 생성해야 한다.



PlatformTransactionManager 사용하기


트랜잭션 관리를 위해 org.springframework.transaction.PlatformTransactionManager 를 직접 사용할 수도 있다. 간단하게 사용중인 PlatformTransactionManager 의 구현체를 빈에 넘기면 된다. 그리고 TransactionDefinition 과 TransactionStatus 객체를 사용해서 트랜잭션을 초기화하고 롤백하고 커밋할 수 있다.


DefaultTransactionDefinition def = new DefaultTransactionDefinition();

// explicitly setting the transaction name is something that can only be done programmatically

def.setName("SomeTxName");

def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);


TransactionStatus status = txManager.getTransaction(def);

try {

    // execute your business logic here

}

catch (MyException ex) {

    txManager.rollback(status);

    throw ex;

}

txManager.commit(status);



프로그래밍 방식 vs 선언적 방식


트랜잭션 작업이 많지 않다면 프로그래밍 방식은 대개 좋은 선택이 된다. 예를 들어, 트랜잭션 작업은 몇 개의 update 가 전부인 웹 어플레케이션을 개발한다면 스프링이나 다른 기술을 사용한 트랜잭션 프록시 설정을 원치 않을 수 있다. 이런 경우에 TransactionTemplate 은 좋은 처리 방법이다. 명시적으로 트랜잭션 이름을 세팅하는 일은 오직 프로그래밍 방식 트랜잭션 관리에서만 할 수 있는 일이기도 하다.


다른 한편으로는, 어플리케이션에 수많은 트랜잭션 작업이 필요하다면 일반적으로 선언적 트랜잭션 관리가 좋다. 이 방식은 트랜잭션 관리를 비즈니스 로직과 분리하며, 설정하기 어렵지 않다. 스프링 프레임워크를 사용하면 EJB CMT 를 사용할 때보다 선언적 트랜잭션 관리 설정 비용이 크게 줄어든다.



트랜잭션 바운드 이벤트


스프링 4.2 에서는 트랜잭션의 한 단계에 이벤트 리스너를 바인딩할 수 있다. 전형적인 예로는 트랜잭션이 성공적으로 완료됐을 때의 이벤트를 핸들링하는 것이다. 리스너에게 현재 트랜잭션의 결과가 중요한 경우, 이런 이벤트 처리가 더 유연하게 사용될 수 있다.


보통의 이벤트 리스너는 @EventListener 어노테이션을 통해 등록한다. 리스너를 트랜잭션에 바인딩하려면 @TransactionalEventListener 를 사용한다. 이 어노테이션을 사용하면 기본적으로 리스너는 트랜잭션의 커밋 단계에 바인딩된다.


이제 이벤트 바인딩 개념의 예제를 보자. 한 컴포넌트가 주문 생성 이벤트를 게시하고, 성공적으로 커밋된 트랜잭션 이벤트를 핸들링하는 리스너를 정의한다고 가정한다.


@Component

public class MyComponent {


    @TransactionalEventListener

    public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {

          ...

    }

}


TransactionalEventListener 어노테이션은 트랜잭션의 어느 단계에 리스너를 바인딩할지 설정하는 phase 속성을 노출한다. 유효한 트랜잭션 단계는 BEFORE_COMMIT, AFTER_COMMIT (기본값), AFTER_ROLLBACK, 그리고 완료된 트랜잭션을 종합하는 AFTER_COMPLETION (커밋 또는 롤백) 이 있다.


구동중인 트랜잭션이 없다면 리스너는 실행되지 않는다. 하지만 어노테이션의 fallbackExecution 속성을 true 로 세팅하여 동작을 오버라이딩할 수 있다.



어플리케이션 서버 별 통합


스프링의 트랜잭션 추상화는 일반적으로 어플리케이션 서버에 무관심하다. 스프링의 JtaTransactionManager 클래스는 선택적으로 JTA UserTransaction 과 TransactionManager 객체에 대한 JNDI 룩업을 수행할 수 있다. JtaTransactionManager 클래스는 TransactionManager 객체를 자동으로 탐지하는데, 이 객체는 어플리케이션 서버마다 다르다. JTA TransactionManager 에 대한 접근 권한을 갖게되면 향상된 트랜잭션을 얻게 되는데, 특히 트랜잭션 일시 중단을 지원한다. 


스프링의 JtaTransactionManager 는 자바 EE 어플리케이션 서버 구동의 표준적인 선택이며 모든 공통된 서버에서 작동한다고 알려져 있다. 트랜잭션 일시 중단과 같은 발전된 기능은 GlassFish, JBoss, Geronimo 를 포함한 많은 서버에서 작동한다. 여기에 특별한 설정은 필요하지 않다. 하지만 완전하게 지원되는 트랜잭션 일시 중단과 더 발전된 통합을 위해 스프링은 WebLogic 과 WebSphere 서버를 위한 특별한 어댑터를 제공한다. 이 어댑터들에 대해서는 다음 섹션에서 다룬다.


WebLogic 과 WebSphere 서버를 포함한 표준적인 시나리오에서는 편리하게 <tx:jta-transaction-manager/> 설정 엘레멘트 사용을 고려하라. 이 엘레멘트를 설정하면 기반이 되는 서버를 자동으로 감지하고 플랫폼에서 유효한 최선희 트랜잭션 매니저를 선택한다. 이는 서버 별 어댑터 클래스 (다음 섹션에서 다루는) 를 명시적으로 설정할 필요가 없다는 뜻이다. 어댑터 클래스는 사용으로 선택된다. 기본적으로 표준 JtaTransactionManager 가 사용된다.



IBM WebSphere


WebSphere 6.1.0.9 와 그 위의 버전에서 권장되는 스프링 JTA 트랜잭션 매니저는 WebSphereUowTransactionManager 이다. 이 특별한 어댑터는 IBM 의 UOWManager API 를 사용한다. 이 API 는 WebSphere 어플리케이션 서버 6.1.0.9 와 이후 버전에서 유효하다. 이 어댑터를 사용하면 스프링 기반 트랜잭션 일시 중단 (PROPAGATION_REQUIRES_NEW 에 의한 중단/재개) 은 공식적으로 IBM 이 지원한다.



오라클 WebLogic 서버


WebLogic 서버 9.0 또는 그 위의 버전에서 일반적으로 WebLogicJtaTransactionManager 를 사용한다. 이 특별한 WebLogic 전용 클래스는 JtaTransactionManager 의 서브클래스이다. 이 클래스는 WebLogic 관리 트랜잭션 환경에서 표준 JTA 를 넘어서 스프링 트랜잭션 정의의 모든 기능을 지원한다. 지원되는 기능은 트랜잭션 이름, 트랜잭션 별 격리 수준, 그리고 모든 경우의 트랜잭션에서의 알맞은 재개를 포함한다.



일반적인 문제의 해결책


특정 DataSource 에 대한 잘못된 트랜잭션 관리자 사용


선택한 트랜잭션 기술과 요건에 맞는 알맞은 PlatformTransactionManager 구현체를 사용하라. 제대로 사용된 스프링 프레임워크는 간단하고 포터블한 추상화를 제공한다. 글로벌 트랜잭션을 사용한다면 반드시 org.springframework.transaction.jta.JtaTransactionManager 클래스 (또는 이 클래스의 어플리케이션 서버 별 서브클래스) 를 사용하라. 그렇지 않으면 트랜잭션 인프라는 컨테이너의 DataSource 인스턴스와 같은 리소스에 대해 로컬 트랜잭션을 수행한다. 이런 로컬 트랜잭션은 의미가 없으며, 좋은 어플리케이션 서버는 이를 오류로 처리한다.



추가 정보


스프링 프레임워크의 트랜잭션 지원에 대한 정보는 여기서 더 얻을 수 있다.


  • 스프링의 분산 트랜잭션 with or without XA 는 JavaWorld 에 기재된, 스프링의 David Syer 의 스프링 어플리케이션의 7 가지 분산 트랜잭션 패턴 가이드이다. 패턴 중 3 가지는 XA 를 사용하고, 4 가지는 사용하지 않는다.
  • 자바 트랜잭션 디자인 전략 은 InfoQ 에서 판매중인 책으로, 자바 트랜잭션에 대해 훌륭하게 소개한다. 또한 스프링 프레임워크과 EJB3 를 사용한 트랜잭션 설정 방법 예제도 포함되어 있다.






이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/amdahls-law.html

!! 쉬운 이해를 위한 의역이 다소 포함되었다.



암달의 법칙은 연산의 병행 처리로 연산 속도를 얼마나 올릴 수 있는지 계산하는 데 쓰일 수 있다. 암달의 법칙은 1967년 이 이론을 발표한 Gene Amdahl 의 이름을 따서 명명되었다. 병행 또는 동시성 시스템으로 작업하는 대부분의 개발자들은 암달의 법칙을 알지 못하더라도 잠재적인 속도 향상에 대한 직관적인 느낌을 가진다. 어쨌든 암달의 법칙은 여전히 알아둘 가치가 있다.


먼저 수학적인 설명 후 다이어그램을 통해 암달의 법칙을 보여주도록 하겠다.



암달의 법칙 정의


병렬화 가능한 프로그램은 두 부분으로 나뉜다.


  • 병렬화 불가능한 부분
  • 병렬화 가능한 부분


디스크의 파일을 처리하는 프로그램이 있고 가정하자. 프로그램에는 디렉토리를 스캔하여 내부 메모리에 파일의 목록을 생성하는 부분이 있다. 각 파일 정보는 파일을 처리하는 쓰레드로 전달된다. 디렉토리를 스캔하여 파일 목록을 생성하는 부분은 병렬화 불가능한 부분이지만 파일을 처리하는 부분은 병렬화 가능한 부분이다.


순차적으로 실행되는 프로그램의 전체 수행 시간을 T 라고 한다. T 에는 병렬화 가능한 부분과 병렬화 불가능한 부분의 수행 시간이 모두 포함되어 있다. 병렬화 불가능한 부분은 B 라고 한다. 이제 병렬화 가능한 부분은  T - B 라 할 수 있다. 요약하자면 다음과 같다.


  • T = 순차적 실행의 전체 수행시간
  • B = 병렬화 불가능한 부분 실행의 전체 수행시간
  • T - B = 병렬화 가능한 부분의 전체 수행시간 (순차적으로 실행되었을 경우)


그리고 다음과 같이 표현된다.


T = B + (T-B)


병렬화 가능한 부분에 대한 부호는 없다는 점에서 위 식은 어딘가 이상해 보이기도 한다. 그러나 식에서 병렬화 가능한 부분은 T 와 B 로 표현될 수 있고, 부호의 수를 줄이면서 식은 개념적으로 축소되었다. 


병렬화 가능한 부분을 나타내는 T - B 는 프로그램을 병렬로 실행했을 때 기대할 수 있는 속도 상승 값이다. 속도가 얼마나 향상되는가는 쓰레드나 CPU 를 얼마나 사용하느냐에 달려있다. 쓰레드 또는 CPU 의 수를 N 이라 한다. 병렬화 가능한 부분의 최대 실행 속도는 다음과 같이 계산된다.


(T - B) / N


다르게 쓸 수도 있다.


(1/N) * (T - B)


위키피디아의 암달의 법칙 문서에는 위 식이 등장한다.


암달의 법칙에 의하면, 프로그램의 병렬화 가능한 부분이 N 개의 쓰레드 혹은 CPU 를 사용하여 실행될 경우 전체 수행시간은 다음과 같이 계산된다.


T(N) = B + (T - B) / N


T(N) 은 병렬 처리의 수와 전체 수행시간을 의미한다. 따라서 T 는 T(1) 으로 표현될 수 있다. 병렬 처리 1 에 대한 프로그램 전체 수행시간이다. T(1) 을 사용하면 암달의 법칙은 다음과 같이 표현된다.


T(N) = B + ( T(1) - B ) / N


의미는 여전히 같다.



계산 예제


암달의 법칙을 보다 잘 이해하기 위해 계산 예제를 하나 보도록 한다. 프로그램 전체 수행 시간은 1 으로 설정한다. 병렬화 불가능한 부분은 프로그램의 40% 을 자치한다. 수행 시간으로는 0.4 이다. 병렬화 가능한 부분은 1 - 0.4 = 0.6 이 된다.


병렬 처리의 수를 2 라고 하면(2 쓰레드 혹은 CPU) 수행 시간은 다음과 같다.


T(2) = 0.4 + ( 1 - 0.4 ) / 2
     = 0.4 + 0.6 / 2
     = 0.4 + 0.3
     = 0.7


같은 식으로 인자를 5 로 바꾸면 다음과 같다.


T(5) = 0.4 + ( 1 - 0.4 ) / 5
     = 0.4 + 0.6 / 5
     = 0.4 + 0.12
     = 0.52



그림으로 보는 암달의 법칙


쉬운 이해를 위해 암달의 법칙을 그림으로 설명해본다.


먼저, 프로그램의 병렬화 가능하지 않은 부분을 B 로 두고, 가능한 부분을 1-B 로 둔다.


Amdahls law illustrated.


상단의 선은 프로그램 전체 수행시간 T(1) 이다.


병렬 처리의 수가 2 라면 수행 시간을 다음과 같이 표현할 수 있다.


Amdahls law illustrated with a parallelization factor of 2.


병렬 처리의 수가 3 이라면 다음과 같다.


Amdahls law illustrated with a parallelization factor of 3.



알고리즘 최적화


암달의 법칙에 의하면 병렬화 가능한 부분은 하드웨어에 맡김으로써 더 빠르게 실행될 수 있다. 더 많은 수의 쓰레드/CPU 를 사용하는 것이다. 하지만 병렬화 가능하지 않는 부분의 속도 향상은 코드 최적화를 통해서 이루어질 수 있다. 따라서 병렬화 가능하지 않은 부분의 코드를 최적화 함으로써 프로그램의 속도와 병렬성을 높일 수 있다. 가능하다면 병렬화 가능하지 않는 부분의 처리를 병렬화 가능한 부분으로 옮겨서 병렬화 가능하지 않은 부분을 더 작게 만들 수도 있다.



순차적 수행의 최적화


프로그램에서 순차적으로 수행되는 부분을 최적화한다면, 최적화 후 암달의 법칙을 사용햐ㅐ 프로그램의 실행 시간을 계산해낼 수 있다. 병렬화 가능하지 않은 부분 B 와 병렬 처리의 수 O 으로 계산한다면암달의 법칙은 다음과 같다.


T(O,N) = B / O + (1 - B / O) / N


병렬화 가능하지 않은 부분의 실행은 이제 B / O 만큼의 시간을 소모한다. 고로 병렬화 가능한 부분의 실행 시간은 1 - B / O 가 된다.


B 가 0.4 이고 O 가 2, N 은 5 라면 계산은 다음과 같다.


T(2,5) = 0.4 / 2 + (1 - 0.4 / 2) / 5
       = 0.2 + (1 - 0.4 / 2) / 5
       = 0.2 + (1 - 0.2) / 5
       = 0.2 + 0.8 / 5
       = 0.2 + 0.16
       = 0.36



실행 시간 vs 향상된 속도


지금까지는 암달의 법칙을 프로그램의 최적화 또는 병렬화 후 실행 시간을 계산하는 데에 사용해왔다. 암달의 법칙은 향상된 속도, 즉 새로운 알고리즘이나프로그램이 이전 버전보다 얼마나 빨라졌는지를 계산하는 데에도 사용될 수 있다.


올드 버전의 실행 시간을 T 라고 하면, 향상된 속도는 다음과 같다.


Speedup = T / T(O,N)


실행 시간이나 향상된 속도를 계산할 때 T 은 주로 1 로 설정한다. 


Speedup = 1 / T(O,N)


T(O,N) 에 암달의 법칙을 넣으면 식은 다음과 같이 된다.


Speedup = 1 / ( B / O + (1 - B / O) / N )


B = 0.4, O = 2, N = 5 라면 계산은 다음과 같이 된다.


Speedup = 1 / ( 0.4 / 2 + (1 - 0.4 / 2) / 5)
        = 1 / ( 0.2 + (1 - 0.4 / 2) / 5)
        = 1 / ( 0.2 + (1 - 0.2) / 5 )
        = 1 / ( 0.2 + 0.8 / 5 )
        = 1 / ( 0.2 + 0.16 )
        = 1 / 0.36
        = 2.77777 ...


이 계산이 의미하는 것은, 병렬화 가능하지 않은 부분(순차적 실행)을 인자 2 로 최적화하고 병렬화 가능한 부분을 인자 5 로 병렬화하면 프로그램의 실행은 최대 2.77777 배로 빨라진다는 것이다.



계산에 그치지 말고 측정하라


암달의 법칙을 통해 프로그램의 향상된 속도를 계산해낼 수 있기는 하지만, 이 계산에 지나치게 의존해선 안된다. 실제로 알고리즘을 최적화 또는 병렬화 할 때는 다른 많은 요인들이 작용할 수 있다.


메모리의 속도, CPU 캐시 메모리, 디스크, 네트워크 카드 기타 등등의 것들이 그 제한 요인이 된다. 만약 새 알고리즘이 병렬화 되었으나 CPU 캐시 미스를 더 많이 일으킨다면 CPU 의 수 만큼의 기대했던 속도 향상을 얻을 수 없다. 알고리즘이 메모리 버스나 디스크 혹은 네트워크 카드 또는 커넥션을 지나치게 사용하게 된다면 역시 같은 결과가 발생한다.


암달의 법칙은 프로그램의 어디를 최적화 할 것인지에 대한 아이디어를 얻기 위해 사용하고, 실제로 얼마만큼의 최적화 속도 향상이 있을지는 측정을 통해 알아보아야 한다. 종종 높은 순차적 실행 알고리즘(단일 CPU)은 병렬 알고리즘보다 나은 성능을 보인다. 이유는 간단하다. 순차적 실행은 동시 실행에 따르는 (작업을 중단하고 다시 시작하는 등의)오버헤드가 없고, 단일 CPU 알고리즘은 하드웨어의 작업에 에 더 친화적일 수 있다. (CPU 파이프라인, CPU 캐시 등)







+ Recent posts