NodeJS

[NodeJS] Node.js가 빠른 이유

jwKim96 2021. 9. 25. 10:31

Node.js가 빠르다는 얘기는 많이 들어봤는데, 조금 생각해보면 신기합니다.

C/C++처럼 low level을 직접 제어하지도 않고, Java처럼 최적화된 가상머신에서 실행되는것도 아닌데 말이죠.

게다가 Node.js는 싱글 프로세스이고 자바스크립트의 Main Thread는 1개라고 합니다.

Main Thread가 1개라는 말은, CPU core중에서 한개만 사용할 수 있다는 말인데 어떻게 빠른걸까요?

그 해답은 none-blocking IO에서 찾을 수 있었습니다.

none-blocking IO를 다루기 전 확실히 하고 갈 것은, Node.js는 싱글 스레드가 아니라는 사실 입니다.
Node.js 내부의 Event Loop가 싱글 스레드이고, Node.js는 멀티 스레드를 활용하도록 설계되어있습니다.

Node.js 구조

nodejs structure

(출처 : https://sjh836.tistory.com/149)

위 사진은 Node.js의 구조입니다.

우리가 자바스크립트로 작성한 소스는 런타임에 libuvEvent Loop를 통해서 실제로 실행이 되는데,

여기서 Event Loop가 싱글 스레드 입니다.

로직을 실행하면서 File IO, DB IO, Network IO 등의 blocking 작업이 발생하면 Event Loop

OS에 맞는 System call을 호출하거나 libuv에 포함된 스레드풀의 워커 스레드에게 작업을 위임하고,

Event Loop는 다음 로직을 계속 실행해 나갑니다.

그러던 중 다른 스레드에서 작업이 완료되면 이벤트 루프의 이벤트큐에 callback을 등록하게 되고,

현재 로직의 모든 call stack이 제거되면 이벤트큐에 등록된 callback이 실행됩니다.

이를 Node.js 공식 문서에 나와있는 간단한 코드로 살펴보겠습니다.

none-blocking IO

예제에서는 Node.js 표준 라이브러리에서 제공하는 fs라는 파일입출력 모듈을 사용합니다.

Node.js 표준 라이브러리에서 제공하는 모든 IO 메서드는 none-blocking 방식을 제공하며, 콜백함수를 받습니다.

하지만, 일부 메서드들의 경우에는 같은 작업을 하는 blocking 방식도 제공하며, 이름 끝에 Sync가 붙습니다.

1. 동기 예제

1| const fs = require('fs');
2| const data = fs.readFileSync('/file.md'); // 파일을 읽을 때까지 여기서 블로킹 됨
3| console.log(data);
4| 
5| otherWork();

2. 비동기 예제

1| const fs = require('fs');
2| fs.readFile('/file.md', (err, data) => {
3|     if (err) throw err;
4|     console.log(data);
5| });
6| 
7| otherWork();

위 예제에서 1번 예제에서는 readFileSync라고 하는 blocking방식의 메소드가 쓰였고, 2번 예제 에서는

readFile이라고 하는 none-blocking방식의 메소드가 쓰인걸 알 수 있습니다.

1번 예제에서는 2번라인에서 파일을 읽으며 자바스크립트 실행이 blocking되고, 이 작업이 끝나고 나서야

3번 라인에서 data를 출력하고 otherWork()를 실행합니다.

하지만, 2번 예제에서는 2번 라인의 none-blocking파일 입출력 메소드를 통해서 다른 스레드에 IO작업을 위임하고

바로 다음 작업인 otherWork() 를 실행합니다. 그리고 다른 스레드에서 IO작업이 완료되면, callback이 실행되며

data를 출력하게 됩니다.

위 예제를 간단히 순서를 나열하면 아래와 같습니다.

1. 동기 예제

    1) fs 모듈 호출
    2) 'file.md' 동기 호출
    3) 동기 호출 완료되면 data 출력
    4) otherWork() 실행
    5) call stack 모두 제거됨

2. 비동기 예제

    1) fs 모듈 호출
    2) 'file.md' 비동기 호출
        2-1) 파일 IO 다른 스레드에서 실행
    3) otherWork() 실행
    4) call stack 모두 제거됨
    5) callBack에서 data출력

위 순서에서 보거나, 소스코드 예제를 봐도 비동기 예제가 조금 더 복잡해 보입니다.

하지만, 만약 fileRead가 10초가 걸린다면 어떨까요?

1번 동기 예제는 fileRead와 상관없는 otherWork()가 요청한지 10초가 지나서야 실행이 됩니다.

하지만 2번 비동기 예제는 fileRead를 다른 스레드에 위임하고 바로 otherWork()가 실행됩니다.

그러니까 정리하면,

Node.js는 싱글 스레드인것이 아니라 오히려 멀티 스레드를 아주 잘 활용하고,  
싱글 스레드인 Event Loop에서 none-blocking IO를 수행하기 때문이다.

라고 정리할 수 있을 것 같습니다.