1. Node의 핵심 개념

노드는 브라우저가 아닌 다른 환경에서 Javascript를 사용할 수 있게 해주는 런타임이다.
크롬의 V8엔진libuv(비동기 I/O 라이브러리)를 추가해 만들어졌다.

Node의 특징은 다음과 같다.

  • 싱글 스레드이며 이벤트 루프 기반
  • 논 블로킹(Non-blocking) 방식으로 동작
  • Javascript를 사용함

V8 엔진이란

V8 엔진은 크롬에서 자바스크립트를 실행 시키기 위한 런타임으로써 만들어졌다.
V8은 데이터 타입, 연산자, 오브젝트, 함수 등 ECMA 표준을 지원한다.
(우리가 Node에서 Javascript를 사용할 수 있는 이유!)

소스 코드 해석 순서

V8 Pipeline


자바스크립트 런타임

Node.js는 Chrome V8 Javascript 엔진으로 빌드된 Javascript 런타임입니다.
[https://nodejs.org/ko]

런타임이란 프로그래밍 언어가 구동되는 환경이다.

Node.js로 인해서 브라우저가 아닌 곳에서 자바스크립트를 실행할 수 있게 되어, 서버와 같은 다른 프로그램을 만들 수 있게 됐다.

  • Node 내부 구조

노드의 이벤트 기반, 논 블로킹 I/O는 libuv를 통해 구현되어져있다.

  • Abstract Syntax Tree (AST)
    추상 구문 트리

  • Ignition
    V8의 인터프리터로써 코드를 한줄씩 읽으면서 AST를 바이트코드로 변환해준다.
    코드를 바이트코드로 변환하기 위해서 초기 실행은 느릴 수 있지만 그 이후부터는 거의 컴파일 언어에 가까운 성능을 보인다.

  • TurboFan
    V8의 최적화 담당 컴파일러. 프로파일러(=코드분석기)가 변환된 바이트코드에서 최적화가 가능한 코드들(반복문, 함수 등)을 찾아서 터보팬에게 보내준다.
    터보팬은 이 코드를 받아서 최적화된 코드(Optimized Machine Code)를 생성한다.
    (예제)open in new window

이처럼 인터프리터로 실행하되 필요할 때 컴파일 하는 방법을 JIT(Just-In-Time) 컴파일러 라고 부른다.


이벤트 기반 (event-driven)

자바스크립트의 이벤트 기반 모델은 크게 4가지 요소로 나눌 수 있다.
/images/TIL/javascript-runtime.gif

  • 호출 스택 (Call Stack)
    코드를 읽어내려가면서 수행해야 할 작업들을 쌓는 공간

  • 백그라운드 (Web API 등등..)
    Timer, EventListener, Promise 같은 비동기 작업을 처리한다.

  • 테스크 큐 (Callback Queue)
    완료 된 콜백함수들이 대기한다.

  • 이벤트 루프
    콜백을 적절한 장소로 옮겨준다.
    이벤트가 발생하면 콜백함수를 호출하여 실행하고, 이벤트를 다 처리하면 다음 이벤트가 발생할 때까지 기다린다.

TIP

  1. 이벤트 루프는 자바스크립트 엔진 내부가 아닌, 브라우저nodejs가 담당한다.
  2. 자바스크립트는 이벤트루프 안에서 실행된다.

이벤트 루프는 백그라운드의 작업을 기다리지 않기 때문에 Non-Blocking의 특징을 가지고 있다.

논 블로킹 I/O : 호출한 함수가 바로 return 되어 다음 작업으로 넘어감
비동기 : 백그라운드 작업 완료 여부를 신경쓰지 않음
(백그라운드에서 알림을 줘야만 처리해줌)

백그라운드에서 작업을 먼저 끝냈더라도 콜 스택이 비어있지 않다면, 이벤트 루프는 콜 스택이 빌 때까지 기다리기 때문에 실행 시간을 보장받지 못한다.

setTimeout을 0초로 해도 마찬가지다. 0초라 할지라도 setTimeout이기 때문에 백그라운드로 콜백이 이동되고 호출스택이 비워질때까지 기다려야한다.

  • Mirco Task Queue
    Promise의 콜백은 이 곳에 쌓이는데, Task Queue보다 우선적으로 실행시켜준다. microtask-queueeventloop-promise

싱글 스레드

  • 프로세스 : OS가 관리하는 작업의 최소 단위이다. 다른 프로세스와 자원(메모리 등)을 공유하지 않는다.
  • 스레드 : 프로세스 내에서 실행되는 흐름의 단위이다.

Node는 싱글 스레드로 동작한다.
대신 백그라운드(web api 등..)에게 비동기 API들을 처리하게 넘겨준다. 이런식으로 백그라운드에서 일을 처리하는 방식으로 싱글 스레드의 한계를 극복한다.


스레드풀

스레드 관련 요청이 들어올 때 마다 스레드를 생성하고 삭제하는 비용을 줄이기 위해 스레드를 미리 만들어 놓는 것을 뜻한다.

libuv가 비동기 I/O 작업 요청을 받는데, libuv는 운영체제의 커널에게 작업을 처리해달라고 전달한다.

스레드풀은 커널수준에서 논블로킹을 지원하지 않는 I/O를 처리하기 위해 사용한다.

그런데 커널이 해당 요청에 대해 비동기 API를 지원하지 않는다면 libuv의 스레드풀에서 작업을 처리한다.

즉, 논블로킹을 지원하는 I/O만 커널에서 처리하고 나머지는 libuv 스레드풀의 워커스레드에서 처리된다.

단적인 예

File System I/O
커널수준에서 해당 API를 지원하지 않아 스레드풀에서 동작한다.

Network I/O
스레드풀이 아닌 커널에서 처리된다.

커널의 비동기 지원 유무작업 처리 장소
O커널
Xlibuv의 스레드풀(워커스레드)
> node threadpool
1: 1504
2: 1542
4: 1542
3: 1575
5: 3016
6: 3130
7: 3131
8: 3215

실행할 때 마다 시간과 순서가 달라지는데 스레드풀이 동시에 작업을 처리하기 때문이다.
1~4, 5~8이 각각 그룹으로 묶여서 실행된걸 볼 수 있는데 이는 스레드풀의 default 개수가 4개이기 때문이다.

스레드 풀의 개수 또한 조절할 수 있는데
mac의 경우 UV_THREADPOOL_SIZE=개수로 스레드풀의 크기를 설정해줄 수 있다.


서버로서의 노드

  • 장점
    • I/O 작업들을 동시에 처리할 수 있어 유리하다.
    • 자체 웹서버가 내장되어 있다.
    • Front단과 언어가 통일된다.
  • 단점
    • CPU 부하가 큰 작업은 스레드 하나에서만 처리되므로 성능이 떨어진다.
    • 멀티 스레드를 구현할 경우 설계가 어렵고 성능도 뛰어나진 않다.

즉, 노드는 네트워크나 데이터베이스, 디스크 작업 같이 개수는 많지만 크기는 작은 데이터를 다루는 작업 위주로 이용하는 것이 좋다.


Reference

로우 레벨로 살펴보는 Node.js 이벤트 루프open in new window
nodejs의 내부 동작 원리 (libuv, 이벤트루프, 워커쓰레드, 비동기)open in new window
이벤트 루프와 태스크 큐 (마이크로 태스크, 매크로 태스크)open in new window

Last Updated: