평소 Node.js를 이용해 백엔드를 개발했지만...
NodeJS에 대해 단순히 이벤트 기반이고 비동기식이다라고 밖에 모르고 사용했었던 것 같아 자신에게 반성하며 장단점과 특징에 대해 자세하게 정리하려고 합니다.
정리하고 나니 근질근질했던 부분이 싹 가라앉는 것 같아 기분이 좋습니다. 😄
Node.js는 Chrome V8 JavaScript 엔진으로 빌드된 JavaScript 런타임입니다.
Node.js 홈페이지에서 소개하는 Node.js의 정의입니다.
Chrome V8 Javascript 엔진이 무엇인지는 모르겠지만 "Jvascript 런타임"이라는 것은 이해할 수 있습니다.
즉 Node.js는 Javascript를 실행할 수 있는 환경입니다.
Node.js가 세상에 나오기 전까지는 Javascript을 실행하는 환경(런타임)은 브라우저뿐이었지만 Node.js를 이용해 브라우저 외에서도 Javascript를 실행할 수 있습니다. 👍
"Node.js는 웹서버다"라는 오해가 있는데, Node.js는 Javascript 런타임일뿐 웹서버가 아닙니다. 웹서버'도' 만들 수 있는 것이죠!
노드에 대해 조금 더 상세한 설명을 보면
Node.js는 이벤트 기반, 논 블로킹 I/O 모델을 사용해 가볍고 효율적입니다.
가볍고 효율적이라는 것은 알겠는데 이벤트 기반, 논 블로킹 I/O 모델은 무엇이고 왜 효율적이라는걸까요?
이벤트 기반, 논 블로킹에 대해 알아보기 전에 프로세스와 스레드에 대해 정확히 알아야 합니다.
참고로 Javascript, Node.js는 싱글 스레드입니다.
프로세스는 운영체제로부터 자원을 할당받는 작업의 단위고
스레드는 할당받은 자원을 이용하는 실행의 단위입니다. 하나의 프로세스는 여러 개의 스레드를 가질 수 있으며 자원을 공유합니다.
즉 애플리케이션 하나가 프로세스고 그 안에서 분기 처리가 스레드인 셈입니다.
Node.js와 반대인 멀티 쓰레드, 블로킹 I/O 모델을 보면 Node.js가 왜 가볍고 효율적인지 쉽게 이해할 수 있습니다.
Blocking I/O
대부분의 애플리케이션은 Blocking I/O를 사용합니다. Blocking I/O란 하나의 프로세스가 어떤 자원을 사용하고자 할 때 그 자원을 다른 프로세스가 점유하고 있다면, 그 프로세스가 자원의 사용을 마칠 때까지 기다리는 것을 말합니다.
내가 쓰고자 하는 자원을 다른 사람이 자원을 쓰고 있다면 자원의 사용을 마칠 때까지 기다린다는 것이죠.
기다리는 상태를 Blocked 되었다고 표현하며 아무것도 하지 않는 상태가 됩니다.
멀티 쓰레드
보통 하나의 프로세스가 하나의 요청에 대해 대응하고 그 일을 처리하게 되는데, 웹서버와 같이 다수의 요청이 들어오게 된다면 멀티 쓰레드라는 개념을 사용할 수밖에 없습니다.
멀티 쓰레드는 말 그대로 쓰레드 여러 개가 동시에 실행되어 요청을 처리한다는 개념입니다.
한 명이 일하는 것보다 여러 명이 일하는 것이 성능이 좋겠죠?
하지만 멀티 쓰레드를 사용하게 되면 많은 자원을 필요로 하게 되고 결국 CPU도 하나의 자원이기 때문에 여러 쓰레드들이 CPU를 점유하기 위해 기다릴 수밖에 없는 근본적인 문제가 있습니다.
위에서 얘기한 것처럼 Blocking I/O는 요청을 하고 응답이 올 때까지 아무것도 하지 않고 시간을 낭비하기 때문에 쓰레드 지연에 대한 문제도 생기게 됩니다.
또 멀티 쓰레드를 위해 스케줄링을 하게 되면 문맥 전환 비용(Context Switch)이 발생하게 됩니다.
쉽게 말해 쓰레드를 분배하기 위해(멀티 쓰레드) 사용하는 스케줄링도 CPU를 이용해 연산이 필요한 작업이기 때문에 쓰레드가 많아질수록 문맥 전환에 따른 성능 저하가 발생한다는 뜻입니다.
잠깐!✋
노드에 장점을 설명하기 위해 블로킹 I/O와 멀티 쓰레드의 단점을 얘기하다 보니 자칫 싱글 쓰레드가 무조건 좋다고 얘기하는 것처럼 보일 수 있지만 당연히 각각의 장단점이 있습니다. 상황에 맞게 사용하는 것이 중요합니다.
이제 Node.js의 장점인 싱글 쓰레드와 이벤트 기반 논 블로킹 I/O 모델에 대해 알아볼까요?
싱글 쓰레드와 이벤트 기반 논 블로킹 I/O
Node.js는 위와 같은 문제들을 해결하기 위해 싱글 쓰레드와 이벤트 기반 논 블로킹 I/O을 채택하였습니다.
싱글 쓰레드를 가진 노드는 I/O 작업이 시작되면 처리 완료에 대한 응답을 기다리지 않고 바로 다음 작업을 실행해버립니다. 작업이 끝날 때까지 기다리지 않아도 되는 것이죠!
대신 이벤트를 통해 수행하여야 하는 작업을 등록해놓고, I/O 작업이 종료되면 이벤트를 감지하여 등록된 작업을 실행하게 됩니다.
이렇게 싱글 쓰레드와 논 블로킹 모델로 가볍고 효율적으로 자원을 사용할 수 있습니다.
다만 싱글 쓰레드의 경우 오류가 발생할 경우 처리를 해줄 사람이 없으므로 큰 문제가 발생할 수 있고 CPU 코어를 모두 활용하지 못한다는 단점이 있습니다. 😂
멀티 쓰레드 - 논 블로킹 방식으로 처리하면 더 좋지 않을까?
라고 생각할 수 있는데 맞는 말입니다. 정확히 말하자면 멀티 스레딩보다 멀티 프로세싱에 가깝습니다.
사실 노드 프로세스는 내부적으로 스레드를 여러 개 가지고 있습니다. 하지만 우리가 직접 제어할 수 있는, 자바스크립트를 실행하는 스레드는 하나(이게 이벤트 루프!)이므로 흔히 싱글 스레드라고 부르는 것입니다.
노드는 스레드를 늘리는 대신, 프로세스 자체를 복사해 여러 작업을 동시에 처리하는 멀티 프로세싱 방식을 택했습니다. js 언어 자체가 싱글 스레드 특성을 띄고 있기 때문이죠.
cluster 모듈과 pm2 패키지를 이용하여 멀티 프로세싱을 가능하게 할 수 있습니다.
이 부분에 대해서는 나중에 따로 정리하고,
이벤트 기반과 논 블로킹 I/O에 대해 이해했으니 이벤트 기반이 어떻게 동작하는 것인지 상세하게 알아보겠습니다.
이벤트 기반
Node.js는 V8이라는 자바스크립트 엔진과 비동기 작업을 처리하는 libuv라는 라이브러리로 이루어져 있습니다.
자바스크립트 엔진은 비동기 처리를 할 수 없습니다. 때문에 비동기로 처리되는 코드를 만날 경우 libuv 라이브러리를 이용해 비동기를 처리하게 됩니다. libuv는 이벤트 기반으로 비동기를 처리하는데 이벤트가 발생할 때 미리 지정해둔 작업을 수행하는 방식을 의미합니다.
이것을 이벤트 리스너에 콜백 함수를 등록한다고 표현합니다.
이벤트 기반 모델에서는 이벤트 루프라는 개념이 등장합니다. 여러 이벤트가 동시에 발생했을 때 어떤 순서로 콜백 함수를 호출할지를 이벤트 루프가 판단합니다.
정리하자면
- Node.js는 Chrome V8 엔진과 libuv library로 이루어져 있으며 libuv에서 비동기를 처리한다.
- libuv는 이벤트 기반으로 비동기를 처리한다.
- 여러 이벤트가 동시에 발생했을 때 순서는 이벤트 루프가 판단한다.
이벤트 루프
아래의 코드에서 결과는 다음과 같습니다.
function run() {
console.log("3초 뒤 실행");
}
console.log("시작");
setTimeOut(run, 3000);
console.log("끝");
/*
시작
끝
3초 후 실행
*/
실행 결과는 쉽게 예측할 수 있지만 호출 스택으로 설명하기는 힘듭니다.
setTimeOut의 콜백인 run이 호출 스택에 언제 들어가는지 알기 어렵기 때문입니다.
이를 이해하기 위해서는 이벤트 루프, 테스크 큐, 백그라운드를 알아야 합니다.
- setTimeOut이 실행되면 타이머와 함께 run 콜백을 백그라운드로 보내고 호출 스택으로 빠집니다.
- 백그라운드에서는 3초를 센 후 run 함수를 태스크 큐로 보냅니다.
- 호출 스택 실행이 끝나 비워지면
- 이벤트 루프가 태스크 큐의 콜백(run)을 호출 스택으로 올립니다.
- 이후 이벤트 루프는 태스크 큐에 콜백이 들어올 때까지 대기합니다.
여기서 하나 알아둬야 될 부분은 3. 호출 스택 실행이 비워지지 않으면 이벤트 루프는 콜백을 호출 스택으로 올리지 않습니다.
따라서 setTimeOut의 실행 시간이 정확하지 않을 수도 있습니다.
- 이벤트 루프 : 이벤트 발생 시 콜백 함수를 관리하고 호출된 콜백 함수의 실행 순서를 결정합니다.
- 태스크 큐 : 이벤트 발생 후 호출되어야 할 콜백 함수들이 기다리는 공간입니다. 콜백들이 이벤트 루프가 정한 순서대로 줄을 서있으므로 콜백 큐라 도고 불립니다.
- 백그라운드: 타이머나 i/o작업 콜백 또는 이벤트 리스너들이 대기하는 곳입니다.
아래의 글을 참고하여 작성하였습니다.
'Node.js' 카테고리의 다른 글
swagger로 API 문서 자동화하기(nodeJS) (0) | 2022.11.01 |
---|---|
NodeJS 환경에서 부하테스트 진행하기(Artillery 이용) (1) | 2022.05.19 |
Node.js(express) 프로젝트 설계하기 (8) | 2021.08.31 |
[Node.js] __dirname is not defined 에러 (0) | 2021.01.27 |
[Node.js] n으로 쉽게 Node 버전변경하기(커맨드 2번으로 끝!) (0) | 2021.01.10 |