nodeJS 란
노드(NodeJS)란?
https://ko.wikipedia.org/wiki/Node.js
- 브라우저는 여러가지 구성 요소들로 이루어져 있다. 그 중 하나가 자바스크립트 엔진(해석기)인데, 이 엔진은 브라우저마다 다른 엔진을 사용한다. (크롬의 V8, 익스플로러의 chakra, 파이어폭스의 spiderMonkey 등)
- 자바스크립트를 해석하는 엔진이 브라우저 내에 들어있기 때문에, 그동안 자바스크립트는 웹 프론트에서 주로 쓰여왔다.
- 그 때, 여기서 자바스크립트를 서버 개발에 이용하기 위해 브라우저에만 국한되어있던 자바스크립트를 브라우저 밖으로 가져온 것이 Node.js라고 보면 된다.
-
“Node.js는 크롭 V8 자바스크립트 엔진으로 빌드된 자바스크립트 런타임이다.”
- 런타임: 특정 언어로 만든 프로그램들을 실행할 수 있는 환경
- 또한, 노드가 사용하는 V8엔진(가상머신)은 ECMAScript 표준을 준수하고 있다. 다시 말해, 노드가 사용하고 있는 자바스크립트의 새로운 기능을 바로 적용할 수 있다는 의미이다.
우선 자바스크립트가 어떻게 동작하는지에 대해 알아보자.
이벤트 기반(Event-driven)
- 이벤트 발생 시, 미리 저장해둔 작업을 수행하는 방식을 의미한다.(클릭, 네트워크 요청 등등)
- 이벤트 기반 시스템에서는 특정 이벤트 발생 시, 무엇을 할 지 미리 등록해두어야 한다.(eventListener에 콜백함수를 등록)
- 노드는 이벤트 기반 방식으로 동작하기에, 이벤트 발생 시 이벤트 리스터에 등록해 둔 콜백 함수를 호출한다. 발생한 이벤트가 없거나 이벤트가 다 처리되면 노드는 다음 이벤트 발생까지 대기한다.(시스템 -> 이벤트 리스너 -> 시스템(콜백 함수 호출))
event loop, Task Queue, Back ground(Web API)
- event loop: 이벤트 발생 시, 호출할 콜백 함수들을 관리, Task Queue에 담겨있는 콜백 함수들을 콜 스택에 넘겨 준다. 이때 루프는 callstack을 확인하고 비어있는 경우에만 Task Queue에 콜백 함수를 넘겨 주게 된다.
- Task Queue: 이벤트 발생 후, 호출되어야 할 콜백 함수들이 기다리는 공간.
- Back ground(Web API): 브라우저에서 지원하는 Web API로는 DOM, Ajax, setTimeout 등 비동기 작업들을 수행할 수 있도록 api를 지원한다. Nodejs는 Background와는 차이가 있다.
Javascript 런타임은 자체적으로 비동기를 지원하는가?
동시성을 보장하는 비동기, 논블로킹 작업들을 Javascript 엔진을 구동하는 런타임에서 제공한다. NodeJS와 브라우저가 있다.
즉, Javascript의 엔진은 단지 코드에 대한 실행환경이며, 각 이벤트 스케쥴링은 런타임에서 지원한다는 것.
Web API, Promise
Web API에서 비동기 함수를 맡아서 처리를 한 다음, 이벤트루프에 의해 Task Queue에 넘겨, 이후 콜스택에 넘기고, 실행이 된다. 그렇다면 Web API에서 제공하는 기능과 Promise 중에서는 Task Queue에서 우선순위가 어떻게 되는가?
결과로는 Promise
가 먼저 처리된다. Queue라서 먼저 끝난 작업이 callstack에 쌓일거 같지만, 아니다.
Task Queue는 하나로 구성되어있지 않다.
- Task Queue (Event Queue)
- MicroTask Queue (Job Queue)
- Animation Frames
와 같이 구성되어 있으며, 우선순위는 MicroTask Queue > Animation Frames > Task Queue이다. Promise는 MicroTask에 담기기에 먼저 실행이 되는 것 이다.
Non-Blocking I/O란?
만약 http 요청을 동기로 수행한다면 해당 함수가 콜 스택에 쌓인 채로 머물고 JS엔진은 해당 작업이 끝날 때까지 어떤 작업도 수행할 수 없게 된다. 바로 동기 작업이 다른 코드들을 블로킹하기 때문이다. 하지만 Javascript는 비동기 작업들을 Web API에게 넘겨줄 수 있기 때문에 해당 작업이 완료될 때까지 다른 코드들을 실행할 수 있게 된다. 이렇게 Non-Blocking이 가능하게 된다.
console.log("start");
setTimeout(() => console.log("비동기 0초"), 0);
console.log("end");
/*
start
end
비동기 0초
*/
Node JS란
Javascript를 브라우저 밖에서도 실행시킬 수 있는 Javascript런타임. Javascript를 실행시킬 수 있는 환경이라는 뜻이다.
Node JS는 싱글 스레드이다.
- 싱글스레드: 프로세스 내에 하나의 스레드가 하나의 요청만을 수행하는 것.
- 멀티스레드: 스레드 풀에서 요청만큼의 스레드를 매칭하여 작업을 요청하여 여러 스레드들이 여러 요청을 수행하는 것.
Node JS의 싱글 스레드는 Non-Blocking이기에 비동기 I/O작업을 통해 요청 작업이 Blocking하지 않는다. 또한 Node JS는 클러스터링을 통해 프로세스를 포크하여 멀티스레드 처럼 사용할 수 있다. 이는 트래픽에 따라서 프로세스를 포크 할 수 있으므로 서버의 확정성면에서 용이하다.
- 성능 개선 방법: Cluster, Worker Threads 사용.
Node JS 는 정말 싱글 스레드인가?
Node JS는 싱글 스레드이지만 와전한 싱글 스레드를 기반으로 동작하지는 않는다. 일부 Blocking 작업들은 libuv의 스레드 풀에서 수해오디기 때문이다.
이벤트 기반
이벤트 기반이란 이벤트가 발생할 때 미리 지정해둔 작업을 처리하는 방식이다. Node JS는 이벤트 리스터에 등록해둔 콜백 함수를 실행하는 방식으로 동작한다. 즉, 이벤트 루프가 이를 가능하게 해 준다.
Node JS의 내부구조
출처: https://stackoverflow.com/questions/37051076/how-nodejs-event-loop-works/37052406
- Node.js는 V8과 더불어 libuv라는 라이브러리를 사용한다.
- V8은 C++ 70% 자바스크립트 30%로 구현되어 있고, libuv라이브러리는 100% C++로 구현되어 있다.
- libuv 라이브러리는 노드의 특성인 이벤트 기반, 논블로킹 I/O모델을 구현하고 있다.
NodeJS를 크게 나누어보면, 내장 라이브러리와 V8엔진, libuv로 구성된다. NodeJS의 특성인 이벤트 기반, Non-Blocking I/O 모델들은 모두 libuv 라이브러리에서 구현된다.
NodeJS에서 작성되는 거의 모든 코드들은 콜백 함수로 이루어져 있는데, 콜백 함수들은 libuv 내에 위치한 이벤트 루프에서 관리 및 처리가 된다. 이벤트 루프는 여러 개의 페이즈들을 갖고 있으며, 해당 페이즈들은 각자만의 큐를 가지고 있다.
이벤트 루프는 라운드 로빈 방식으로 노드 프로세스가 종료될 때까지 여러 페이지들을 계속 순회한다. 페이즈들은 각각의 큐들을 관리하고 각각의 큐는 FIFO 방식으로 처리 된다.
- 라운드 로빈(Round Robin): 프로세스들 사이에 우선순위를 두지 않고, 순서대로 시간단위로 CPU를 할당하는 방식이다. 보통 시간단위는 10ms ~ 100ms 정도이고 시간 단위동안 수행한 프로세스는 준비 큐의 끝으로 밀려나게 된다. 문맥전환의 오버헤드가 큰 반면, 응단시간이 짧아지는 장점이 있어 실시간 시스템에 유리하고, 할당되는 시간이 클 경우 비선점 FIFO 기법과 같아지게 된다.
- 비선점형 FIFO: 선입선출이며, 먼저 들어온 작업이 끝나지 않으면, 다음 작업을 실행 될 수 없는 비효율적인 방식이다.
논블로킹 I/O
NodeJS에서 논블로킹 I/O 모델은 Input Output이 관련된 작업(DB CRUD, 파일 시스템)등의 블로킹 작업들은 백그라운드에서 수행하고 이를 비동기 콜백 함수로 이벤트 루프에 전달하는 것을 의미한다.
I/O 작업들은 OS 커널 or libuv 내의 스레드 풀에서 담당하게 된다. libuv는 OS 커널에서 어떤 비동기 작업들을 지원하는지 파악되어 있으므로, 작업을 OS 커널 or 스레드 풀로 분기한다. libuv의 스레드풀은 커널이 지원안하는 작업들을 수행하게 된다. 또한 libuv의 스레드 풀은 멀티 스레드로 이루어져 있다. 예를 들어 파일 시스템은 libuv에서 처리되게 되고 스레드풀도 마찬가지로 작업을 마친 후 이벤트 루프에 콜백 함수를 전달하게 된다.
libuv의 동작
NodeJS는 기본적으로 libuv 위에서 동작하며, node 인스턴스가 생성될 때, libuv에는 스레드 풀이(기본값은 4개의 스레드)가 생성된다. libuv는 OS 커널이 어떤 비동기 작업들을 지원해주는지 알고 있기에 libuv의 스레드는 커널이 지원하지 않는 작업들을 수행하게 된다.
이벤트 루프의 내부 동작 과정은?
출처: https://developer.ibm.com/tutorials/learn-nodejs-the-event-loop/
이벤트 루프는 6phase들로 구성되어 있다. 각 phase 들은 Queue를 가지고 있고, 이벤트 루프가 해당 phase를 라운드 로빈 방식으로 순환하면서 실행되게 된다.
- timer
setTimeOut(), setInterver() 같은 timer 함수들이 처리가 된다. - I/O callbacks
close callback, timer로 스케쥴링된 callback, setImemediate를 제외한 거의 모든 콜백들이 큐에 놓이게 된다. (http, api 호출, db 조회 등) - poll
poll 큐에 있는 이벤트 콜백들을 처리한다. 이때 poll에 쌓인 콜백 함수들을 실행하게 되는데 더 이상 실행할 콜백 함수가 없을 때에는 규칙에 따라 다음 단계로 넘어가거나 대기하게 된다. 일단 check 단계를 검사하여 setImmediate가 있는지 확인하고 setImmediate가 있으면 check 단계로 넘어가게 된다. 만약 setImmediate가 없다면 timer 단계에 실행할 timer 함수가 있는지 확인하게 된다. timer 단계로 넘어갈 수 있을 때까지 대기하고 도중에 poll 큐에 콜백함수가 들어온다면 즉시 실행합니다. - check
setImediate() 콜백이 호출되고 실행된다. - close callbacks
.on(‘close’) 같은 이벤트에 따른 콜백 함수를 실행한다.
setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(() => {
console.log("immediate");
});
위 코드를 실행하면 setTimeout()은 timer영역에 들어가고 setimmediate()는 check영역에 들어간다. 이벤트 루프가 돌고 있는 시점에 따라 다르게 되는데 timer 단계라면 setTimeout이 먼저 실행되고 그렇지 않다면, setImmediate가 먼저 실행되게 된다.
아래는 NodeJS 공식 홈페이지에 나와있는 설명이다.
각 단계는 실행할 콜백의 FIFO 큐를 가집니다. 각 단계는 자신만의 방법에 제한적이므로 보통 이벤트 루프가 해당 단계에 진입하면 해당 단계에 한정된 작업을 수행하고 큐를 모두 소진하거나 콜백의 최대 개수를 실행할 때까지 해당 단계의 큐에서 콜백을 실행합니다. 큐를 모두 소진하거나 콜백 제헌에 이르면 이벤트 루프는 다음 단계로 이동합니다.
이러한 작업이 또 다른 작업을 스케줄링하거나 poll 단계에서 처리된 새로운 이벤트가 커널에 의해 큐에 추가될 수 있으므로 폴링 이벤트를 처리하면서 poll 이벤트를 큐에 추가할 수 있습니다. 그 결과 오래 실행되는 콜백은 poll 단계가 타이머의 한계 시점보다 훨씬 더 오래 실행되도록 할 수 있습니다.
NodeJS가 시작되면 스레드가 생기고, 이벤트 루프가 생성된다. 이벤트 루프는 6개의 페이즈를 라운드 로빈 방식으로 순회하며 동작한다고 언급했었는데, 이제 각각의 페이즈들이 세부적으로 어떤 역할을 수행하는지에 대해 알자보자.
-
timers
setTimeout()
과setInterval()
같은 timer 함수들이 처리된다. 이벤트 루프가 페이즈를 순회하면서 timer 단계에 오면 처리할 수 있는 timer함수들을 확인하고 콜백함수를 실행한다. timer함수의 입력된 지연시간은 콜백함수가 실행되는 정확한 값이 아니다. 단지 해당 지연시간 이후에 실행된다는 기준시간의 의미와 같다. -
pending callbacks
이단계에서는 다음 루프 반복으로 연기된 I/O 완료 결과가 큐에 담긴다. I/O 작업 완료되면 다음번 루프에 이 단계에 들어와 있게 되고, I/O 작업 블랙내의 콜백함수들을 poll 단계의 큐로 넘겨준다. 또한 TCP 오류같은 시스템 작업의 콜백을 실행한다.
-
idle, prepare 해당 단계는 내부용으로만 사용된다.
-
poll poll 단계에서는 I/O와 연관된 콜백(클로즈 콜백, 타이머로 스케쥴링된 콜백,
setImmediate
를 제외한 거의 모든 콜백)을 실행한다. 또한 timer 단계에서의 실행 시간 제어를 담당한다. 이 단계에서는 poll 큐에 쌓인 콜백함수들을 한도가 넘지 않을 때까지 모두 동기적으로 실행한다. 만약 한도가 넘거나, 더 이상 실행할 콜백함수가 없을 때는 별도의 규칙을 따라, 다음 단계로 넘어가거나 대기한다.- check 단계를 검사하여
setImmediate()
가 있는지 확인한다. - check 단계에
setImmediate()
에 있는 경우에는 check 단계로 넘어간다. - 만약 없다면, timer 단계에서 실행할 timer 함수가 있는지 확인한다.
- timer 함수를 실행할 수 있는 시간까지 대기한 후에, timer 단계로 넘어간다. 대기하는 동안에 poll 큐에 콜백함수가 쌓인다면 즉시 실행한다.
- check 단계를 검사하여
-
check
setImmediate()
의 콜백함수가 실행된다. 위에서 언급한대로, 이벤트 루프가 poll 단계에서 작업을 수행한 뒤, poll 단계가 유휴상태가 되었다면 poll 이벤트를 기다리지 않고 check 단계로 넘어가게 된다. -
close callbacks close 이벤트에 따른 콜백함수를 실행한다. socket.on(‘close’,…) 이벤트에 따른 콜백함수를 예로 들 수 있다.
const fs = require("fs");
fs.readFile(__filename, () => {
setTimeout(() => {
console.log("timeout");
});
setImmediate(() => {
console.log("immediate");
});
});
I/O block내에서는 poll단계에서 실행되기 때문에, setImmediate()
가 먼저 실행된다.
참고
https://medium.com/@vdongbin/node-js-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC-single-thread-event-driven-non-blocking-i-o-event-loop-ce97e58a8e21
https://ninjaggobugi.tistory.com/7
https://codingjuny.tistory.com/58
댓글남기기