대학교때 했던 3개의 프로젝트는 nodejs의 express프레임워크를 써서 개발했었다.
결정기준이 개발이 비교적 쉽고, js언어로 백,프론트를 다 할 수있기 때문이었다.
그때는 지식이 너무 부족했기 퍼포먼스, 확장성 등 다양한 요소를 고려할수 있는 시야가 부족했고 때문에 단순한 기준으로 선택을 했었다.
취준을 하고 있는 지금 대기업 IT서비스 기업들에서 대부분 spring-boot 개발자를 채용하고 있는 것을 보고 다시 java 기반 spring을 공부하고 있다.
일주일 정도 공부를 진행했는데 정확히 보안성, 퍼포먼스 측면에서 왜 spring이 좋다는 건지 정확히 감이 안와서 이것저것 찾아보게 되었다.
nodejs
chrome V8 JavaScript엔진으로 빌드된 JavaScript런타임 입니다.
런타임: 어떤것을 만들고, 실행해주는 일련의 구성 요소들로 이루어진 실행환경
v8: javascript 컴파일러로 javascript코드를 기계어로
Nodejs: javascript 런타임
스프링 부트(프레임워크- 뼈대): 스프링 프레임워크 기반 프로젝트를 복잡한 설정없이 쉽고 빠르게 만들어 주는 라이브러리.
즉 사용자가 일일이 설정하지 않아도 자주 사용되는 기본설정을 알아서 해줌.
Spring 와 Node.js를 이용해서 백엔드 개발을 할때 어떤 차이가 있을까
하나의 요청당 하나의 스레드가 생성되고 이에 해당 스레드가 요청을 처리한 후 응답을 하는 방식이다.
spring MVC의 Multi-Thread방식에서 I/O작업이 수반된다면 자원의 낭비가 발생된다.
Thread가 수행하는 요청과 응답에 대한 일처리에 비하여 io 작업은 훨씬 더 긴 작업을 가지고 있고 이러한 시간동안 Thread들이 블로킹되기 때문이다.
Thread 갯수가 증가될 때마다 컨텍스트 스위칭이 빈번하게 일어나게 될 것이고 이로 인한 성능저하도 야기될 수 있다.
nodejs는 상대적으로 긴 시간이 소요되는 io작업을 기다리지 않고 client의 요청과 응답만을 담당하기 때문에 더 적은 자원으로도 높은 성능을 기대할 수 있다.
이벤트 루프가 싱글스레드이고 내부적인 아키텍쳐를 살펴보면 C++로 작성된 프로젝트인 libuv에 스레드풀이 존재한다.
즉, 싱글스레드로만 모든 작업을 하는 것이 아니다.
클라이언트 하나당 한개의 스레드가 생성되는 전통적인 방식과 달리 모든 클라이언트의 요청은 이벤트루프(싱글스레드)로 들어가게 된다.
이벤트 루프가 무거운작업들(ex. crud, 파일시스템) 등의 블로킹 작업들을 백그라운드에서 수행하고 이를 비동기 콜백함수로 이벤트 루프로 다시 전달한다.
V8엔진의 메모리는 기본적으로 32비트에서는 512MB로 설정되어 있고 64비트는 1.4GB값으로 설정되어있다.
전통적인 웹애플리케이션과 달리 동시에 최대 10,000개의 요청을 처리할 수 있도록 설계되어있다.
쫌더 자세하게...
이벤트 루프가 왜 중요한가?
이벤트 루프는 메인 스레드 겸 싱글 스레드로서, 비즈니스 로직을 수행한다. 수행중에 블로킹 io작업을 만나면 커널 비동기 또는 자신의 워커 쓰레드 풀에게 넘겨주는 역할도 한다.
ex) if문분기, 반복문돌며 필터링, 콜백내부로직 -> 이벤트 루프 수행, DB데이터 읽기, 외부api콜-> 커널의비동기 또는 자신의 워커쓰레드가 수행
여기서 주의할 점은!!
동시에 많은 요청이 들어온다해도 1개의이벤트루프에서 처리하기 때문에 js로직(if문분기, 반복문) 이 무겁다면 많은 요청을 처리해내기 힘들 것이다.
예를들어 while(true){}가 중간에 있다면 nodejs웹서버는 요청조차 받지 못함
GC조차 이벤트 루프에서 돌기 때문에 이벤트루프가 바쁘면(=cpu 인텐시브작업) 메모리가 부족해서 뻗을 것이다.
libuv는 어떻게 동작하는가?
블록킹 작업(api콜,db read/wirte등)이 들어오면 이벤트 루프가 uv_io에게 내려준다.
이벤트 루프의 phase들
이벤트루프는 몇 개의 phase들로 구성되어 있다.
각 phase들은 FIFO큐를 가지고 있으며, 이 큐에는 특정 이벤트의 콜백들을 넣고, CPU가 할당될 때 실행한다.
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('A');
}, 0);
setImmediate(() => {
console.log('B');
});
});
위 코드를 실행하면 이벤트 루프는 다음과 같은 순서로 동작
1. fs.readFile 라는 블로킹 작업을 만난 시점에 이벤트 루프는 워커 쓰레드에게 작업을 넘김
2. 워커쓰레드가 작업을 완료한 뒤 io callbacks영역의 큐에 콜백을 등록
3. 이벤트 루프가 callbacks영역을 실행할때, 콜백을 poll영역의 큐에 등록
4. poll영역을 실행할때, 큐에 1개가 있으므로 이걸 실행함
5. (콜백내부)2라인에서 setTimeout() 이므로 다시 timers영역에 넣고 5라인으로 간다.
6. (콜백내부)5라인에서 setImmediate()이므로 check영역에 넣는다.
7. 이벤트 루프가 poll큐를 비우고, 다음 실행 영역인 check영역으로 간다. check영역의큐에는 들어있는 'B'를 콘솔에 찍는다.
check영역의 큐를 비우고 다시 while문의 시작지점으로 간다.
8.이벤트루프가 timers영역을 호출한다.
uv_run_timers()는 setTimeout()의 콜백을 poll큐에 등록한다.
9.이벤트 루프가 2번째 poll 영역을 실행한다. 큐에 1개가 있으므로 이걸 실행하고 'A'를 찍는다.
10. node프로세스가 반환되고 끝
setTimeout(()=>{
console.log('A');
},0);
setImmediate(()=> {
console.log('B');
});
위 코드를 실행하면 이벤트 루프에서는 아래와 같은 순서로 동작한다.
1. setTimeout() 를 만나면 timers영역에 넣고 4라인으로 간다.
2. setImmediate() 를 만나면 check영역에 넣는다.
3. 프로세스의 기분과 상태 등에 따라 랜덤하지만, timers가 이벤트루프의 은총을 받으면 먼저 poll큐에 등록되고 timers다음은 poll이니까 'A'가 찍힐것이다. 하지만 timers를 지나쳤을 경우, check영역이 호출되므로 'B'가 찍힌다.
언제 어떤기술을 사용하는게 유리할까?
Cpu Intensive
cpu intensive한 작업이 많은 경우 Node.js가 불리(모든 req를 main thread로 처리하는데, cpu-intensive작업의 경우 딥러닝, 데이터분석, 머신러닝 등 main thread를 잠깐 멈추게 함)
io 작업이 아니라 cpu를 많이 사용하도록 작성된 함수가 있는데 어떤 요청이 들어와서 이 함수를 사용한다고 가정해보자. 해당 코드는 libuv에 있는 스레드풀로 던겨저서 요청을 비동기방식으로 처리하지 않고 이벤트 루프 자체에서 blocking 당하고 작업이 끝날때 까지 다른 요청들은 계속해서 쌓이게 되면서 성능이 저하된다.
Concurency
채팅 서비스의 경우 동시에 엄청난 클라이언트 요청이 발생할 수 있다.
일반적인 채팅서비스에는 단순하게 텍스트 메시지만 주고 받는데 여기서 Node.js를 사용하게 되면 유리하다.
Object DB의 API서버
mongoDB와 같은 JSON기반 DB를 사용하는 경우 Java를 선택하면 JSON데이터를 읽어오고 쓸때 JSON변환과정이 생기게 된다.
Nodejs를 사용하면 JSON을 별도의 변환없이 바로 사용할수 있어 훨씬 유리하다.
그래서 Node.js가 Spring MVC보다 성능상 유리할때는 언제일까?
이같은 전제사항은 오늘날 전형적인 웹 애플리케이션의 특징이다. 따라서 Node.js가 spring에 비해 성능상 우위라고 거론되는 이유는 Node.js가 이러한 '전형적인' 웹 애플리케이션의 요구사항과 잘 어우러지기 때문이라고 할 수 있다.
(반대로 만하면 I/O작업이 많지 않고 강도 높은 CPU연산이 요구된다면 한개의 Thread를 사용하는 Node.js는 급격한 성능 저하가 야기된다.)
Spring WebFlux의 등장
Spring WebFlux는 Spring5에서 도입되었습니다.
Spring WebFlux는 Node.js와 유사하게 완전한 비동기-논블러킹 방식의 I/O작업이 가능하고, 이를 제어하기 위한 Event Loop도 가지고 있습니다.
다른점이라면 single-thread기반이 아닌 multi-thread기반 입니다.
참고자료
https://codingjuny.tistory.com/58
https://syundev.tistory.com/229
https://siyoon210.tistory.com/164