Loading...
Spring Framework Reference Documentation 7.0.2의 Asynchronous Requests의 한국어 번역본입니다.
아래의 경우에 피드백에서 신고해주신다면 반영하겠습니다.
감사합니다 :)
Spring MVC는 Servlet 비동기 요청 processing과 광범위하게 통합되어 있습니다:
controller 메서드에서의 반환 값으로
DeferredResult,
Callable,
WebAsyncTask를 사용하면 단일 비동기 반환 값을 지원합니다.
Controller는 reactive 클라이언트를 사용하고 response handling을 위해 reactive types를 반환할 수 있습니다.
이것이 Spring WebFlux와 어떻게 다른지 개요는 아래의 Async Spring MVC compared to WebFlux 섹션을 참고하십시오.
DeferredResultServlet 컨테이너에서 비동기 요청 처리 기능이
enabled되면,
controller 메서드는 다음 예제와 같이 지원되는 어떤 controller 메서드 반환 값이든지
DeferredResult로 감쌀 수 있습니다:
1@GetMapping("/quotes") 2@ResponseBody 3public DeferredResult<String> quotes() { 4 DeferredResult<String> deferredResult = new DeferredResult<>(); 5 // Save the deferredResult somewhere.. 6 return deferredResult; 7} 8 9// From some other thread... 10deferredResult.setResult(result);
1@GetMapping("/quotes") 2@ResponseBody 3fun quotes(): DeferredResult<String> { 4 val deferredResult = DeferredResult<String>() 5 // Save the deferredResult somewhere.. 6 return deferredResult 7} 8 9// From some other thread... 10deferredResult.setResult(result)
controller는 다른 thread에서 비동기적으로 반환 값을 생성할 수 있습니다. 예를 들어 외부 event(JMS message), scheduled task 또는 다른 event에 대한 응답으로 생성할 수 있습니다.
Callable다음 예제와 같이 controller는 어떤 지원되는 반환 값이든지
java.util.concurrent.Callable로 감쌀 수 있습니다:
1@PostMapping 2public Callable<String> processUpload(final MultipartFile file) { 3 return () -> "someView"; 4}
1@PostMapping 2fun processUpload(file: MultipartFile) = Callable<String> { 3 // ... 4 "someView" 5}
그런 다음 반환 값은
configured AsyncTaskExecutor를 통해 주어진 task를 실행하여 얻을 수 있습니다.
WebAsyncTaskWebAsyncTask는
Callable을 사용하는 것과 비슷하지만,
request timeout 값과 같은 추가 설정을 customizing하고
java.util.concurrent.Callable을 실행할 AsyncTaskExecutor를 Spring MVC에 대해 전역으로 설정된 기본값 대신 지정할 수 있게 해줍니다.
아래는 WebAsyncTask를 사용하는 예제입니다:
1@GetMapping("/callable") 2WebAsyncTask<String> handle() { 3 return new WebAsyncTask<String>(20000L, () -> { 4 Thread.sleep(10000); //simulate long-running task 5 return "asynchronous request completed"; 6 }); 7}
1@GetMapping("/callable") 2fun handle(): WebAsyncTask<String> { 3 return WebAsyncTask(20000L) { 4 Thread.sleep(10000) // simulate long-running task 5 "asynchronous request completed" 6 } 7}
다음은 Servlet 비동기 요청 처리에 대한 매우 간결한 개요입니다:
ServletRequest는 request.startAsync()를 호출하여 비동기 모드로 전환할 수 있습니다.
이렇게 하는 주된 효과는 Servlet(및 어떤 filter든지)이 종료될 수 있지만,
response는 나중에 처리 완료를 위해 열린 상태로 남는다는 것입니다.
request.startAsync() 호출은 비동기 처리에 대한 추가 제어에 사용할 수 있는 AsyncContext를 반환합니다.
예를 들어, Servlet 컨테이너 thread에서 애플리케이션이 request processing을 재개할 수 있게 해주는,
Servlet API의 forward와 유사한 dispatch 메서드를 제공합니다.
ServletRequest는 현재 DispatcherType에 대한 access를 제공하며,
이를 사용하여 initial request 처리, 비동기 dispatch, forward 및 다른 dispatcher type 간을 구분할 수 있습니다.
DeferredResult processing은 다음과 같이 동작합니다:
controller는 DeferredResult를 반환하고, 이를 access할 수 있는 in-memory queue 또는 list 어딘가에 저장합니다.
Spring MVC는 request.startAsync()를 호출합니다.
그동안 DispatcherServlet과 구성된 모든 filter는 request processing thread에서 종료되지만,
response는 열린 상태로 남습니다.
애플리케이션이 어떤 thread에서 DeferredResult를 설정하면 Spring MVC는 request를 Servlet 컨테이너로 다시 dispatch합니다.
DispatcherServlet이 다시 호출되고, 비동기적으로 생성된 반환 값으로 processing이 재개됩니다.
Callable processing은 다음과 같이 동작합니다:
controller는 Callable을 반환합니다.
Spring MVC는 request.startAsync()를 호출하고, 별도의 thread에서 처리하기 위해 Callable을
AsyncTaskExecutor에 제출합니다.
그동안 DispatcherServlet과 모든 filter는 Servlet 컨테이너 thread에서 종료되지만,
response는 열린 상태로 남습니다.
결국 Callable이 result를 생성하고, Spring MVC는 request를 Servlet 컨테이너로 다시 dispatch하여
processing을 완료합니다.
DispatcherServlet이 다시 호출되고, Callable에서 비동기적으로 생성된 반환 값으로
processing이 재개됩니다.
배경 및 context에 대해서는 Spring MVC 3.2에서 비동기 요청 처리 지원을 도입한 the blog posts도 읽어볼 수 있습니다.
DeferredResult를 사용할 때, setResult를 호출할지 아니면 exception과 함께 setErrorResult를 호출할지 선택할 수 있습니다.
두 경우 모두 Spring MVC는 request를 Servlet 컨테이너로 다시 dispatch하여 processing을 완료합니다.
그런 다음 controller 메서드가 주어진 값을 반환한 것처럼 또는 주어진 exception을 발생시킨 것처럼 처리됩니다.
exception은 이후 regular exception handling mechanism(예: @ExceptionHandler 메서드 호출)을 거치게 됩니다.
Callable을 사용할 때도 유사한 processing logic이 발생하며,
주된 차이점은 result가 Callable에서 반환되거나 그 안에서 exception이 발생한다는 점입니다.
HandlerInterceptor instance는 AsyncHandlerInterceptor type일 수 있으며,
비동기 처리를 시작하는 initial request에서
(postHandle 및 afterCompletion 대신) afterConcurrentHandlingStarted callback을 받을 수 있습니다.
HandlerInterceptor 구현은 또한 CallableProcessingInterceptor 또는
DeferredResultProcessingInterceptor를 등록하여,
비동기 요청의 lifecycle(예: timeout event 처리)을 보다 깊이 통합할 수 있습니다.
자세한 내용은
AsyncHandlerInterceptor를 참조하십시오.
DeferredResult는 onTimeout(Runnable) 및 onCompletion(Runnable) callback을 제공합니다.
자세한 내용은
javadoc of DeferredResult를 참조하십시오.
Callable은 timeout 및 completion callback을 위한 추가 메서드를 노출하는 WebAsyncTask로 대체될 수 있습니다.
Servlet API는 원래 Filter-Servlet chain을 한 번만 통과하도록 설계되었습니다. 비동기 요청 처리는 애플리케이션이 Filter-Servlet chain을 빠져나가되, 추가 processing을 위해 response를 열린 상태로 둘 수 있게 해줍니다. Spring MVC 비동기 지원은 이 메커니즘을 기반으로 구축되었습니다.
controller가 DeferredResult를 반환하면, Filter-Servlet chain이 종료되고 Servlet 컨테이너 thread가 해제됩니다.
나중에 DeferredResult가 설정되면, 동일한 URL로 ASYNC dispatch가 이루어지고,
이 동안 controller가 다시 매핑되지만, controller를 호출하는 대신
(controller가 이를 반환한 것처럼) DeferredResult value가 사용되어 processing이 재개됩니다.
반면 Spring WebFlux는 Servlet API 위에 구축되지 않았으며, 설계상 비동기이기 때문에 이러한 비동기 요청 처리 기능을 필요로 하지 않습니다. 비동기 handling은 모든 framework contract에 내장되어 있으며, request processing의 모든 단계에서 본질적으로 지원됩니다.
Programming model 관점에서 Spring MVC와 Spring WebFlux는 모두 controller 메서드의 반환 값으로 비동기 및 Reactive Types를 지원합니다. Spring MVC는 reactive back pressure를 포함한 streaming도 지원합니다. 그러나 WebFlux와 달리 response에 대한 개별 write는 여전히 blocking이며 (별도의 thread에서 수행됩니다). WebFlux는 non-blocking I/O에 의존하며 각 write마다 추가 thread를 필요로 하지 않습니다.
또 다른 근본적인 차이점은 Spring MVC는 controller 메서드 argument(예: @RequestBody,
@RequestPart 및 기타)에서 비동기 또는 reactive type을 지원하지 않으며,
model attribute로서 비동기 및 reactive type에 대한 명시적인 지원도 없다는 점입니다.
Spring WebFlux는 이러한 모든 것을 지원합니다.
마지막으로 configuration 관점에서 비동기 요청 처리 기능은 Servlet container level에서 enabled되어야 합니다.
See equivalent in the Reactive stack
DeferredResult와 Callable을 사용하여 단일 비동기 반환 값을 만들 수 있습니다.
여러 개의 비동기 value를 생성하고 이를 response에 write하고 싶다면 어떻게 해야 할까요?
이 섹션에서는 그 방법을 설명합니다.
ResponseBodyEmitter 반환 값을 사용하여 object stream을 생성할 수 있습니다.
각 object는
HttpMessageConverter에 의해 직렬화되고,
다음 예제와 같이 response에 write됩니다:
1@GetMapping("/events") 2public ResponseBodyEmitter handle() { 3 ResponseBodyEmitter emitter = new ResponseBodyEmitter(); 4 // Save the emitter somewhere.. 5 return emitter; 6} 7 8// In some other thread 9emitter.send("Hello once"); 10 11// and again later on 12emitter.send("Hello again"); 13 14// and done at some point 15emitter.complete();
1@GetMapping("/events") 2fun handle() = ResponseBodyEmitter().apply { 3 // Save the emitter somewhere.. 4} 5 6// In some other thread 7emitter.send("Hello once") 8 9// and again later on 10emitter.send("Hello again") 11 12// and done at some point 13emitter.complete()
또한 ResponseBodyEmitter를 ResponseEntity의 body로 사용하여
response의 status와 header를 customize할 수 있습니다.
emitter가 IOException을 발생시키는 경우(예: remote 클라이언트가 사라진 경우),
애플리케이션은 connection을 정리할 책임이 없으며 emitter.complete 또는
emitter.completeWithError를 호출해서는 안 됩니다.
대신 servlet 컨테이너는 자동으로 AsyncListener error notification을 시작하고,
이때 Spring MVC는 completeWithError 호출을 수행합니다.
이 호출은 차례로 애플리케이션으로 마지막 ASYNC dispatch를 수행하며,
이 동안 Spring MVC는 구성된 exception resolver를 호출하고 request를 완료합니다.
SseEmitter(ResponseBodyEmitter의 subclass)는
server에서 전송된 event가 W3C SSE specification에 따라 포맷되는
Server-Sent Events를 지원합니다.
controller에서 SSE stream을 생성하려면, 다음 예제와 같이 SseEmitter를 반환하면 됩니다:
1@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE) 2public SseEmitter handle() { 3 SseEmitter emitter = new SseEmitter(); 4 // Save the emitter somewhere.. 5 return emitter; 6} 7 8// In some other thread 9emitter.send("Hello once"); 10 11// and again later on 12emitter.send("Hello again"); 13 14// and done at some point 15emitter.complete();
1@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) 2fun handle() = SseEmitter().apply { 3 // Save the emitter somewhere.. 4} 5 6// In some other thread 7emitter.send("Hello once") 8 9// and again later on 10emitter.send("Hello again") 11 12// and done at some point 13emitter.complete()
SSE는 browser로 streaming하기 위한 주요 옵션이지만, Internet Explorer는 Server-Sent Events를 지원하지 않는다는 점에 유의하십시오. Spring의 WebSocket messaging과 넓은 범위의 browser를 대상으로 하는 SockJS fallback transport(SSE 포함)를 사용하는 것을 고려하십시오.
Exception handling에 대한 참고 사항은 previous section을 참조하십시오.
때때로 message conversion을 우회하고 response OutputStream에 직접 stream하는 것이 유용할 수 있습니다
(예: file download의 경우). 다음 예제와 같이 StreamingResponseBody
반환 값 type을 사용하면 됩니다:
1@GetMapping("/download") 2public StreamingResponseBody handle() { 3 return new StreamingResponseBody() { 4 @Override 5 public void writeTo(OutputStream outputStream) throws IOException { 6 // write... 7 } 8 }; 9}
1@GetMapping("/download") 2fun handle() = StreamingResponseBody { 3 // write... 4}
StreamingResponseBody를 ResponseEntity의 body로 사용하여
response의 status와 header를 customize할 수 있습니다.
See equivalent in the Reactive stack
Spring MVC는 controller에서 reactive 클라이언트 라이브러리 사용을 지원합니다
(WebFlux 섹션의 Reactive Libraries도 참고).
여기에는 spring-webflux의 WebClient 및 Spring Data reactive data repository와 같은 것들이 포함됩니다.
이러한 시나리오에서는 controller 메서드에서 reactive type을 반환할 수 있으면 편리합니다.
Reactive 반환 값은 다음과 같이 처리됩니다:
단일 값 promise는 DeferredResult를 사용하는 것과 유사하게 adapt됩니다.
예로는 CompletionStage(JDK), Mono(Reactor), Single(RxJava)가 있습니다.
streaming media type(예: application/x-ndjson 또는 text/event-stream)을 사용하는
multi-value stream은 ResponseBodyEmitter 또는 SseEmitter를 사용하는 것과 유사하게 adapt됩니다.
예로는 Flux(Reactor) 또는 Observable(RxJava)이 있습니다.
애플리케이션은 Flux<ServerSentEvent> 또는 Observable<ServerSentEvent>를 반환할 수도 있습니다.
다른 media type(예: application/json)을 사용하는 multi-value stream은
DeferredResult<List<?>>를 사용하는 것과 유사하게 adapt됩니다.
Spring MVC는
spring-core의ReactiveAdapterRegistry를 통해 Reactor 및 RxJava를 지원하며, 이를 통해 여러 reactive 라이브러리에서 adapt할 수 있습니다.
response로 streaming할 때 reactive back pressure는 지원되지만,
response에 대한 write는 여전히 blocking이며,
Flux가 WebClient에서 반환된 것과 같은 upstream source를 blocking하지 않기 위해
configured AsyncTaskExecutor를 통해 별도의 thread에서 실행됩니다.
java.lang.ThreadLocal을 통해 context를 propagate하는 것은 일반적입니다.
이는 동일한 thread에서의 handling에는 투명하게 동작하지만,
여러 thread에 걸친 비동기 handling에는 추가 작업이 필요합니다.
Micrometer
Context Propagation
라이브러리는 ThreadLocal 값,
Reactor context,
GraphQL Java context 및 기타
context mechanism 전반에 걸쳐 thread 간 context propagation을 단순화합니다.
Micrometer Context Propagation이 classpath에 존재하는 경우,
controller 메서드가 Flux 또는 Mono와 같은
reactive type을 반환하면,
등록된 io.micrometer.ThreadLocalAccessor가 있는 모든 ThreadLocal 값이
ThreadLocalAccessor에 의해 할당된 key를 사용하여 key-value pair 형태로 Reactor Context에 write됩니다.
다른 비동기 handling 시나리오에서는 Context Propagation 라이브러리를 직접 사용할 수 있습니다. 예를 들어:
Java
1// Capture ThreadLocal values from the main thread ... 2ContextSnapshot snapshot = ContextSnapshot.captureAll(); 3 4// On a different thread: restore ThreadLocal values 5try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) { 6 // ... 7}
다음 ThreadLocalAccessor 구현이 기본으로 제공됩니다:
LocaleContextThreadLocalAccessor — LocaleContextHolder를 통해 LocaleContext를 propagate합니다.RequestAttributesThreadLocalAccessor — RequestContextHolder를 통해 RequestAttributes를 propagate합니다.위의 것들은 자동으로 등록되지 않습니다.
startup 시 ContextRegistry.getInstance()를 통해 등록해야 합니다.
자세한 내용은 Micrometer Context Propagation 라이브러리의 documentation을 참조하십시오.
See equivalent in the Reactive stack
Servlet API는 remote 클라이언트가 사라졌을 때 어떤 notification도 제공하지 않습니다. 따라서 SseEmitter나 reactive types를 통해 response로 streaming하는 동안에는 클라이언트가 disconnect되었을 경우 write가 실패하므로 주기적으로 data를 전송하는 것이 중요합니다. 전송은 빈(주석만 있는) SSE event 형식이거나, 반대편에서 heartbeat로 해석하고 무시해야 하는 다른 어떤 data도 될 수 있습니다.
또는 built-in heartbeat 메커니즘을 가진 web messaging solution (예: STOMP over WebSocket 또는 SockJS가 있는 WebSocket)을 사용하는 것을 고려하십시오.
비동기 요청 처리 기능은 Servlet 컨테이너 level에서 enabled되어야 합니다. MVC configuration은 또한 비동기 요청에 대한 여러 옵션을 노출합니다.
Filter 및 Servlet 선언에는 비동기 요청 처리를 enable하기 위해 true로 설정해야 하는
asyncSupported flag가 있습니다.
또한 Filter mapping은 ASYNC jakarta.servlet.DispatchType을 처리하도록 선언되어야 합니다.
Java configuration에서 Servlet 컨테이너를 초기화하기 위해
AbstractAnnotationConfigDispatcherServletInitializer를 사용할 때는,
이 작업이 자동으로 수행됩니다.
web.xml configuration에서는 <async-supported>true</async-supported>를
DispatcherServlet과 Filter 선언에 추가하고,
filter mapping에 <dispatcher>ASYNC</dispatcher>를 추가할 수 있습니다.
MVC configuration은 비동기 요청 처리를 위해 다음 옵션을 노출합니다:
WebMvcConfigurer의 configureAsyncSupport callback을 사용합니다.<mvc:annotation-driven> 아래의 <async-support> element를 사용합니다.다음 항목을 configuration할 수 있습니다:
비동기 request에 대한 기본 timeout 값은 명시적으로 설정하지 않는 한, 기본적으로 underlying Servlet 컨테이너에 따라 달라집니다.
Reactive Types로 streaming할 때의 blocking write와
controller 메서드에서 반환된 Callable instance 실행에 사용할 AsyncTaskExecutor.
기본적으로 사용되는 것은 부하가 있는 production 환경에는 적합하지 않습니다.
DeferredResultProcessingInterceptor 구현 및 CallableProcessingInterceptor 구현.
또한 DeferredResult, ResponseBodyEmitter, SseEmitter에서
기본 timeout 값을 설정할 수 있다는 점에 유의하십시오.
Callable의 경우, timeout 값을 제공하기 위해 WebAsyncTask를 사용할 수 있습니다.
URI Links
Range Requests