Asynchronous Requests

Dev.Hammy·2024년 4월 7일
0

Spring MVC는 Servlet 비동기 요청 처리와 광범위하게 통합되어 있습니다.

  • 컨트롤러 메서드의 DeferredResultCallable 반환 값은 단일 비동기 반환 값에 대한 기본 지원을 제공합니다.

  • 컨트롤러는 SSE원시 데이터를 포함한 여러 값을 스트리밍할 수 있습니다.

  • 컨트롤러는 반응 클라이언트를 사용하고 응답 처리를 위해 반응 type을 반환할 수 있습니다.

이것이 Spring WebFlux와 어떻게 다른지에 대한 개요는 아래의 WebFlux와 비교한 Async Spring MVC 섹션을 참조하세요.

DeferredResult

서블릿 컨테이너에서 비동기 요청 처리 기능이 활성화되면 컨트롤러 메서드는 다음 예제와 같이 지원되는 컨트롤러 메서드 반환 값을 DeferredResult로 래핑할 수 있습니다.

@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
	DeferredResult<String> deferredResult = new DeferredResult<>();
	// Save the deferredResult somewhere..
	return deferredResult;
}

// From some other thread...
deferredResult.setResult(result);

컨트롤러는 예를 들어 외부 이벤트(JMS 메시지), 예약된 작업 또는 기타 이벤트에 대한 응답으로 다른 스레드에서 비동기적으로 반환 값을 생성할 수 있습니다.

Callable

컨트롤러는 다음 예제와 같이 java.util.concurrent.Callable을 사용하여 지원되는 반환 값을 래핑할 수 있습니다.

@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
	return () -> "someView";
}

그러면 구성된 AsyncTaskExecutor를 통해 지정된 작업을 실행하여 반환 값을 얻을 수 있습니다.

Processing

다음은 서블릿 비동기 요청 처리에 대한 매우 간결한 개요입니다.

  • ServletRequestrequest.startAsync()를 호출하여 비동기 모드로 전환할 수 있습니다. 이렇게 하면 주요 효과는 서블릿(및 모든 필터)이 종료될 수 있지만 나중에 처리가 완료될 수 있도록 응답이 열린 상태로 유지된다는 것입니다.

  • request.startAsync()에 대한 호출은 비동기 처리를 추가로 제어하는 데 사용할 수 있는 AsyncContext를 반환합니다. 예를 들어, 애플리케이션이 서블릿 컨테이너 스레드에서 요청 처리를 재개할 수 있다는 점을 제외하면 서블릿 API의 전달과 유사한 dispatch 메소드를 제공합니다.

  • ServletRequest는 초기 요청 처리, 비동기 디스패치, 전달 및 기타 디스패처 유형을 구별하는 데 사용할 수 있는 현재 DispatcherType에 대한 액세스를 제공합니다.

DeferredResult 처리는 다음과 같이 작동합니다.

  • 컨트롤러는 DeferredResult를 반환하고 이를 액세스할 수 있는 일부 메모리 내 대기열이나 목록에 저장합니다.

  • Spring MVC는 request.startAsync()를 호출합니다.

  • 그 사이에 DispatcherServlet과 구성된 모든 필터는 요청 처리 스레드를 종료하지만 응답은 열린 상태로 유지됩니다.

  • 애플리케이션은 일부 스레드에서 DeferredResult를 설정하고 Spring MVC는 요청을 다시 서블릿 컨테이너로 전달합니다.

  • DispatcherServlet이 다시 호출되고 비동기적으로 생성된 반환 값으로 처리가 재개됩니다.

Callable 처리는 다음과 같이 작동합니다.

  • 컨트롤러는 Callable을 반환합니다.

  • Spring MVC는 request.startAsync()를 호출하고 별도의 스레드에서 처리하기 위해 CallableAsyncTaskExecutor에 제출합니다.

  • 그 사이에 DispatcherServlet과 모든 필터는 서블릿 컨테이너 스레드를 종료하지만 응답은 열린 상태로 유지됩니다.

  • 결국 Callable은 결과를 생성하고 Spring MVC는 처리를 완료하기 위해 요청을 Servlet 컨테이너로 다시 전달합니다.

  • DispatcherServlet이 다시 호출되고 Callable에서 비동기적으로 생성된 반환 값으로 처리가 재개됩니다.

