Spring MVC 3.2부터 Servlet 3.0 비동기 요청 처리와 광범위하게 통합되었다.
DefferedResult와 Callable은 Controller 메소드에서 하나의 비동기 리턴 값을 반환한다.
Servlet 비동기 요청 처리는 다음과 같이 진행된다.
ServletRequest는 request.startAsync()를 호출하여 비동기 모드로 전환 할 수 있다. 이렇게 하면 주요 효과는 Servlet 및 모든 필터가 종료가 되어도, 나중에 응답 처리를 완료할 수 있다는 것이다.
request.startAsync()의 호출은 AsyncContext를 반환하게 되는데, 이는 비동기 처리에 대한 추가 제어에 사용할 수 있다. 예를 들어, AsynContext는Servlet API의 forward 요청과 유사하지만 추가로 Servlet 컨테이너 스레드에 애플리케이션에 요청 처리를 재개할 수 있는 dispatch 메소드를 제공한다.
ServletRequest는 현재 DispatcherType에 접근할 수 있게 하고, 이는 초기 요청인지, 비동기 요청인지, forward 요청인지, 기타 dispatch 요청인지 구분할 수 있게 해준다.
DefferedResult는 다음과 같이 작동한다.
컨트롤러는 DefferedResult를 반환하고 이를 메모리 내 큐나 리스트에 저장해 나중에 접근할 수 있도록 한다.
Spring MVC는 request.startAsync()를 호출한다.
그동안 DispatcherServlet과 모든 필터는 쓰레드를 종료하지만 응답은 아직 처리되지 않았다.
애플리케이션은 DeferredResult를 다른 쓰레드에서 처리하게 하고 스프링 MVC는 서블릿 컨테이너에 다시 요청을 디스패치한다.
Dispatcherservlet는 다시 호출되고, 비동기 리턴값의 처리를 다시 재개한다.
Callable 처리는 다음과 같이 작동한다.
컨트롤러는 Callable을 반환한다.
Spring MVC는 request.startAsync()을 호출하고 별도의 쓰레드에서 처리하기 위해 Callable을 내부 TaskExecutor에 전송한다.
그동안 DispatcherServlet과 모든 필터는 쓰레드를 종료하지만 응답은 아직 처리되지 않았다.
Callable이 리턴값을 생성하고, Spring MVC가 서블릿 컨테이너에 처리를 완료하도록 서블릿 컨테이너로 다시 디스패치한다.
Dispatcherservlet는 다시 호출되고, Callable리 리턴한 비동기 리턴값의 처리를 다시 재개한다.
둘의 차이점은 DefferedResult는 스프링 MVC 외의 쓰레드에서 비동기 처리를 하는 것이고, Callable은 스프링 MVC의 스레드에서 비동기 처리를 하는 것이다.
서블릿 컨테이너에서 비동기 요청 처리 기능이 활성화되면, 컨트롤러의 메서드에서는 DefferedResult를 다음과 같이 반환할 수 있다. 위에서 DefferedResult는 스프링 MVC 외의 쓰레드에서 비동기 처리를 하는 거라고 언급했는데 예를 들어 JMS 메시지나 scheduled task 등과 같은 외부의 이벤트로 부터 생성된 Response를 반환해야 하는 경우를 말한다.
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// Save the deferredResult somewhere..
return deferredResult;
}
// From some other thread...
deferredResult.setResult(result);
이 프로세스에서는 값을 바로 반환하지 않고 컨트롤러가 java.util.concurrent.Callable을 먼저 반환하고 내부 TakeExecutor가 Callable 작업을 실행한 후 작업이 종료되면 리턴값을 반환해 다시 요청이 서블릿 컨테이너로 디스패치 되는 것이다.
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public String call() throws Exception {
// ...
return "someView";
}
};
}
setResult나 setErrorResult를 호출해서 예외를 처리할 수 있다. 두가지 모두 Spring MVC가 다시 서블릿 컨테이너에 처리를 완료하도록 디스패치한다. 또한 이는 마치 컨트롤러 메서드가 반환값을 생성하거나, 예외를 생성한 것처럼 보이게 한다. 예외는 일반적인 @ExceptionHandler와 같은 예외 처리 로직을 거치게 된다.
DeferredResult와 비슷하지만, 가장 큰 차이점은 Callable이 결과를 리턴하거나, 예외를 발생시킨다는 것이다.
비동기 요청은 다음과 같은 Interceptor를 등록해서 사용할 수 있다.
DerredResult와 Callable이 하나의 요청에 하나의 응답만 가능한 반면, HttpStreaming을 사용하면 SSE 및 원시 데이터를 포함한 여러 비동기 리턴 값을 반환할 수 있다.
다음 예제와 같이 ResponseBodyEmitter를 반환하여 객체 스트림을 생성할 수 있다. 여기서 각 객체는 HttpMessageConverter에 의해 직렬화되어 응답에 추가된다.
@GetMapping("/events")
public ResponseBodyEmitter handle() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();
또한 ResponseEntity에서 응답 본문으로 ResponseBodyEmitter를 사용하거나 상태 코드와 헤더를 커스터마이징할 수 있다.
만약 ResponseBodyEmitter가 IOException을 던지면, 애플리케이션은 연결을 해제하거나 emitter.complete, emitter.completeWithError를 호출할 책임이 없다. 대신, 서블릿 컨테이너가 자동으로 AsyncListener error notification를 초기화하여 스프링 MVC가 completeWithError를 호출하게 한다. 결국 이 호출은 최종 비동기 요청처리는 어플리케이션에 dispatch해 Spring MVC가 ExceptionResolver를 호출하거나 요청을 완료하도록 한다.
SseEmitter(Server Send Event, ResponseBodyEmitter의 하위클래스)는 Server-Sent Events 데이터 스트림을 포함한 비동기 리턴값을 반환할 수 있게 해준다.
@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
SseEmitter emitter = new SseEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();
때로는 파일 다운로드의 경우와 같이 바이트단위 입출력을 하는 경우, Message Conversion 과정을 수행하지 않고 바로 OutputStream이 처리하도록 데이터 스트림을 반환하는 경우가 있다. 이 경우에는 StreamingResponseBody를 사용한다.
@GetMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// write...
}
};
}
또한 ResponseEntity에서 응답 본문으로 StreamingResponseBody를 사용하거나 상태 코드와 헤더를 커스터마이징할 수 있다.
@WebServlet(urlPatterns = {"/async"}, asyncSupported = true)
public class CourtWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext ctx) throws ServletException {
DispatcherServlet servlet = new DispatcherServlet();
ServletRegistration.Dynamic registration = ctx.addServlet("dispatcher", servlet);
registration.setAsyncSupported(true);
}
}
서블릿을 초기화하기 위해 AbstractAnnotationConfigDispatcherServletInitializer을 사용하면 자동으로 활성화된다.
web.xml에서 DispatcherServlet이나 필터에 <async-supported>true</async-supported> 추가. 또한 필터 매핑에 <dispatcher>ASYNC</dispatcher> 설정
<servlet>
<!-- ASYNC -->
<async-supported>true</async-supported>
</servlet>
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!-- ASYNC -->
<async-supported>true</async-supported>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<!-- ASYNC -->
<dispatcher>ASYNC</dispatcher>
</filter-mapping>
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMVCConfigurer {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(5000);
}
@Bean
public AsyncTaskExecutor mvcTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setThreadGroupName("mvc-executor");
return taskExecutor;
}
}
참고