본문 바로가기
개인공부/NodeJs

Node.js가 작동하는 원리

by BuyAndPray 2021. 7. 11.
반응형

Node.js란?

Node.js의 공식 사이트에 따르면 Node.js는 아래와 같이 정의할 수 있다.

 

Node.js®는 Chrome V8 JavaScript 엔진으로 빌드된 JavaScript 런타임입니다.

 

여기서 의미하는 런타임(Runtime)이란 특정 언어로 만든 프로그램을 실행할 수 있는 환경을 의미한다. 이전까지 JavaScript는 웹 브라우저에서만 사용되는 스크립팅 언어였지만, 구글이 성능이 뛰어난 V8 엔진을 출시하고 나서는 속도 문제가 많이 해결되었다. 그 결과로 웹 브라우저 외의 환경에서 JavaScript를 실행할 수 있는 프로그램이 개발될 수 있었고, 그것이 Node.js이다.

Node.js

Node.js를 설명하는 글을 보면 Node.js는 싱글 스레드 모델(single-thread), 논 블로킹 I/O(non-blocking I/O), 이벤트 기반(event-driven)의 특징을 가지고 있다고 말한다. 그렇다면 각각을 통해 어떻게 Node.js가 작동하는지 살펴보도록 하겠다.

싱글 스레드 모델(single-thread)

사실 제일 처음 Node.js가 싱글 스레드 기반으로 돌아간다고 들었을 때는 꽤 충격적이었던 것 같다. 사실 Node.js는 Javascript 런타임이기 때문에 Node.js가 싱글 스레드 기반으로 돌아간다는 말은 Javascript 또한 싱글 스레드 모델로 실행되고 있다는 말이다. 

 

스레드는 프로세스의 실행 흐름으로 식당의 예를 들면 Node.js라는 이름의 음식점이 있다고 가정하자. 그럴 때 싱글 스레드의 의미는 이 Node.js 음식점의 종업원 수는 오직 한 명이라는 것이다!! 이와 반대로 Spring MVC라는 식당은 여러 명의 종업원이 손님을 응대하고 있다.

 

서버라는 것은 클라이언트의 요청(식당으로 따지면 손님)을 처리해주는 컴퓨터를 말하는데 Node.js처럼 종업원이 한 명이라면 효율적으로 요청을 처리를 해주지 못할 것으로 생각했다. 

Node.js vs. Spring MVC

하지만 이는 필자가 Node.js를 싱글 스레드 + 블로킹 I/O 모델로 봤기 때문이었다. 즉 필자는 A와 B라는 두 클라이언트의 요청이 들어왔을 때 Node.js가 A의 요청을 모두 처리 한 뒤 B의 요청을 처리하는 식으로 진행될 것으로 생각했지만, 이는 틀린 생각이었다. Node.js의 내부 구조를 살펴보면, V8 엔진 외에 libuv라는 라이브러리가 있는 것을 확인할 수 있다. 그리고 이 libuv 라이브러리가 Node.js의 특성인 이벤트 기반과 논 블로킹 I/O를 구현하고 있다.

노드의 내부 구조 [출처: Node.js 교과서]

논 블로킹 I/O(non-blocking I/O)

블로킹과 논 블로킹은 Node.js 공식문서에서 잘 설명해주고 있다.

블로킹은 Node.js 프로세스에서 추가적인 JavaScript의 실행을 위해 JavaScript가 아닌 작업이 완료될 때까지 기다려야만 하는 상황입니다.

그렇다면 반대로 논 블로킹은 추가적인 JavaScript 실행을 위해 JavaScript가 아닌 작업이 완료될 때까지 기다리지 않아도 되는 상황을 말한다. 예를 살펴보면 아래와 같다.

// 블로킹 예시
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // 파일을 읽을 때까지 여기서 블로킹됩니다.
console.log(data);
moreWork();
// moreWork 는 console.log(data)가 실행되고 실행됩니다.

 

// 논 블로킹 예시
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
moreWork();
// moreWork();는 console.log 이전에 실행될 것입니다.

위의 블로킹 예시에서는 file.md를 다 읽을 때까지 전체 JavaScript 시스템이 멈추지만, 아래 논 블로킹 예시에서는 비동기적으로 실행되기에 전체 JavaScript 시스템이 멈추지 않고 moreWork()가 먼저 실행된다. 그리고 file.md를 다 읽고 나서 fs.readFile의 두 번째 파라미터로 들어간 콜백(callback) 함수가 실행된다. 여기서 콜백 함수란 어떤 이벤트(이 경우에서는 file I/O)가 끝나고 나서 실행되는 함수를 말한다. 

이벤트 기반(event-driven)

