안녕하세요. 미더덕입니다.
이번 시간엔 Spring MVC 프로젝트에서 WebClient를 사용 시 내부 동작이 어떻게 이루어지는 지에 대해 알아보도록 하겠습니다.
1. Spring MVC (톰캣 -서블릿 - 동기 멀티스레드) VS Spring WebFlux (네티 - 이벤트루프-비동기 단일/멀티스레드)
우선 해당 상황을 이해하기 위해 Spring MVC의 기술적 바탕과 WebClient를 사용하기 위한 Spring WebFlux의 기술적 바탕이 다르다는 사실을 인지하고 있어야합니다.
2. Spring MVC - 톰캣 & 서블릿 기반의 동기 멀티스레드 기술
우리에게 익숙한 Spring MVC는 톰캣 서블릿 기반의 동기 멀티스레드 기술입니다.
톰캣 컨테이너는 스레드 풀에 미리 스레드를 생성시켜두고 HTTP 요청 마다 서블릿에 스레드를 할당하여 요청을 처리합니다.
기본적으로 I/O 발생 시 스레드는 블로킹되어 응답을 받은 후에 후속 작업을 처리할 수 있습니다.
3. WebFlux - 네티 & 이벤트 루프 기반의 비동기 단일/멀티 스레드 기술
Spring WebFlux는 Netty와 이벤트 루프 기반의 비동기 방식으로 동작합니다.
HTTP 요청을 처리하는 스레드는 작업을 비동기로 즉시 위임하며,
I/O 작업이 발생하면 스레드는 논블로킹으로 해당 작업을 이벤트 루프에 할당하고, 응답이 도착하면 비동기 방식으로 후속 작업을 처리합니다.
4. Spring MVC + WebFlux = ?
위에서 SpringMVC와 WebFlux는 기반 기술이 다른 것을 알아보았습니다.
그런데 우리는 SpringMVC 프로젝트에서 WebFlux의 라이브러리인 WebClient를 사용할 수 있습니다.
이러한 경우 서버 구조는 어떻게 작동되는 것일까요?
스프링 웹플럭스 문서를 보면 위의 두 기술은 공존할 수 있다고 합니다.
스프링 프로젝트 내에 Spring MVC 와 Spring WebFlux의 두가지 의존성을 추가하면
톰캣의 스레드 풀과 네티의 이벤트 루프 둘 다 생성 되는 것을 알 수 있습니다!!
앞단에선 기존 톰캣의 요청 처리 방식과 동일하게 스레드 풀의 멀티 스레드로 요청 당 스레드를 각각 할당합니다.
앞에서 스프링 MVC는 I/O 발생 시 해당 블로킹되어 응답을 기다린다고 했었죠?
이 때 WebFlux 기반인 WebClient를 사용하면 작업을 이벤트 루프에 할당하여 블로킹이 아닌 논블로킹으로 즉시 후속 작업을 처리할 수 있습니다.
5. 하지만 결국 일반적인 DB I/O 요청 상황에선 비동기 블로킹으로 처리되어 결국 동기적인 방식으로 응답된다.
하지만 문제가 있습니다.
우리의 어플리케이션이 비동기적으로 즉시 응답을 하여도
의존을 하고 있는 시스템에서 동기로 작업을 처리하면 기대하는 값을 바로 사용자에게 보여줄 수 없다는 점입니다.
특히 대부분의 서비스 어플리케이션은 DB를 의존하고 있을텐데요.
이 DB와 구현 기술 대부분 동기로 처리된다는 문제가 있습니다.
(이를 지원하는 R2DBC나 Nosql 환경에선 비동기적으로 활용 가능)
5. 코드로 확인하기!
그럼 위의 설명이 맞는지 디버깅을 돌려보실까요?
예상하는 상황은 이렇습니다.
1. HTTP 요청 시 톰캣 스레드 풀에서 스레드 하나 할당 받음.
2. WebClient 요청 시 논블로킹으로 요청 (톰캣 스레드)
3. 하지만 응답을 받기 위해 결국 블로킹되어 대기 (톰캣 스레드)
- 요청을 논블로킹으로 보냈지만 동기적인 응답을 받기 위해 대기..
- 요청을 보내고 다른 작업을 하다가 응답을 받을 수는 있음 !!!
4. 응답을 이벤트 루프에 네티 스레드로 받음
5. 최종 결과값을 톰캣 스레드가 받아 처리함
5-1. 환경 구성
먼저 Spring MVC와 Spring WebFlux의 의존성을 동시에 추가해줍니다.
프로젝트 구동 시 Netty 이벤트 루프 스레드(reactor-http-nio 스레드)들이 생성된 것도 확인할 수 있습니다!!
주의) Netty는 단일 스레드라며 ?
- Netty가 단일 스레드라는 설명은 요청을 수신하는 Acceptor Thread가 단일 스레드로 설정할 수 있다는 것이며, 그 외의 네티를 구성하는 이벤트 루프 등에 대한 여러 Worker Thread가 별도로 존재한다.
5-2. 클라이언트로부터 HTTP 요청 받은 상황 - 톰캣 스레드 풀의 스레드가 할당됨
톰캣 스레드 풀의 스레드가 할당되어 요청을 받은 것을 확인할 수 있습니다.
5-3. WebClient로 다른 서버에 HTTP 요청 - Netty 이벤트 루프 스레드가 할당 됨
콘솔을 보면 응답을 보내는 즉시 콘솔에 프린트를 찍었습니다. 이로서 해당 작업이 비동기적으로 진행된다는 것을 알았습니다.
또한 디버그 툴이 아래의 라인에서 멈추었는데 해당 스레드 명이 "reactor-http-nio-4"로 Netty 이벤트 루프 스레드인 것을 알 수 있습니다!
하지만 위에서 언급 드렸듯이 많은 상황에선 I/O의 응답을 블로킹하여 기다려야 합니다.
응답값이 Flux면 toStream(), Mono면 block()으로 톰캣의 스레드를 블로킹하여 응답을 대기합니다.
5-4. 톰캣 스레드가 블로킹되어 대기하다 WebClient의 응답 값을 받음
WebClient의 응답을 받은 톰캣 스레드는 다시 "http-nio-8080-exec-6"임을 확인해볼 수 있습니다.
이벤트 루프를 통해 네티 스레드가 응답을 받을 때까지 해당 톰캣 스레드는 블로킹되어 대기하다 응답을 최종 리턴 받은 것입니다 !!
6. 결론) "비동기 블로킹"은 결국 동기적으로 응답된다.
Spring MVC에서 일반적인 동기 방식으로 WebClient를 사용할 시 해당 기술을 "비동기 블로킹"으로 사용하는 것입니다.
(비동기적으로 println()를 찍었지만 결국 WebClient의 응답을 블로킹하여 대기하였음)
따라서 이는 결국 사용자에게 동기적인 형태로 응답하는 것으로 이해해볼 수 있습니다.
총총
참고 :
https://www.stefankreidel.io/blog/spring-webmvc-with-webclient
https://moonsiri.tistory.com/187
https://velog.io/@ksiisk99/spring2
https://findmypiece.tistory.com/276
'백엔드 > Spring' 카테고리의 다른 글
HTTP Method & Content-Type (0) | 2024.08.29 |
---|---|
@ResponseBody VS ResponseEntity 클래스 VS 커스텀 응답 클래스 (2) | 2024.06.25 |
WAS 내부 요청 처리를 코드로 알아보자! - 1. EndPoint 분석 (0) | 2023.12.24 |
AOP 적용 방법 5가지 정리 (프록시를 이용한 트랜잭션 AOP) (0) | 2023.12.13 |
WAS(톰캣)의 내부 구성 및 작동 과정 - 1. 초기화 단계 (0) | 2023.12.10 |