본문 바로가기
Coding/Java Spring

스프링 Webflux - 스레드와 이벤트 루프

by Hide­ 2023. 11. 13.
반응형

개요

이벤트 루프는 단순한 스레드인데 어떻게 CPU를 블락킹하는 일반적인 자바 스레드와 다르게 동작할 수 있을까? 리액티브 프로그래밍은 이해하기 어렵진 않지만 완벽하게 이해하기는 쉬운 일이 아니다.

카페에 가서 선호하는 음료를 주문한다고 상상해보자. 주문을 한 후 바리스타가 커피를 다 만들때까지 카페에서 기다리거나 또는 카페 근처를 산책할수도 있다. 또는 주문 직후 핸드폰으로 이메일을 보내거나 다음 미팅 스케줄을 잡을수도 있다. 리액트 프로그래밍의 원리를 이해하고 사용하는것은 매우 중요한데, 프레임워크에 가려져있기에 쉽게 파악하기가 까다롭다.

본 포스팅에서는 일반적인 리액티브 프로그래밍에 대해 초점을 맞추지 않을 것이다. 대신, 스프링 내부에서 어떻게 통합되고 동작하는지에 대해 설명하겠다.

Reactive란

EventHandler는 두 가지에 반응한다. 첫 번째는 새로운 요청이 들어오는 것, 두 번째는 일종의 처리 완료 단계가 되는 것이다. 이 두 가지 타입의 이벤트는 언제든지 발생할 수 있다. 만약 이벤트 핸들러가 아무런 일을 하고 있지 않다면, 그 즉시 새로운 이벤트를 가져와서 처리할 것이다. 만약 이미 다른 작업을 진행중이라면 이벤트는 핸들러가 작업을 완료할 때 까지 큐에서 대기한다.

위 사진의 애니메이션을 보면 2개의 인스턴스(네모박스)가 존재한다. 그리고 각 인스턴스가 실제로 동작할때는 Blocking IO와 같은 블락킹 지점을 만났거나 프로그래머에 의해 다른 스레드로 제어권이 넘어갔거나 할때이다. 위 애니메이션은 리액티브 프로그래밍이 정상적으로 구현되었을 때 어떻게 하나의 이벤트 핸들러가 여러개의 요청을 처리할 수 있는지 보여주기 위한 것이다.

Reactive를 위해 Thread를 사용하는 방법

스프링은 논 블락킹을 위해 다양한 서버를 지원한다. (Tomcat, Jetty, Netty 등) 스프링은 자체 내장 서버를 가지고 있지 않은 반면 스프링 부트는 Netty를 기본적으로 사용하는 Webflux starter등이 존재한다. 앞서 설명한 서버 중 Netty만이 처음부터 논 블락킹을 위해 구현되었기에 다른 서버가 아닌 Netty를 사용하는 것을 강력하게 권장한다. 앞으로 설명할 예제들도 Netty를 사용할 것이다.

Getting started with WebFlux and Netty

Webflux-starter를 사용하면 스프링은 구동 시 자동으로 내장 Netty 서버를 실행한다. 스프링이 지원하는 다른 서버와 다른점은, Netty는 서블릿에서 동작하지 않는다는 점이다. 또한 클라이언트로 부터 오는 요청이 워커 스레드가 아니라 이벤트 루프로 배정된다.

Netty EventLoop

이벤트 루프는 Java NIO에 기반한 논 블락킹 IO 스레드이다. 기술적으로 우리가 일반적으로 알고있는 스프링 웹의 워커 스레드와 크게 다르지 않다. 하지만 이벤트 루프의 작동 방식을 이해하면 중요한 차이점이 무엇인지 알게 될 것이다.

일반적으로 EventLoopGroup에 의해 관리되는 2개의 이벤트 루프가 항상 실행된다. 각 이벤트 루프는 요청을 수락하거나(서버 관점에서) 생성(클라이언트 관점에서) 할 수 있는 여러개의 SocketChannel을 처리한다. 새로운 소켓 채널이 생성될 때마다 하나의 이벤트 루프로 바인딩되고 이는 더이상 변경할 수 없다. 소켓은 하나의 이벤트 루프에 바인딩되며 더이상 변경할 수 없다는 점, 이것이 일반적인 스프링 MVC와의 가장 큰 첫 번째 차이점이다. 이는 블락킹된 이벤트 루프가 다른 이벤트 루프는 여유로운 상태임에도 불구하고 사용자로부터 오는 요청을 처리할 수 없는 상태가 된다는 것을 의미한다. 