추가 배경 및 컨텍스트를 보려면 Spring MVC 3.2에서 비동기 요청 처리 지원을 소개한 블로그 게시물을 읽어보세요.

Exception Handling

DeferredResult를 사용할 때 예외를 포함하여 setResult 또는 setErrorResult를 호출할지 여부를 선택할 수 있습니다. 두 경우 모두 Spring MVC는 처리를 완료하기 위해 요청을 서블릿 컨테이너로 다시 전달합니다. 그런 다음 컨트롤러 메서드가 지정된 값을 반환한 것처럼 처리되거나 지정된 예외를 생성한 것처럼 처리됩니다. 그런 다음 예외는 일반적인 예외 처리 메커니즘(예: @ExceptionHandler 메서드 호출)을 거칩니다.

Callable을 사용하면 유사한 처리 논리가 발생합니다. 주요 차이점은 결과가 Callable에서 반환되거나 예외가 발생한다는 점입니다.

Interception

HandlerInterceptor 인스턴스는 비동기 처리를 시작하는 초기 요청에서 afterConcurrentHandlingStarted 콜백을 수신하기 위해 AsyncHandlerInterceptor 유형일 수 있습니다(postHandleafterCompletion 대신).

HandlerInterceptor 구현은 CallableProcessingInterceptor 또는 DeferredResultProcessingInterceptor를 등록하여 비동기 요청의 수명 주기와 더 깊이 통합할 수도 있습니다(예: 시간 초과 이벤트 처리). 자세한 내용은 AsyncHandlerInterceptor를 참조하세요.

DeferredResultonTimeout(Runnable)onCompletion(Runnable) 콜백을 제공합니다. 자세한 내용은 DeferredResult의 javadoc을 참조하세요. 제한 시간 및 완료 콜백에 대한 추가 메서드를 노출하는 WebAsyncTaskCallable로 대체할 수 있습니다.

Async Spring MVC compared to WebFlux

Servlet API는 원래 필터-서블릿 체인을 통해 단일 패스를 만들기 위해 구축되었습니다. 비동기식 요청 처리를 통해 애플리케이션은 필터-서블릿 체인을 종료하지만 추가 처리를 위해 응답을 열어 둡니다. Spring MVC 비동기 지원은 해당 메커니즘을 기반으로 구축되었습니다. 컨트롤러가 DeferredResult를 반환하면 필터-서블릿 체인이 종료되고 서블릿 컨테이너 스레드가 해제됩니다. 나중에 DeferredResult가 설정되면 ASYNC 디스패치(동일한 URL로)가 이루어지며, 그 동안 컨트롤러는 다시 매핑되지만 이를 호출하는 대신 DeferredResult 값을 사용하여(컨트롤러가 이를 반환한 것처럼) 처리를 재개합니다. .

이와 대조적으로 Spring WebFlux는 Servlet API를 기반으로 구축되지도 않았고 비동기식으로 설계되었기 때문에 그러한 비동기 요청 처리 기능도 필요하지 않습니다. 비동기 처리는 모든 프레임워크 계약에 내장되어 있으며 요청 처리의 모든 단계를 통해 본질적으로 지원됩니다.

프로그래밍 모델 관점에서 Spring MVC와 Spring WebFlux는 모두 컨트롤러 메서드의 반환 값으로 비동기 및 반응형 type을 지원합니다. Spring MVC는 반응성 역압을 포함한 스트리밍도 지원합니다. 그러나 비차단 I/O에 의존하고 각 쓰기에 추가 스레드가 필요하지 않은 WebFlux와는 달리 응답에 대한 개별 쓰기는 차단 상태로 유지되며 별도의 스레드에서 수행됩니다.

또 다른 근본적인 차이점은 Spring MVC가 컨트롤러 메서드 인수(예: @RequestBody, @RequestPart 등)에서 비동기식 또는 반응성 유형을 지원하지 않으며 모델 속성으로 비동기식 및 반응성 유형에 대한 명시적인 지원도 없다는 점입니다. Spring WebFlux는 이 모든 것을 지원합니다.

마지막으로 구성 관점에서 비동기 요청 처리 기능은 서블릿 컨테이너 수준에서 활성화되어야 합니다.

HTTP Streaming

반응형 스택에서 이에 상응하는 내용 보기

