Spring MVC 에서 WebClient 의 동작

이광훈·2024년 7월 12일

배경

  • 이번 프로젝트 내에서 Chat GPT 를 사용하기 위해 open API 로의 api 통신이 필요했다. 이 과정에서 모든 응답이 한번에 JSON 으로 오는 형식이 아닌 stream 의 형식으로 응답을 받기 위해 webClient 를 사용했다. 그리고 이를 사용하던 도중 만났던 문제의 발생원인을 찾는 과정에서 정리 및 기록용으로 해당 글을 작성하게 되었다.

Spring Webclient

  • 현재 Spring 은 공식적으로 HTTP 통신을 위해 기존에 사용하던 RestTemplate 대신 Webclient 를 사용하기를 권장하고 있다. 이 Spring Webclient 는 기본적으로 Spring Webflux 의 일부로 non-blocking 이 기본적이다. 하지만 현재 Spring MVC 에서도 이 기술을 사용할 수 있다.

  • 위의 굵은 글씨로 써진 부분이 궁금했다. 기본적으로 Spring MVC ( RestTemplate 을 이용할 때) 와 Spring Webflux 는 다른 방식으로 동작한다. 먼저 복습 겸 이 부분에 대해서 먼저 정리하려한다.

Spring MVC 의 동작

  • Spring MVC 는 기본적으로 One thread per request, 즉 하나의 요청에는 하나의 스레드가 바인딩된다.

  • 먼저 요청이 들어오면, 스레드 풀에 idle 한 스레드가 해당 요청을 담당한다. 만약 현재 idle 한 스레드가 없는 경우, 해당 요청은 Request Queue 에 들어가 idle 한 스레드가 생기기를 기다린다.

  • DispatcherServlet 과 controller 를 지나 service layer 로 들어와 직접 HTTP 통신이 필요한 상황을 보자. 이때 RestTemplate 을 사용한다고 가정한다.

  • Spring MVC 와 RestTemplate 은 Blocking stack 에서 동작한다. HTTP 통신이 필요한 상황에서 해당 요청을 처리하는 스레드는 HTTP 통신이 끝나고 통신에 대한 응답을 받을때까지 기다린다. 그리고 이후 HTTP 통신에 대한 응답을 받으면 이후 다시 service layer , controller 의 코드들을 순차적으로 실행한다.

Spring Webflux

  • 요청이 들어오면, EventLoopGroup 에 idle 한 eventLoopThread (스레드) 가 해당 요청을 서비스에서 처리한다.

  • Blocking 작업이 필요하지 않다면, 해당 스레드가 요청을 계속 처리하고 응답을 한다.

  • 만약 blocking 작업이 필요하다면, 해당 요청을 처리하던 eventLoopThread 는 blocking 작업을 Client 로 넘긴다. 이때, eventLoopThread 는 작업이 끝날때 까지 기다리지 않고 eventLoopGroup 으로 복귀한다.

  • Blocking 작업이 끝나면, 해당 작업은 ScheduledTaskQueue 로 들어간다. ScheduledTaskQueue 에 있는 작업은 이후 다른 idle 한 eventLoopGroup 의 eventLoopThread 에 의해 처리가된다. Spring MVC 와 다르게 여러개의 스레드에서 하나의 요청이 처리될 수 있고, blocking 작업을 기다리지 않는다.

  • 이렇게 blocking 작업을 기다리지 않는 덕분에 ( 스레드가 block 되지 않아서 해당 시간에 다른 작업들을 처리할 수 있어서 ) Webflux 는 높은 동시성 처리 능력을 갖고, 자원 효율성이 좋다.

Spring MVC 에서의 webclient

  • 원점으로 다시 돌아와서 Spring MVC 에서의 WebClient 는 어떻게 작동할까? 에 대해 다시 알아보자. 공부해 보니 Spring MVC 와 Spring Webflux 의 작업들이 합쳐진 느낌이었다.

  • 요청이 들어오면 spring MVC 와 같이 하나의 요청은 하나의 스레드가 담당한다. 이후 DispatcherServlet 과 Controller 를 거친다.

  • 이전에는 restTemplate 을 사용한 예시를 봤는데 이제 WebClient 를 사용한 예시를 보자. 외부 API 와 통신하기 위해 WebClient 에 들어간다.

  • 그러면 여기서 Webflux 에서 봤던 eventLoopThread 가 사용된다. 이 과정에서 eventLoopThread 는
    1. connection 이 시작되고, 요청을 보낸다.
    2. response callback handler 가 등록된다. (여기서 response 를 받는것이 아님)
    3. 해당 작업을 진행한 eventLoopThread 는 eventLoopGroup 으로 복귀한다.

  • 이후 외부 API 통신에 대한 응답을 받으면, callback 이 실행되고 이를 통해 만들어진 task 가 ScheduledTaskQueue 로 들어간다.

  • 그러면 eventLoopGroup 에 있는 다른 idle 한 eventLoopThread 가 이를 처리하고 이 작업 이후의 코드들 (service layer 의 남은 코드 , controller) 이 실행된다.

  • 사실, spring MVC 에서 webClient 를 사용하는 경우, non-blocking 의 이점을 누리지 못한다. 해당 작업을 처리하는 스레드는 WebClient 가 작업하는 동안 계속 이 작업이 끝날때까지 기다리기 때문이다.

프로젝트 과정 중 만난 문제

  • 프로젝트 작업 중 만난 한 오류 때문에 사실 현재까지의 글을 쓰고 정리를 하게 되었다. 이 문제가 왜 발생하는지가 궁금했다

  • GPT 에게 응답을 받는 서비스코드이다. 현재 사진에 있는것은 그 이전에 먼저 DB 에 접근해 해당 리소스가 존재하는지 체크를 하려는 의도로 작성하였다.

  • 그런데 사진에서 보이는 것 처럼 “Possibly blocking call in non-blocking context could lead to thread starvation” 이라는 노란 줄이 떴다. non-blocking 환경에서 blocking call 을 하면 thread starvation 을 일으킬 수 있다고 한다.

  • Webflux 환경에서는 해당 작업이 끝나는것을 기다리지 않고 eventLoopThread 는 eventLoopGroup 을 복귀한다. 그런데 이 작업을 blocking 으로 수행하는 경우, eventLoopThread 가 해당 작업이 끝남을 기다리고 eventLoopGroup 으로 복귀하지 못한다. 결국 그 시간동안 작업을 할 수 있는 스레드들이 묶여있게 되는것이다.

  • 그러면 어떻게 해야할까?

    • 현재 작업 자체를 NestJs 나 NodeJS 환경/ 혹은 spring MVC 를 사용하지 않고 아예 Webflux 를 이용해 진행 했으면 해당 문제가 생기지 않았을 듯 하다.

    • 혹은 R2DBC 를 사용해도 된다고 한다. 이에 대해 조금 더 찾아봐야겠다,,,,

      https://www.stefankreidel.io/blog/spring-webmvc-with-webclient

profile
허허,,,

0개의 댓글