사진을 보면 EventLoopGroup에 총 3개의 이벤트 루프가 있다. 그리고 각 소켓이 하나씩 이벤트 루프에 바인딩된 상태이다. 이 때 1번 소켓 채널에 요청이 쌓인다고 가정해보자. 위에서 말했듯이 한번 바인딩된 소켓은 타 이벤트 루프에 바인딩될 수 없다. 따라서 2, 3번 이벤트 루프는 놀고 있는 상태임에도 불구하고 1번 소켓 채널에 들어온 요청을 처리할 수 없다. 이러한 특성때문에 이벤트 루프는 블락킹되지 않아야 한다. 

또한 자바 스레드의 컨텍스트 스위칭에 대한 비용이 크기 때문에 사양에 맞게 가용 가능한 프로세스만큼만 이벤트 루프를 보유하는것이 좋다. 물론 이러한 권장 사항에는 한가지 주의사항이 있는데 이는 나중에 다시 설명한다.

EventLoop Resource Handling

리액티브 프로그래밍의 핵심은 블락킹 작업이 완료될 때 까지 기다리기 대신 다른 작업을 수행하고 작업이 완료된 후 응답을 처리하는 것이다. 이는 다음과 같은 두 가지 뜻을 내포하고 있다.

첫 번째로 하나의 요청은 여러개의 스레드에서 처리될 수 있다. (하나의 요청이 하나의 스레드에서 동작하는 스프링 MVC와는 반대로) 이것만으로도 Requset Scope Bean이 왜 Webflux에 없는지 설명된다. 또한 Webflux가 SubscribeContext를 도입한 이유이기도 하다.

두 번째로 다양한 컨텍스트 작업으로 스레드를 처리하기 위한 경량화 매커니즘이 필요하다. 위 2가지를 통해 우리는 Spring Webflux가 어떻게 리액트 프로그래밍을 구현했는지 알게 되었다.

각 요청은 ChannelHandler를 통해 SockerChannel의 이벤트 루프에 할당된다. 그리고 이벤트 루프는 네트워크 콜이나 파일 시스템 접근과 같은 블락킹 작업을 만날 때 까지 코드에 정의된 모든 연산 작업을 실행한다(사진에서는 Service 영역) 블락킹 작업을 만나면 제어권이 넘어가고 콜백 함수가 등록된다. 이 때 이벤트 루프는 또 다시 다른 작업을 수행할 수 있는 상태가 된다. 위 사진은 이벤트루프 1이 첫 번째 요청을 클라이언트에 전달한 후 자유롭게 다른 요청을 처리하는 모습을 보여준다.

블락킹 작업이 끝나면 등록된 콜백 함수가 실행된다. 그리고 그 결과가 ScheduledTaskQueue에 담긴다. 다음으로 놀고있는 이벤트 루프가 이를 가져가서 또 다른 연산 작업을 수행한다. 위 사진에서 우리는 이벤트 루프 1로부터 실행된 요청이 놀고있는 이벤트 루프 2번에서 다뤄지고 있는 모습을 확인할 수 있다.

Blocking Operations

지금까지 우리가 배운 것을 정리해보자.

  • Spring Webflux는 이벤트 루프를 통해 요청이 처리된다. 이벤트 루프는 단순한 스레드일 뿐이며 일반적으로 이벤트 루프를 태스크 단위로 처리하기 위한 경량 메커니즘을 가지고 있다. 따라서 컨텍스트를 자주 전환한다.
  • 블락킹 작업을 만나기 전까지 이벤트 루프는 Spring MVC의 워커 스레드와 거의 유사하게 동작한다.
  • 블락킹 작업을 만나면 이벤트 루프는 해당 작업이 끝날때까지 기다리지 않고 실행 컨텍스트를 넘긴다. 그리고 다른 요청을 처리한다.

Reference

원글에 애니메이션이 있으니 꼭 확인해보시길 권장드립니다.

https://www.stefankreidel.io/blog/spring-webflux