단일 비동기 반환 값에 대해 DeferredResultCallable을 사용할 수 있습니다. 여러 비동기 값을 생성하고 이를 응답에 기록하려면 어떻게 해야 합니까? 이 섹션에서는 이를 수행하는 방법을 설명합니다.

Objects

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();

ResponseBodyEmitterResponseEntity의 본문으로 사용하여 응답의 상태와 헤더를 사용자 정의할 수도 있습니다.

emitterIOException을 발생시키는 경우(예: 원격 클라이언트가 사라진 경우) 애플리케이션은 연결 정리를 담당하지 않으며 emitter.complete 또는 emitter.completeWithError를 호출해서는 안 됩니다. 대신 서블릿 컨테이너는 자동으로 AsyncListener 오류 알림을 시작하는데, 여기서 Spring MVC는 completeWithError 호출을 생성합니다. 이 호출은 차례로 애플리케이션에 대한 최종 ASYNC 디스패치를 수행하며, 그 동안 Spring MVC는 구성된 예외 해결 프로그램을 호출하고 요청을 완료합니다.

SSE

SseEmitter(ResponseBodyEmitter의 하위 클래스)는 서버에서 전송된 이벤트가 W3C SSE 사양에 따라 형식화되는 서버 전송 이벤트에 대한 지원을 제공합니다. 컨트롤러에서 SSE 스트림을 생성하려면 다음 예제와 같이 SseEmitter를 반환합니다.

@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();

SSE는 브라우저로 스트리밍하기 위한 기본 옵션이지만 Internet Explorer는 서버에서 보낸 이벤트를 지원하지 않습니다. 다양한 브라우저를 대상으로 하는 SockJS fallback 전송(SSE 포함)과 함께 Spring의 WebSocket 메시징을 사용하는 것을 고려해보세요.

예외 처리에 대한 참고 사항은 이전 섹션을 참조하세요.

Raw Data

때로는 메시지 변환을 우회하고 응답 OutputStream으로 직접 스트리밍하는 것이 유용합니다(예: 파일 다운로드의 경우). 다음 예제와 같이 StreamingResponseBody 반환 값 유형을 사용하여 이를 수행할 수 있습니다.

@GetMapping("/download")
public StreamingResponseBody handle() {
	return new StreamingResponseBody() {
		@Override
		public void writeTo(OutputStream outputStream) throws IOException {
			// write...
		}
	};
}

StreamingResponseBodyResponseEntity의 본문으로 사용하여 응답의 상태와 헤더를 사용자 지정할 수 있습니다.

Reactive Types

반응형 스택에서 이에 상응하는 내용 보기

Spring MVC는 컨트롤러에서 반응형 클라이언트 라이브러리의 사용을 지원합니다(WebFlux 섹션의 반응형 라이브러리도 읽어보세요). 여기에는 spring-webfluxWebClient와 Spring Data 반응형 데이터 저장소와 같은 기타 항목이 포함됩니다. 이러한 시나리오에서는 컨트롤러 메서드에서 반응형 형식을 반환할 수 있는 것이 편리합니다.

반응형 반환 값은 다음과 같이 처리됩니다.

  • DeferredResult를 사용하는 것과 비슷하게 단일 값 Promise가 적용됩니다. 예로는 Mono(Reactor) 또는 Single(RxJava)이 있습니다.

  • 스트리밍 미디어 유형(예: application/x-ndjson 또는 text/event-stream)이 있는 다중 값 스트림은 ResponseBodyEmitter 또는 SseEmitter를 사용하는 것과 유사하게 조정됩니다. 예로는 Flux(Reactor) 또는 Observable(RxJava)이 있습니다. 애플리케이션은 Flux<ServerSentEvent> 또는 Observable<ServerSentEvent>를 반환할 수도 있습니다.

  • DeferredResult<List<?>>를 사용하는 것과 유사하게 다른 미디어 유형(예: application/json)이 포함된 다중 값 스트림이 적용됩니다.

[Tip]
Spring MVC는 spring-coreReactiveAdapterRegistry를 통해 Reactor와 RxJava를 지원하므로 여러 반응성 라이브러리에서 적응할 수 있습니다.

응답으로 스트리밍하는 경우 반응적 역압력이 지원되지만 응답에 대한 쓰기는 여전히 차단되고 구성된 AsyncTaskExecutor를 통해 별도의 스레드에서 실행되어 WebClient에서 반환된 Flux와 같은 업스트림 소스를 차단하지 않습니다.

