-
[스프링 5 레시피] 5장: 스프링 MVC 비동기 처리Spring 2021. 4. 7. 15:46반응형
이 장의 주제인 스프링 MVC 비동기 처리가 필요한 이유에 대해서 간단하게 살펴보겠습니다.
클라이언트에서 요청을 보내면 하나의 서블릿 쓰레드가 그 요청을 받아서 처리하고 응답까지 도맡아 하기 때문에 클라이언트에게 응답을 돌려줄 때까지 해당 쓰레드는 블로킹(차단)됩니다.
Tomcat의 쓰레드 풀에 생성할 수 있는 최대 쓰레드 개수는 기본 200개이기 때문에 요청이 폭주하게 되면 여유 쓰레드가 없어서 Connection refuse나 Connection timeout이 발생할 수 있는데요,
이러한 문제로 서블릿 3부터는 HTTP 요청을 비동기로 처리할 수 있게 되면서 HTTP 요청을 접수했던 쓰레드를 WAS에 반환하고 Backend 작업을 할 때는 스레드를 작업 쓰레드 풀에서 꺼내 쓰다가 작업이 완료되면 다시 WAS의 쓰레드로 응답합니다.
이렇게 WAS의 쓰레드가 논블로킹으로 동작을 하게되면 더 많은 요청을 처리할 수 있게 됩니다.
요청의 동기와 비동기 처리 비교
먼저 동기 처리를 살펴볼텐데요, HelloService의 test() 메서드가 처리 시간이 3초 걸리는 작업이라고 가정하고 우리가 일반적으로 사용하고 있던 방식으로 요청을 해보겠습니다.
@RestController @RequiredArgsConstructor @RequestMapping("/api/hello") public class HelloApiController { private final HelloService helloService; @PostMapping("/blocking") public String submit() throws InterruptedException { Thread.sleep(3000L); // 서비스를 일부러 지연시킴 helloService.test(); return "result"; } }
POST http://localhost:8080/api/hello/blocking
위와 같이 요청을 보냈을 때 응답을 반환하기 까지 약 3.1초의 처리 시간이 걸린 것을 확인할 수 있는데요, 위의 핸들러 메서드는 동기방식으로 동작하기 때문에 WAS의 쓰레드가 3.1초 간 블로킹 되어있었습니다.
다음 코드는 String을 반환하는 대신 Callable<String>을 반환하는 핸들러 메서드입니다. Callable은 call() 메서드 하나만 가지고있는 함수형 인터페이스이기 때문에 람다식으로 구현할 수 있는데요, Runnable.run() 메서드에서 반환 값이 있는 버전이라고 생각하시면 됩니다.
@RestController @RequiredArgsConstructor @RequestMapping("/api/hello") public class HelloApiController { private final HelloService helloService; @PostMapping("/callable") public Callable<String> submitCallable() { System.out.println("Main: " + Thread.currentThread().getName()); // ------------ 요청을 받은 WAS 쓰레드는 return 전까지 사용되고 WAS의 쓰레드 풀에 반환 // Main: http-nio-8080-exec-1 // ------------ call() 메서드 안에 구현된 로직은 작업 쓰레드로 처리. return () -> { System.out.println("Work: " + Thread.currentThread().getName()); // Work: mvcTaskExecutor-1 Thread.sleep(3000L); // 서비스를 일부러 지연시킴 helloService.test(); return "result"; }; // ------------ 이후 응답할 때 다시 WAS의 쓰레드를 사용함 // 응답 처리 쓰레드: http-nio-8080-exec-2 } }
POST http://localhost:8080/api/hello/callable
앞에서와 마찬가지로 요청을 보냈을 때 약 3.1초 정도의 시간이 걸리는 것을 확인할 수 있구요, 해당 코드는 비동기이기 때문에 내부적으로 동작 방식이 달라집니다. (위 코드 주석 참고)
이와같이 WAS의 쓰레드는 요청/응답만 처리하고 쓰레드 풀에 반환되고 백엔드 로직을 처리하는 약 3초의 작업은 작업 쓰레드를 사용하기 때문에 외부에서 들어오는 요청을 더 많이 받을 수 있게됩니다.
비동기 핸들러 인터셉터
스프링 MVC를 학습할 때 핸들러 인터셉터 다루는 방법을 배웠는데요, 핸들러가 비동기로 처리될 때는 인터셉터를 조금 다르게 구성해야합니다.
아래 코드를 보면 HandlerInterceptor 대신 AsyncHandlerInterceptor를 구현 하였고, 추가적으로 afterConcurrentHandlingStarted() 메서드 사용할 수 있게 됩니다.
@Slf4j public class AsyncMeasurementInterceptor implements AsyncHandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { log.info("preHandle() 호출 !!"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { log.info("postHandle() 호출 !!"); } @Override public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) { log.info("afterConcurrentHandlingStarted() 호출 !!"); } }
POST http://localhost:8080/api/hello/callable
위에서 확인해봤던 Callable을 반환하는 비동기 핸들러를 요청해서 Interceptor의 실행 순서를 살펴보겠습니다.
WAS 쓰레드 1 [요청]
- preHandle() 호출
- 동기 로직 수행
- afterConcurrentHandlingStarted() 호출
작업 쓰레드 1 [작업]
- 비동기 로직 수행
WAS 쓰레드 2 [응답]
- preHandle() 호출
- postHandle() 호출
반응형'Spring' 카테고리의 다른 글
[스프링 5 레시피] 10장: 스프링 트랜잭션 관리 (0) 2021.04.20 [스프링 5 레시피] 3장: 스프링 MVC (0) 2021.03.10 [스프링 5 레시피] 2장: 스프링 코어 (2-7 ~ 2-12) (0) 2021.03.04 [스프링 5 레시피] 2장: 스프링 코어 (2-1 ~ 2-6) (4) 2021.02.26 [JPA] JpaRepository save() 메서드 주의 사항 (1) 2021.02.17