위처럼 어떤 특정 이벤트가 발생할 때 미리 지정해둔 작업을 수행하는 방식을 이벤트 기반(event-driven)이라고 한다. 즉 노드는 이벤트 기반 방식으로 동작하기 때문에, 어떤 이벤트가 발생하면 이벤트 리스너에 등록해둔 콜백(callback) 함수를 호출한다. 이때 이 호출할 콜백 함수들을 관리하고, 호출된 콜백 함수의 실행 순서를 결정하는 역할을 담당하는 것이 이벤트 루프(event-loop)이다.

이미지 출처 :  https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf

이해하기 쉽게 그림을 통해 설명하자면(Node.js 런타임 말고 웹 브라우저 런타임 기준의 그림) JavaScript는 싱글 스레드 기반의 언어이기 때문에 단 한 개의 Call Stack을 가지고 있다. 따라서 이 Call Stack의 상태를 잘 유지해줘야 한다. 만일 엄청나게 시간이 오래 걸리는 연산이 Call Stack에서 돌아가고 있다면, 다른 JavaScript 코드들은 실행되지 못하고 블로킹(Blocking) 되어 있을 것이다. 따라서 블로킹을 일으키는 이벤트(DOM, AJAX 등)들은 Web APIs들을 통해 메인 스레드를 블로킹시키지 않고 비동기적(Asynchronous)으로 실행된다. 이 이벤트들은 모두 콜백(callback) 함수를 지니고 있는데, 콜백 함수는 특정 이벤트가 발생했을 때 수행되는 함수를 의미한다. 그리고 이 콜백 함수들은 백그라운드에서 이벤트 처리가 끝난 뒤 태스크 큐(Task Queue)에 순차적으로 쌓이게 되고, 이벤트 루프(Event Loop)는 Call Stack이 비어있을 때 태스크 큐에서 콜백을 꺼내 Call Stack에 올리는 역할을 수행한다.

 

조금 길었지만 이러한 과정을 통해 웹 브라우저 위에서 JavaScript가 수행되고 있다. Node.js에서도 전체적으로 비슷한 구성을 가지고 있다. 다만 Node.js에서는 이벤트 루프와 비동기 I/O를 libuv 라이브러리가 담당하고 있다. 또 여기서 하나 알아둬야 할 것이 사실 따지고 보면 Node.js 또한 따지고 보면 싱글 스레드가 아니라는 말이다.

 

Node.js 프로세스가 실행되면 내부적으로 스레드를 여러 개 생성한다. 하지만 이 중에서 우리가 직접 제어할 수 있는 스레드는 단 한 개이기 때문에 Node.js를 싱글 스레드라고 부른다. 그리고 나머지 스레드들은 내부적으로 블로킹되는 작업들(file I/O, DB 등)을 수행하고, 그 콜백을 이벤트 루프에 등록한다.

 

이미지를 포함한 더 자세한 내용은 다음 블로그에 잘 나와있어서 이 블로그를 참고해주길 바란다.(https://sjh836.tistory.com/149)

정리

정리하자면 Node.js는 우리가 직접 제어할 수 있는 스레드가 단 한 개이기 때문에 싱글 스레드 기반이라고 부른다. 그렇기 때문에 효율적으로 클라이언트의 요청을 처리하기 위해서는 이 메인 스레드가 블로킹되거나 멈추지 않게 잘 관리해야 한다. Node.js는 libuv 라이브러리를 통해 이벤트 기반, 논 블로킹 I/O 모델을 구현하고 있다. 메인 스레드를 통해 JavaScript 코드를 처리하다 블로킹 I/O(외부 API 요청, DB read/write, File I/O 등)를 만나게 되면, 이벤트 루프가 해당 작업을 백그라운드로 보내게 된다. 그리고 백그라운드에서 OS 커널이 비동기 작업을 지원해주게 되면 커널 단의 함수를 호출한다. 만일 커널이 지원해주지 않을 경우, Node.js를 실행했을 때 생긴 다른 워크 스레드들이 해당 작업을 수행하고 작업이 끝난 뒤 콜백 함수를 다시 이벤트 큐에 등록한다. 그렇다면 마지막으로 이벤트 루프가 이벤트 큐에 등록된 콜백 함수를 메인 스레드로 올리는 역할을 하게 된다.

 

이렇게 Node.js는 새로운 스레드를 생성하거나 멀티 스레드를 관리하는 데 필요한 작업(lock)을 수행하지 않아도 되기에 다른 언어에 비해 적은 오버헤드를 가지고 있고, 확장성이 크며 개발하기가 쉽다. 다만 블로킹 때문에 CPU 연산을 많이 요구하는 작업에는 적합하지 않다는 단점을 지니고 있다.

 

참고자료

https://www.geeksforgeeks.org/how-node-js-works-behind-the-scene/

<What the heck is event-loop>

https://www.geeksforgeeks.org/why-node-js-is-a-single-threaded-language/

https://sjh836.tistory.com/149

 

반응형

댓글