Context Propagation

java.lang.ThreadLocal을 통해 컨텍스트를 전파하는 것이 일반적입니다. 이는 동일한 스레드에서 처리할 때 투명하게 작동하지만 여러 스레드에서 비동기 처리를 수행하려면 추가 작업이 필요합니다. Micrometer 컨텍스트 전파 라이브러리는 스레드 전체와 ThreadLocal 값, Reactor 컨텍스트, GraphQL Java 컨텍스트 등과 같은 컨텍스트 메커니즘 전체에서 컨텍스트 전파를 단순화합니다.

클래스 경로에 Micrometer Context Propagation이 있는 경우 컨트롤러 메서드가 Flux 또는 Mono와 같은 반응형 type을 반환하면 등록된 io.micrometer.ThreadLocalAccessor가 있는 모든 ThreadLocal 값이 Reactor Context에 키-값으로 기록됩니다. ThreadLocalAccessor에서 할당한 키를 사용하여 쌍을 이룹니다.

다른 비동기 처리 시나리오의 경우 컨텍스트 전파 라이브러리를 직접 사용할 수 있습니다. 예를 들어:

// Capture ThreadLocal values from the main thread ...
ContextSnapshot snapshot = ContextSnapshot.captureAll();

// On a different thread: restore ThreadLocal values
try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
	// ...
}

자세한 내용은 Micrometer Context Propagation 라이브러리의 설명서를 참조하세요.

Disconnects

반응형 스택에서 이에 상응하는 내용 보기

Servlet API는 원격 클라이언트가 사라질 때 알림을 제공하지 않습니다. 따라서 SseEmitter 또는 reactive types을 통해 응답으로 스트리밍하는 동안 클라이언트 연결이 끊어지면 쓰기가 실패하므로 주기적으로 데이터를 보내는 것이 중요합니다. 전송은 빈(주석 전용) SSE 이벤트 또는 상대방이 하트비트로 해석하고 무시해야 하는 기타 데이터의 형태를 취할 수 있습니다.

또는 하트비트 메커니즘이 내장된 웹 메시징 솔루션(예: WebSocket을 통한 STOMP 또는 SockJS를 통한 WebSocket) 사용을 고려해보세요.

Configuration

비동기 요청 처리 기능은 서블릿 컨테이너 수준에서 활성화되어야 합니다. MVC 구성은 비동기 요청에 대한 여러 옵션도 제공합니다.

Servlet Container

필터 및 서블릿 선언에는 비동기 요청 처리를 활성화하려면 true로 설정해야 하는 asyncSupported 플래그가 있습니다. 또한 ASYNC jakarta.servlet.DispatchType을 처리하려면 필터 매핑을 선언해야 합니다.

Java 구성에서 AbstractAnnotationConfigDispatcherServletInitializer를 사용하여 서블릿 컨테이너를 초기화하면 이 작업이 자동으로 수행됩니다.

web.xml 구성에서 DispatcherServlet 및 필터 선언에 <async-supported>true</async-supported>를 추가하고 필터 매핑에 <dispatcher>ASYNC</dispatcher>를 추가할 수 있습니다.

Spring MVC

MVC 구성은 비동기 요청 처리를 위해 다음 옵션을 공개합니다.

  • Java 구성: WebMvcConfigurer에서configureAsyncSupport 콜백을 사용합니다.

  • XML 네임스페이스: <mvc:annotation-driven> 아래의 <async-support> 요소를 사용합니다.

다음을 구성할 수 있습니다.

  • 설정되지 않은 경우 기본 서블릿 컨테이너에 따라 달라지는 비동기 요청의 기본 시간 초과 값입니다.

  • AsyncTaskExecutor반응형 type으로 스트리밍할 때 쓰기를 차단하고 컨트롤러 메서드에서 반환된 Callable 인스턴스를 실행하는 데 사용됩니다. 기본적으로 사용되는 것은 부하가 걸리는 생산에는 적합하지 않습니다.

  • DeferredResultProcessingInterceptor 구현 및 CallableProcessingInterceptor 구현.

DeferredResult, ResponseBodyEmitterSseEmitter에 기본 시간 초과 값을 설정할 수도 있습니다. Callable의 경우 WebAsyncTask를 사용하여 시간 초과 값을 제공할 수 있습니다.

0개의 댓글