비동기 처리란 무엇인지와 Netty, Spring WebFlux 개요
1. Netty와 WebFlux 탄생 배경
기존 동기 방식 웹 서버(톰켓)에서는 해결하기 힘든 문제들이 있었다.
- 요청이 아주 많아질 경우 스레드풀이 충분하지 않다면 요청 처리가 지연될 수 있는 문제
- 잦은 스레드 스위칭으로 인한 컨텍스트 스위칭 비용 문제
- 소켓에 데이터를 쓸 때 애플리케이션의 데이터를 커널 버퍼로 한 번 복사해야 하는 문제
- 클라이언트의 요청을 Accept, Read, Write할 때 발생하는 블로킹 문제
이러한 문제들을 해결하고 더 높은 성능을 제공하기 위해 비동기 방식 웹 서버(Netty)가 등장하게 된다.
Netty는 위 문제들을 이벤트 루프 패턴과 NIO 기반의 기술들(DirectByteBuffer 등)을 활용해서 효율적으로 해결한다.
이 때 이벤트 루프 패턴은 적은 수의 스레드만을 사용하는데,
적은 수의 스레드를 사용하면 Blocking(스레드가 외부 리소스를 기다리고 있는 상황)에 매우 취약하게 된다.
극단적인 예로 스레드가 1개인 이벤트 루프에서 Blocking이 발생한다면, 해당 이벤트 루프는 Blocking이 해소될 때 까지 아무것도 안하고 자원만 가지고 있는 상황이 발생 하는것이다.
위 이벤트 루프에서 요청 처리에 매번 10초의 Blocking이 발생한다면, 이 웹 서버는 아무리 좋은 컴퓨팅 자원을 가지고 있더라도 10초에 1개의 요청만 처리할 수 있는 최악의 웹 서버가 될 것이다.
이러한 Blocking에 취약한 단점을 효율적으로 해결하기 위해 Subscriber - Publisher 패턴이 등장하게 됐고,
이것을 구현하기 매우 쉽게 만들어 놓은 것이 WebFlux라고 할 수 있다.
2. 비동기 처리는 어떻게 동작할까?
비동기 방식 웹 서버에서 "비동기"란 무엇인지 개념을 정리하고 어떻게 구현 되는지 살펴보겠다.
2.1 비동기 처리란?
비동기 처리란 대기가 필요한(외부 리소스를 기다려야되는 Blocking 작업)작업을 메인 스레드가 직접 기다리지 않고 백그라운드(OS Kqueue, 다른 스레드, Worker 등)에서 실행한 후 작업이 완료되면 후속 처리를 수행하는 방식이다.
이를 통해 메인 스레드는 블로킹되지 않고 다른 작업을 계속 수행할 수 있다.
편의상 다음 설명에서는 블로킹 없이 작업을 계속 하는 스레드를 메인 스레드 / 외부 리소스를 기다려야 되는 주체를 백그라운드로 서술하겠다.
2.2 비동기 처리는 어떻게 구현할 수 있을까?
1. 폴링 패턴
가장 먼저 생각이 드는 간단한 방법은 백그라운드 작업이 완료 됐는지 수시로 체크하는 방법이다.
메인 스레드는 다른일을 하고 있다가 중간 중간 백그라운드 작업이 완료 됐는지 체크하는것이다.
구현이 간단하지만 중간 중간 체크하는 행위 자체가 리소스를 낭비할 가능성이 높아 일반적으로 비효율적인 방식으로 간주된다.
하지만 HTTP Long Polling과 같이 적절한 주기를 설정한 폴링 기법은 특정 상황에서 유용하게 활용될 수도 있다.
2. 이벤트 루프 패턴
이벤트 루프는 비동기 기능을 구현할 수 있는 패턴 중 하나이다.
Netty 뿐 만 아니라 Node.js, Nginx 등등 많은 비동기 기능을 제공하는 프로그램들이 해당 패턴을 채용하고 있다.
이벤트 루프 패턴을 이용하여 작업이 완료 됐는지 매번 체크 하지 않고도 비동기 처리를 할 수 있다.
이벤트 루프 패턴에서는 백그라운드에서 대기가 필요한 작업이 완료 됐을 때 이벤트를 발생시키고, 이벤트 루프에 등록한다.
메인 스레드는 이벤트 루프에 있는 이벤트만 계속해서 처리하고, I/O가 필요할때는 백그라운드에 위임한다.
이렇게 하면 메인 스레드는 Blocking 없이 계속해서 작업을 수행할 수 있다.
그러면 Netty 이벤트 루프에서 외부 작업을 기다려야하는 백그라운드는 무엇이 될까?
Netty의 이벤트 루프는 OS에서 제공하는 비동기 I/O 이벤트 통지 메커니즘(Epoll, Kqueue 등)을 백그라운드로 사용한다.
OS가 백그라운드로서 I/O처리가 완료될 때 까지 대기하고, 작업이 처리 될 때 마다 이벤트를 발생시켜 이벤트 루프에 등록한다.
3. Subscriber - Publisher 패턴
Subscriber - Publisher 패턴은 이벤트 루프 패턴과는 별개의 개념이며 함께 사용될 수는 있지만 혼동하면 안 된다.
이 패턴을 기반으로 설계된 Reactive Streams를 구현하게 쉽게 해 놓은 것이 WebFlux가 되겠다.
Publisher에서 데이터가 준비 됐을 때 사용 할 콜백 메소드를 등록하고
데이터가 준비 됐을 때 subscriber가 등록된 콜백 메소드를 이용하여 작업을 처리하는 패턴이다.
Subscriber - Publisher 패턴에서 비동기 처리를 하려면 Scheduler 등을 이용해서 메인 스레드와는 별도의 스레드를 할당하여 백그라운드로 사용해야 한다.
3. Spring WebFlux + Netty의 요청 처리 흐름
위에서 이벤트 루프 패턴과 Subscriber - Publisher 패턴에 대해 간략하게 다루었었다.
아래 그림과 함께 WebFlux와 Netty가 어떻게 통합되어 이벤트 루프 패턴과 Subscriber - Publisher 패턴을 사용하고 있는지 살펴보겠다.
Netty 이벤트 루프에서 발생하는 이벤트에는 [Accept, Read, Write]가 있다.
말 그대로 Accept이벤트는 클라이언트의 요청을 Accept하는 이벤트이다.
Read는 클라이언트의 요청을 읽는 이벤트이고, Write는 클라이언트에게 요청을 쓰는 이벤트이다.
일반적으로 클라이언트가 요청을 보냈을 때 이를 수락하기 위해 Accept이벤트가 발생한다.
Accept이벤트가 처리되고 클라이언트 요청을 읽을 준비가 완료 되면 Read이벤트가 발생한다.
Read 이벤트가 완료되고 클라이언트에게 응답을 쓸 준비가 완료 되면 Write이벤트가 발생한다.
(클릭해서 봐주시면 잘 보입니다)
4. Netty의 스레드 모델
Netty 이벤트 루프 스레드는 BossGroup과 ChildGroup(WorkerGroup으로 불리기도 한다.)으로 나누어진다.
BossGroup은 보통 스레드 1개로 구성되고 ChildGroup은 보통 cpu 코어의 2배수 만큼의 스레드로 구성된다.
Tomcat이 수천 수백개의 스레드로 구성된 스레드풀을 사용하는것에 비하면 굉장히 적은 수의 스레드라고 볼 수 있다.
BossGroup은 요청 Accept 이벤트 처리만 담당하고 Read이벤트를 발생시킨다.
ChildGroup은 Read와 Write 이벤트를 처리한다.
위 그림에서 3. Client Request Accepted를 처리하는것이 BossGroup이 하는 일이 되겠고, 나머지는 ChildGroup이 하는 일로 보면 될 것 같다.
Boss - Child 구조를 사용함으로서 Child 그룹에서 read, write 처리가 늦어진다 해도 클라이언트 요청의 accept처리는 계속해서 할 수 있게 된다.