Home Node.js란?
Post
X

Node.js란?

Node.js의 동작 원리에 대해 알아보자.


Node.js

Node.jsChrome V8 Javascript 엔진으로 빌드된 Javascript 런타임입니다.

여기서 런타임이란 프로그래밍 언어가 구동되는 환경을 말합니다.

즉, 웹 서버의 개념이 아닌 Javascript로 서버를 구축하고 서버에서 작동되도록 해주는 런타임 환경입니다.

이전까지는 웹 브라우저에서만 실행됐지만, Node.js의 등장으로 브라우저 외의 환경에서도 javascript로 작성된 파일을 구동할 수 있게 되었습니다.


Single Thread Non-blocking

위의 설명에서 좀 더 나아가면 Node.js는 싱글 스레드의 논블로킹 모델로 구성되어 있습니다.

싱글 스레드란?
프로세스 내에서 하나의 스레드로 하나의 요청만을 수행하는 것입니다.

블로킹이란?
하나의 요청이 수행되는 동안 다른 요청을 함께 수행할 수 없는 방식을 말합니다.

Node.js는 싱글 스레드지만 비동기 입출력을 통해 블로킹이 일어나지 않습니다.

동시에 많은 요청을 비동기로 수행하여 싱글 스레드지만 논 블로킹을 가능하게합니다.

또한, Node.js는 클러스터링을 통해 프로세스를 fork하여 멀티 스레드처럼 사용할 수도 있습니다.


Node.js의 내부 구조

Node.js

Node.js는 싱글 스레드인데 어떻게 동시성을 가질 수 있을까요?

Node.js는 내장 코어 라이브러리V8 엔진, 그리고 libuv로 구성되어 있습니다.

Node.js의 특징인 이벤트 기반, 논 블로킹 I/O 모델libuv에서 구현됩니다.


libuv 라이브러리

Node.js가 사용하는 비동기 I/O 라이브러리입니다.

운영체제의 Kernel을 추상화한 라이브러리이며 Kernel이 어떤 비동기 API를 지원하는 지 알고있습니다.

즉, 비동기 작업의 요청이 들어오면 해당 작업을 Kernel이 지원하는 지 확인하고 지원한다면 Kernel에 요청했다가 응답이 돌아오면 전달해주는 역할을 합니다.

Kernel이 지원하지 않는 작업은 자신만의 워커 스레드가 담긴 스레드 풀을 사용합니다.

기본적으로 4개의 스레드를 갖는 스레드 풀을 생성합니다.
(uv_threadpool이라는 환경 변수를 설정해 128개까지 늘릴 수 있습니다.)

즉, Kernel과 threadpool을 사용해서 비동기 작업을 처리합니다.

멀티 스레드로 이루어진 libuv의 스레드 풀을 사용하기 때문에 Node.js는 완전한 싱글 스레드라고 볼 수 없습니다.

libuv-threadpool


Node.js의 Event-Loop

Node.js는 I/O 작업을 메인 스레드가 아닌 다른 스레드에 넘겨줌으로써 싱글 스레드로 논 블로킹 I/O 모델을 구현하며 그 기반에는 이벤트 루프가 있습니다.

이벤트 루프는 Node.js가 비동기 작업을 관리하기 위한 구현체로 6가지의 Phase로 구성됩니다.

  1. Timer Phase
    • 타이머에 관한 비동기 작업을 관리 (setTimeout, setInterval 등)
  2. Pending Callbacks Phase
    • 이전 이벤트 루프 반복에서 수행되지 못한 I/O 콜백을 관리
  3. Idle, prepare Phase
    • Node.js의 내부적인 관리를 위한 페이지 (JS 실행 X)
  4. Poll Phase
    • 거의 모든 I/O 콜백을 관리 (즉, 타이머, close 콜백 등을 제외한 모든 콜백)

      Poll Phase는 다른 Phase와 다르게 큐가 비어있어도 잠시 대기할 수 있다.

  5. Check Phase
    • setImmediate의 콜백만을 관리
  6. Close Callbacks Phase
    • close 이벤트 타입의 핸들러를 처리

timers -> pending callbacks -> idle, prepare -> poll -> check -> close callbacks 순으로 호출되며 다음 페이즈로 넘어가는 것을 틱(Tick)이라고 부릅니다.

