이번 글에선 이론적으로 WebFlux가 Spring MVC에 대비해 어떤 이점이 있는지 분석하고,
외부의 performance Test를 이러한 이론을 증명하겠습니다.
WebFlux를 사용하는 이유는?
- aync & Non-blocking방식으로 인해 cpu, Thread, memory등의 리소스 낭비 없이 효율적으로 동작한다.
- 서비스간 호출이 많은 MSA에 적합하다.
spring webmvc vs webflux
webmvc
- Blocking & sync
- 사용자의 요청이 들어올 때 마다 Thread를 생성하여 처리(multi thread)
- Thread Pool에서 미리 스레드를 일정 갯수 생성하고 사용(스레드 생성, 삭제 비용의 절감)
- 요청이 들어오면 Queue 에 쌓고 순서에 따라서 Thread 를 하나 씩 할당하여 처리한다.
- 스레드 풀내의 스레드를 초과하는 요청이 발생하면 Thread Pool Hell( 스레드를 할당 받기 위해 대기) 현상 발생
webflux
- webflux는 이 요청을 처리하는 방식이 Event-Driven 방식
- 이벤트 루프를 계속해서 돌다가 요청 발생 시, 그에 맞는 핸들러에게 처리를 위임하고 callback 메소드 등으로 응답을 반환받는다(non blocking&async)
- 대량의 요청이 동시다발적으로 발생시에 유리하다.
- 함수형 코딩 자체로 이점을 가진다.
webflux의 함수형 코딩 스타일의 장점
1) 모든 웹 요청 처리 작업을 명시적으로 작성 가능
- 기존 spring MVC는 어노테이션에 의한 흐름을 이해하기 어렵다.
- webflux는 시그니처 관례, 타입체크가 불가능한 어노테이션에 의존하지 않기에 명시적인 코딩이 가능하다.
- 정확한 타입체크가 가능하다.
2) 추상화, 모듈화에 유리하다.
3) 테스트 작성에 용이하다
- spring mvc는 Controller, Service 단위의 테스트 진행을 위해서 Web Test를 진행해야하지만, webflux는 핸들러 로직, 요청 매핑과 리턴 값 처리까지 Unit Test로 작성 가능.
WebFlux가 생긴 이유
- "적은 양의 스레드" 와 "최소한의 하드웨어 자원" 으로 동시성을 핸들링하기 위해
- 함수형 프로그래밍이 webflux의 기반이 됨( Java5에서 Rest controllers나 unit test가 만들어지고, Java8에서는 함수형 API를 위한 Lambda 표현식이 추가. 이는 논블로킹 어플리케이션 API의 토대가 되었다. )
+@
Servlet 3.1 이후부터는 Non-Blocking 을 지원하기는 하지만 일부분만 지원하기 때문에, WebFlux에서는 netty 와 같이 잘만들어진 async, non-blocking 서버를 사용한다고 합니다.
따라서, Spring WebFlux는 리엑터 라이브러리 (Reactor library) 와 넷티 (Netty) 를 기반으로 동작합니다.
이제 본격적으로 web mvc와 webflux의 성능을 비교 분석하며 위의 내용들을 적용해보자.
Boot 1: spring MVC
Boot 2: spring webflux
300명 가량의 동시접속 유저
5000명 가량의 동시접속 유저
위의 테스트 통계를 통해서 유저가 적을 때에는 성능 차이가 거의 없다는 것을 볼 수 있다.
하지만, 유저가 늘어날수록 webflux의 성능이 상대적으로 엄청난 우위에 있다는 것 역시 확인할 수 있다.
왜 특정 시점(유저 증가에 따라) 이후에 spring MVC가 webflux에 비해 낮은 성능을 기록할까?
1) Blocking I/O
spring MVC에서 이러한 모델을 사용한다.
이때의 단점은, I/O가 요청되고 완료되기 전까지 다른 작업을 수행하지 못한다는 것인데, spring MVC는
multi-thread를 사용하기에, 어떠한 thread가 Block되면 다른 Thread를 사용하는 것으로, non-Blocking과 유사하게 대기 시간을 완화할 수 있게된다.
하지만, block되는 시점에서 thread를 전환하는 것에는 Context Switching 비용이 발생하여, 동시에 여러개의 I/O를 처리해야하는 상황에 직면하면 여전히 성능적으로 나빠진다.
이것이 spring MVC가 많은 동시 요청 발생시에 낮은 퍼모먼스를 보이는 첫번째 이유이다.
2) Syncronous
spring mvc는 I/O 요청 후, 올바르게 요청이 전달되었는지 주기적으로 확인하는 Polling을 수행하게 된다.
이 방식은 요청이 완료되기 전까지, 계속해서 체크하기에, 불필요하게 리소스를 사용하게 된다.
이것이 spring MVC가 낮은 성능을 보이는 두번째 이유이다.
3) multi thread
spring mvc는 multi Thread방식을 사용하여 각 HTTP 요청마다 쓰레드를 1개씩 할당한다. 하지만 Thread를 매 요청마다 생성, 삭제하는 것은 너무 큰 Cost이기에, Thread Pool에서 미리 일정 개수의 쓰레드를 생성하여 재사용하게된다.
하지만, 만약 동시다발적으로 많은 요청이 들어와서 Thread pool에서 할당해줄 쓰레드가 없다면 어떻게 될까?
해당 요청들을 Queue에 넣고, 쓰레드가 반환될 때까지 대기해야한다.
이러한 이유로 발생하는 지연 시간을 Thread pool hell 이라고한다.
그럼 이제, webflux가 어떻게 위와 같은 단점을 극복하는지 알아보자.
spring webflux는 소수의 스레드가 event-driven 방식으로 동작하고, async, non-blocking 방식을 사용한다.
Event-Driven
시장 조사 기관 가트너는 비즈니스 업계가 주목해야 할 2018 10대 전략기술 트렌드를 발표했고, Event-Driven가 포함되어 있다. 또한 Event-Driven을 토대로 많은 프레임워크와 라이브러리가 발전하고 있다.
ex) Spring WebFlux, Node.js, Vert.x
Event-Driven Programming은 프로그램 실행 흐름이 이벤트(ex : 마우스 클릭, 키 누르기 또는 다른 프로그램의 메시지와 같은 사용자 작업)에 의해 결정되는 프로그래밍 패러다임이다. Event가 발생할 때 이를 감지하고 적합한 이벤트 핸들러를 사용하여 이벤트를 처리하도록 설계됐다. 순차적으로 진행되는 과거의 프로그래밍 방식과는 달리 유저에 의해 종잡을 수 없이 진행되는 GUI(Graphical User Interface)가 발전됨에 따라 Event-Driven 방식은 더욱더 많이 쓰이게 되었다.
event-driven 코드 예시
우선 과거에 C 코드로 구현한 keyboard Event-driven 코드를 살펴보자.
int main(void){
char key;
while(1){
key = getch(); // (1)
switch (key) { // (2)
case 1 : 실행문; break;
case 2 : 실행문; break;
case 3 : 실행문; break;
case 4 : 실행문; break;
default : 실행문; break;
}
}
return 0;
}
Event Loop(무한 루프)가 돌면서 Event를 감지한 뒤 그에 적절한 Event Handler 또는 Event Listener에게 보내서 작업을 처리한다.
ex) key a 가 눌리면, key a 가 눌릴 때 어떻게 처리할지 작성된 핸들러에게 이벤트를 보낸다.
Java Swing
JButton button = new JButton();
button.addActionListener(e -> System.out.println("clicked"));
일일이 Event를 제어했던 과거와는 달리 요즘은 이를 단순히 Listener에 행위만 등록해주면 간편하게 Event를 제어할 수 있다. 즉 Evevt Loop를 돌면서 요청을 감지하고 적합한 Handler에 위임해주는 부수적인 부분은 언어 레벨에서 처리를 해준다는 말이다.
webflux는 각각의 HTTP Request를 Event로 보고 처리한다.
webflux는 하나의 스레드가 Event Loop 역할을 수행하여 들어온 Event( HTTP Request)를 올바른 핸들러에 넘겨준다.
보통 이러한 worker Thread(Event Loop기능을 수행하는 쓰레드)는 서버의 core 수만큼 설정한다.
따라서 동시에 많은 요청이 있어도 소수의 worker thread에서 빠르게 요청을 처리해 나가기에 Thread pool hell, context swithing 비용도 적고, 요청을 주기적으로 체크하지 않기에 성능적 이점이 존재하는 것이다.
그렇다면 무조건 webflux, Node.js 등의 event-driven, async & non-blocking 방식이 우월한 성능을 가질까?
정답은 '그렇지 않다' 이다.
다시 위의 그래프를 보면 알겠지만, webflux와 web mvc의 성능이 유사한 구간이 존재한다.
이제 우리는 해당 구간이 "쓰레드 풀에서 감당 가능한 수의 요청이 들어오는 구간" 이라는 것을 알 수 있을 것이다.
쓰레드 풀에서 감당하지 못할 정도의 요청이 들어오게되면 Queue에서 대기하고, 이때문에 성능 저하가 발생했던 것이다.
webflux, Node.js에서 사용하는 event-driven 방식은 결국 "성능" 이라는 이점이 있지만 코드의 가독성, 편리성, 디버깅 난이도, 안정성 등등 모든 면에서 spring mvc가 우위에 있다.
그렇기에 서버의 성능상 예상되는 요청량을 쓰레드 풀이 감당할 수 있다면, 굳이 webflux를 사용할 이유가 없는 것이다.
하지만, 웹 앱 애플리케이션의 특성 상, 대규모 요청을 배제하기는 어렵다.
또한 특히나 유행하는 MSA에서는 수많은 Microservice가 거미줄처럼 서로를 네트워크를 통해서 호출하고 있다.
즉 많은 수의 Network I/O가 발생할 텐데 이를 Non Blocking I/O를 통해 좀 더 성능을 끌어올릴 수 있다.
이러한 적합성 때문에 트렌드상 spring MVC가 점점 열세를 띄고 있는 것 같다.
'MSA, EDA, Reactive 패러다임' 카테고리의 다른 글
Reactive System과 event-driven Architecture (0) | 2023.07.06 |
---|---|
Request-Respone(Rest 통신) VS 비동기 메세지 통신(Pub-Sub) (0) | 2023.07.06 |
reactive programing [이론 정리] (0) | 2023.07.06 |
Spring reactive Stack VS Servlet Stack (0) | 2023.07.05 |
Spring mvc VS Node.js 비교분석 (0) | 2023.04.20 |