요청을 비동기 처리해서 서블릿 컨테이너의 부하를 줄이기
HTTP 요청을 동기적으로 처리하면 요청 처리 스레드가 블로킹되고 응답을 열린 상태로 씌어질 준비
일정한 작업 시간이 소요되는 호출이면 스레드를 블로킹하지 않고 백그라운드에서 처리한 후 결괏값을 유저에게 돌려주는 게 더 효율적
스프링 MVC 컨트롤러의 핸들러 메서드는 여러 가지 반환형을 지원
| 타입 | 설명 |
|---|---|
| DeferredResult | 나중에 다른 스레드가 생산할 비동기 결과 |
| ListenableFuture<?> | 나중에 다른 스레드가 생산할 비동기 결과, DeferredResult 대신 사용 가능 |
| CompletableStage / CompletableFunture | 나중에 다른 스레드가 생산할 비동기 결과, DeferredResult 대신 사용 가능 |
| Callable<?> | 나중에 결과를 생산할(또는 예외를 던질) 작업 |
| ResponseBodyEmitter | 여러 객체를 응답에 실어 클라이언트에 비동기로 전송 |
| SseEmitter | 서버 전송 이벤트를 비동기로 작성 |
| StreamingResponseBody | OutputStream을 비동기로 작성 |
비동기 반환 클래스/인터페이스는 모두 제너릭형이라, 모델에 추가할 객체, 뷰, 이름, ModelAndView 객체까지 컨트롤러가 반환하는 어떤 반환형도 수용 가능
비동기 요청 처리는 서블릿 3.0부터 추가된 지원 기능
스프링 MVC에서 사용하려면 모든 필터와 서블릿이 비동기로 작동하게끔 활성화
필터/서블릿을 등록할 때 setAsyncSupported() 메서드를 호출하면 비동기 모드 활성화
public class CourtWebApplicationInitializer extends WebApplicationInitializer {
@Override
public void onStartup(ServletContext ctx) {
DispatcherServlet servlet = new DispatcherServlet();
ServletRegistration.Dynamic registration = ctx.addServlet("dispatcher", servlet);
registration.setAsyncSupported();
}
}
애플리케이션의 모든 필터/서블릿에 대해
isAsyncSupported프로퍼티를true로 설정해야 비동기 처리 가능
추상 클래스 AbstractAnnotationConfigDispatcherServletInitializer를 상속하면 이 클래스에 등록된 DispatcherServlet과 필터의 isAsyncSupported 프로퍼티가 켜져 있어 간편하게 구현 가능
비동기 모드를 켜도 끄는 분기 로직을 직접 구현하려면 setAsyncSupported() 메서드를 오버라이드
public class CourtWebApplicationInitializer extends WebMvcConfigurationSupport {
@Overrode
protected void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(5000);
configurer.setTaskExecutor(mvcTaskExecutor());
}
@Bean
public ThreadPoolTaskExecutor mvcTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setThreadGroupName("mvc-executor");
return taskExecutor;
}
}
WebMvcConfigurationSupport 클래스의 configureAsyncSupport() 메서드를 오버라이드해서 비동기 처리 보드로 구성한 코드
이 메서드를 오버라이드 하면 AsyncSupportConfigurer에 접근해서 defaultTimeout, AsyncTaskExecutor 값을 지정 가능
간단히 핸들러 메서드의 반환형만 바꾸면 요청을 비동기 처리하는 컨트롤러 작성 가능
조회 시간이 오래 걸리는
ReservationService.query()메서드를 서버를 블로킹하지 않는 상태로 호출
@Controller
@RequestMapping("/reservationQuery")
public class ReservationQueryController {
private final ReservationService reservationService;
public ReservationQueryController(ReservationService reservationService) {
this.reservationService = reservationService;
}
@GetMapping
public void setupForm() {}
@PostMapping
public Callable<String> sumbitForm(@RequestParam("courtName") String courtName, Model model) {
return () -> {
List<Reservation> reservations = java.util.Collections.emptyList();
if (courtName != null) {
Delayer.randomDelay();
reservations = reservationService.query(courtName);
}
model.addAttribute("reservations", reservations);
return "reservationQuery";
};
}
}
sumbitForm() 메서드는 String을 직접 반환하지 않고 Callable<String>을 대신 반환
람다 표현식 안에 query() 메서드의 처리 시간의 모의하고자 랜덤 대기 시간
여러 스레드에서 요청을 처리하다 마지막 요청이 다시 DispatcherServlet에 전송되고 또다른 스레드가 결과를 건네받아 처리
Callable<String> 대신 DefferdResult<String> 사용 가능
DefferdResult를 사용하려면 클래스 인스턴스를 만들어 비동기 처리 작업(Runnable)을 전송한 후, 이 작업 내부에서 setResult() 메서드를 이용해 DefferdResult 결괏값을 설정
예외가 발생하면 DefferdResult.setErrorResult 메서드의 인수로 보내 처리
@Controller
@RequestMapping("/reservationQuery")
public class ReservationQueryController {
private final ReservationService reservationService;
private final TaskExecutor taskExecutor;
public ReservationQueryController(ReservationService reservationService, AsyncTaskExecutor taskExecutor) {
this.reservationService = reservationService;
this.taskExecutor = taskExecutor;
}
@GetMapping
public void setupForm() {}
@PostMapping
public DeferredResult<String> sumbitForm(@RequestParam("courtName") String courtName, Model model) {
final DeferredResult<String> result = new DeferredResult<>();
taskExecutor.execute(() -> {
List<Reservation> reservations = java.util.Collections.emptyList();
if (courtName != null) {
Delayer.randomDelay();
reservations = reservationService.query(courtName);
}
model.addAttribute("reservations", reservations);
result.setResult("reservationQuery");
});
return result;
}
}
렌더링할 뷰 이름은 DeferredResult<String>형으로 반환
스프링이 주입한 TaskExecutor의 execute() 메서드의 실행 코드에 해당하는 Runnable 객체를 전달하고 실제 결괏값은 이 안에서 설정
DeferredResult로 반환할 경우 스레드를 직접(또는 TaskExecutor에 위임해서) 만들어야 하지만 Callable로 반환할 경우엔 그럴 필요가 없는 중요한 차이
CompletableFuture<String> 을 반환하고 TaskExecutor로 코드를 비동기 실행
@Controller
@RequestMapping("/reservationQuery")
public class ReservationQueryController {
private final ReservationService reservationService;
private final TaskExecutor taskExecutor;
public ReservationQueryController(ReservationService reservationService, TaskExecutor taskExecutor) {
this.reservationService = reservationService;
this.taskExecutor = taskExecutor;
}
@GetMapping
public void setupForm() {}
@PostMapping
public CompletableFuture<String> sumbitForm(@RequestParam("courtName") String courtName, Model model) {
return CompletableFuture.supplyAsync(() -> {
List<Reservation> reservations = java.util.Collections.emptyList();
if (courtName != null) {
Delayer.randomDelay();
reservations = reservationService.query(courtName);
}
model.addAttribute("reservations", reservations);
return "reservationQuery";
}, taskExecutor);
}
}
실행 코드를 CompletableFuture.supplyAsync() 메서드에 넣고 호출(또는 반환형 없는 runAsync() 메서드를 실행)하고 CompletableFuture 객체 반환
supplyAsync() 메서드는 Supplier, Executor 두 타입의 객체를 매개변수로 받으므로 비동기 처리 시 TaskExecutor 재사용 가능
매개변수가 Supplier 하나뿐인 supplyAsync() 메서드는 JVC에서 가용 가능한 기본 포크/조인fork/join 풀을 써서 실행
CompletableFuture를 반환받으면 여러 CompletableFuture 인스턴스를 조합compose하거나 연결chain해서 모든 기능을 최대한 활용 가능
자바 Future를 구현한 스프링의 ListenableFuture 인터페이스는 Future 완료 시점에 콜백을 실행
실행 코드를 AsyncListenableTaskExecutor에 전송하면 ListenableFuture가 반환
AsyncConfiguration 구성 클래스에 나온 ThreadPoolTaskExecutor도 AsyncListenableTaskExecutor 인터페이스 구현체
@Controller
@RequestMapping("/reservationQuery")
public class ReservationQueryController {
private final ReservationService reservationService;
private final AsyncListenableTaskExecutor taskExecutor;
public ReservationQueryController(ReservationService reservationService, AsyncListenableTaskExecutor taskExecutor) {
this.reservationService = reservationService;
this.taskExecutor = taskExecutor;
}
@GetMapping
public void setupForm() {}
@PostMapping
public ListenableFuture<String> sumbitForm(@RequestParam("courtName") String courtName, Model model) {
return taskExecutor.submitListenable(() -> {
List<Reservation> reservations = java.util.Collections.emptyList();
if (courtName != null) {
Delayer.randomDelay();
reservations = reservationService.query(courtName);
}
model.addAttribute("reservations", reservations);
return "reservationQuery";
});
}
}
Callable형 실행 코드를 submitListenable 메서드로 taskExecutor에 전달하면 ListenableFuture 반환, 이 객체를 메서드 결과로 활용
ListenableFuture의 성공/실패 콜백은 스프링 MVC 내부적으로 ListenableFuture를 DeferredResult에 맞추기 때문에 작업 성공 시 DeferredResult.setResult, 에러가 나면 DeferredResult.setErrorResult를 호출
모든 작업을 스프링에 내장된 HandlerMethodReturnValueHandler 구현체 (예제에서는 DeferredResultMethodReturnValueHandler)가 대행