ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스프링 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 [요청]

    1. preHandle() 호출
    2. 동기 로직 수행
    3. afterConcurrentHandlingStarted() 호출

    작업 쓰레드 1 [작업]

    1. 비동기 로직 수행

    WAS 쓰레드 2 [응답]

    1. preHandle() 호출
    2. postHandle() 호출
    반응형
Designed by Tistory.