Bami 2023. 9. 25. 15:50
728x90
반응형

Node.JS?

JavaScript를 브라우저 외부에서 실행하는 런타임입니다.

Node.js는 싱글 스레드, 논블로킹 모델을 사용합니다. 이 모델은 비동기 I/O 작업을 통해 여러 요청을 동시에 처리하며, 클러스터링을 통해 확장성을 갖추고 있죠.

싱글스레드?
프로세스 내에서 하나의 스레드가 하나의 요청만을 수행합니다. 한 번에 여러 요청을 수행할 수 없는 것이죠 그래서 싱글 스레드는 블로킹 모델이라고 합니다. 반면 멀티스레드는 스레드 풀에서 실행의 요청만큼 스레드를 매칭 하여 작업을 수행합니다.

 

NodeJS 는 완전한 싱글 스레드인가?

NodeJS는 싱글 스레드이지만 완전한 싱글 스레드를 기반으로 동작하지는 않습니다. 무슨 말일까요?

일부 블로킹 작업들은 libuv의 스레드 풀에서 수행되기 때문이죠. 이를 이해하기 위해서 몇 가지 알아야 하는 사항이 있습니다.

 

이벤트 기반

이벤트 기반이란 이벤트가 발생할 때 미리 지정해둔 작업을 처리하는 방식입니다. NodeJS는 이벤트 리스너에 등록해둔 콜백 함수를 실행하는 방식으로 동작합니다. 즉 이벤트 루프가 이를 가능하게 하죠.

Node JS 내부 구조

NodeJS를 크게 나눠보면 내장 라이브러리와 V8 엔진 그리고 libuv로 구성됩니다. 

NodeJS의 특성인 이벤트 기반, 논 블로킹 I/O 모델들은 모두 libuv 라이브러리에서 구현됩니다.

NodeJS에서 작성되는 거의 모든 코드들은 콜백 함수로 이루어져 있죠. 

콜백 함수들은 libuv 내에 위치한 이벤트 루프에서 관리 및 처리됩니다. 이벤트 루프는 여러 개의 페이즈 들을 갖고 있으며 해당 페이즈

들은 각자만에 큐를 가지고 있습니다. 이벤트 루프는 라운드 로빈 방식으로 노드 프로세스가 종료될 때까지 여러 페이지들을 계속 순회합니다. 페이즈들은 각각의 큐들을 관리하고 각각의 큐는 FIFO 순서로 콜백 함수들을 처리하게 됩니다.

 

논블로킹 I/O

NodeJS에서 논블로킹 I/O 모델은 Input Output 이 관련된 작업(데이터베이스 CRUD, 파일 시스템) 등의 블로킹 작업들은 백그라운드에서 수행하고 이를 비동기 콜백 함수로 이벤트 루프에 전달하는 것을 의미합니다.

I/O 작업들은 OS 커널 혹은 libuv 내의 스레드 풀에서 담당하게 됩니다. libuv는 OS 커널에서 어떤 비동기 작업들을 지원해주는지 알고 있기 때문에 작업 종류에 따라서 커널 혹은 스레드 풀로 분기합니다. libuv의 스레드풀은 커널이 지원안하는 작업들을 수행하게 됩니다. 또한 libuv 의 스레드 풀은 멀티 스레드로 이루어져 있습니다. 예를 들어 파일 시스템은 libuv에서 처리되게 되고 스레드 풀도 마찬가지로 작업을 마친 후 이벤트 루프에 콜백 함수를 전달하게 됩니다.

 

libuv는 어떻게 동작할까

NodeJS는 기본적으로 libuv 위에서 동작하여 node 인스턴스가 생성될 때 libuv 에는 스레드 풀이(기본값은 4개의 스레드)가 생성됩니다. libuv는 OS 커널이 어떤 비동기 작업들을 지원해주는지 알고 있기 때문에 libuv의 스레드는 커널이 지원하지 않는 작업들을 수행하게 됩니다.

 

이벤트 루프의 내부 동작 과정

 

이벤트 루프는 6 phase 들로 구성되어있습니다. 각 phase 들을 FIFO 큐를 가지고 있고 이벤트 루프가 해당 phase를 라운드 로빈 방식으로 순환하면서 실행되게 됩니다.

timers

setTimeOut(), setIntever() 같은 timer 함수들이 처리가 됩니다.

 

I/O callbacks

클로즈 콜백, 타이머로 스케쥴링된 콜백, setImemediate를 제외한 거의 모든 콜백들이 큐에 놓이게 됩니다.(http, api 호출, db 조회 등)

 

poll

poll 큐에 있는 이벤트 콜백들을 처리합니다. 이때 poll에 쌓인 콜백 함수들을 실행하게 되는데 더 이상 실행할 콜백 함수가 없을때에는 규칙에 따라 다음 단계로 넘거가거나 대기하게 됩니다. 일단 check 단계를 검사하여 setImemediate 가 있는지 확인하고 setImemediate 가 있으면 check 단계로 넘어가게 됩니다. 만약 setImemediate 가 없다면 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

가 먼저 실행되게 됩니다.

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

위 코드를 실행하면 결과는 항상 아래와 같게 됩니다.

immediate
timeout
  1. fs.readFile 는 블로킹 작업이고 OS 가 지원하지 않는 비동기 작업 이므로 libuv의 스레드 풀에서 담당하게 됩니다.
  2. 스레드가 작업을 완료한 뒤 I/O callback 영역에 큐에 콜백을 등록합니다.
  3. 이벤트 루프가 I/O callback 영역을 실행할 때 콜백을 poll 영역에 큐에 등록하게 됩니다.
  4. 이제 poll 영역을 실행하게 되면서 I/O callback에서 받은 콜백 내부 로직을 실행하게 됩니다. setTimeout 같은 경우는 timer 등록되게 되고 setImmediate 같은 경우는 check 영역에 들어가게 됩니다.
  5. 이벤트 루프가 큐를 비우고 다음 실행 영역인 check를 확인하게 됩니다. 위에서 말했듯이 check 영역에 실행할 setImmediate 이있기때문에 immediate을 출력하게 됩니다.
  6. timers에 등록되었던 setTimeout의 콜백을 poll 큐에 등록하게됩되고 이벤트 루프가 poll 영역을 실행하고 timeout 을 출력하게 됩니다.
  7. 노드 프로세스가 반환되고 종료됩니다.

참고자료

nodejs.org/ko/docs/guides/blocking-vs-non-blocking/

https://nodejs.org/ko/docs/guides/event-loop-timers-and-nexttick/

728x90
반응형