각 페이즈의 큐에 담긴 작업들을 모두 실행하거나, 시스템의 실행 한도에 도달했을 때 다음 페이지로 넘어갑니다.

Node.js는 싱글 스레드이므로 한번에 하나의 페이지에서 하나의 작업을 수행합니다.
동시에 여러 Phase의 작업을 한번에 여러 개씩 처리하는 것은 불가능합니다.

즉, 각 페이즈는 자신만의 큐를 관리하고 Node.js는 페이지를 순회하며 큐에 쌓인 작업들을 하나씩 실행합니다.
또한 이벤트 루프가 살아있다면 Node.js는 이벤트 루프를 반복합니다.


Node 실행과 이벤트 루프의 흐름 예제

Nodejs-event-loop-flow

test.js 파일을 실행할 때의 흐름을 알아보자.

  1. node test.js 실행 -> Node.jsEvent-Loop를 생성합니다.
  2. 이벤트 루프 바깥에서 전체 코드를 실행한 후 이벤트 루프를 확인합니다.
  3. 이벤트 루프에 남아있는 비동기 작업이 있다면 Node.js는 이벤트 루프에 진입해 Tick을 반복하며 작업을 실행합니다.
  4. 남은 작업이 없다면 Node.jsprocess.on('exit', callback)을 실행하고 종료합니다.

nextTickQueue, microTaskQueue

nextTickQueue와 microTaskQueue는 libuv 라이브러리에 포함되지 않은 Node.js에 구현되어 있습니다.

이벤트 루프의 일부는 아니지만 Node.js의 비동기 작업 관리를 도와줍니다.

  • nextTickQueue : process.nextTick() 의 콜백을 관리하는 특수한 microTaskQueue
  • microTaskQueue : Resolve된 Promise의 콜백을 관리하는 큐

큐들의 우선순위는 nextTickQueue > microTaskQueue > taskQueue 순이다.

  • 간단 예제

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    setTimeout(() => {
      console.log(1);
      process.nextTick(() => {
        console.log(3);
      });
      Promise.resolve().then(() => console.log(4));
    }, 0);
    setTimeout(() => {
      console.log(2);
    }, 0);
    

    Node v11.0.0 이전

    1. timer Phase 진입해 큐에 있는 console.log(1)를 수행합니다.
    2. nextTick()Promise.resolve()는 각각 netTickQueuemicroTaskQueue에 콜백을 등록합니다.
    3. 다시 Timer Phase의 큐를 확인하고 console.log(2)를 수행합니다.
    4. Timer Phase의 큐가 비어있으므로 다음 Phase로 넘어갑니다.
    5. Pending Callbacks Phase에 진입 전 우선순위가 높은 nextTickQueue를 확인한 후 console.log(3)를 수행합니다.
    6. nextTickQueue가 비었다면 다음 우선순위인 microTaskQueue를 확인한 후 console.log(4)를 수행합니다.
    7. nextTickQueue가 비었다면 Pending Callbacks Phase에 진입합니다.

    Node v11.0.0 이후

    1. timer Phase 진입해 큐에 있는 console.log(1)를 수행합니다.
    2. nextTick()Promise.resolve()는 각각 netTickQueuemicroTaskQueue에 콜백을 등록합니다.
    3. 현재 실행하고 있는 작업이 끝났으므로 Node.js는 다시 Timer Phase의 큐를 확인하지 않고 바로 nextTickQueue를 확인한 후 console.log(3)를 수행합니다.
    4. nextTickQueue가 비었다면 다음 우선순위인 microTaskQueue를 확인한 후 console.log(4)를 수행합니다.
    5. nextTickQueue가 비었다면 다시 Timer Phase의 큐를 확인하고 console.log(2)를 수행합니다.
    6. 현재 실행하고 있는 작업이 끝났으므로 Node.js는 nextTickQueuemicroTaskQueue에 작업이 있음을 확인한 후 Timer Phase의 큐가 비었음을 확인하고 Pending Callbacks Phase로 넘어갑니다.

v11.0.0 이전

  • 한 페이즈에서 다음 페이즈로 넘어가는 시점에 nextTickQueuemicroTaskQueue를 확인합니다.

v11.0.0 이후

  • 현재 실행 중인 작업이 끝나는 즉시 nextTickQueuemicroTaskQueue에 담긴 작업을 콜 스택에 push 합니다.

참조

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.