[JavaScript/Node.js] Node.js의 동작 원리

JuseungL·2024년 1월 9일
0

Node.js

목록 보기
3/8
post-thumbnail

이전에 JavaScript의 동작원리를 Chrome 브라우저 런타임 환경을 기반으로 공부해보았다. 이번에는 내가 실제로 개발할때 사용하는 Node.js 런타임 환경의 작동 원리에 대해서 알아보고자 한다.

Node.js를 왜 썼는데?
비동기 이벤트 주도 방식을 사용하여 성능이 빠르고 자바스크립트 엔진으로 V8 엔진을 사용하여 자바스크립트임에도 불구하고 성능이 좋다.
Node.js는 자바스크립트라는 프로그래밍 언어를 쓰고 있기 때문에 인터프리터 기반의 프로그래밍 언어의 특성상 자바나 다른 컴파일러 기반의 언어로 서비스를 구현하는 것에 비해 느릴 수도 있지만 자바스크립트는 인터프리터 기반의 언어 중에서도 V8엔진을 통해 충분히 빠르고 비동기 이벤트 주도 방식의 특성을 잘 활용하여 코드를 작성한다면 좋은 서비스를 구현할 수 있을 것 같다.

Node.js란?

자바스크립트를 실행시키는 런타임 환경 중 하나로 이때 런타임 환경이란 컴퓨터 프로그램이나 소프트웨어 응용 프로그램이 실행 또는 작동하는 동안 실행되는 환경을 말하며 다른 언어의 예시로는 Java의 런타임 환경은 JRE(Java Runtime Environment)이다.
즉, Node.js를 브라우저 밖에서도 실행할 수 있도록 하는 런타임 환경이다.

생성 계기
JavaScript는 처음에 브라우저에 종속적인 언어였다. 오직 브라우저에서만 해당 언어를 사용할 수 있었다. 브라우저 외부에서 사용하려는 다양한 시도가 있었지만 너무 느려 크게 상용화되지는 못했다. 그러다가 구글에서 속도가 아주 빠른 새로운 자바스크립트 엔진인 V8 엔진을 탑재한 크롬 브라우저를 출시하게 됐다. 이와 동시에 V8 엔진이 오픈소스로 공개되면서 Node 프로젝트가 시작되게 됐고 Node.js가 탄생됐다.

비동기 이벤트 주도 JavaScript 런타임으로써 Node.js는 확정성 있는 네트워크 애플리케이션을 만들 수 있도록 설계되었습니다.

비동기 이벤트 주도(Async Event-driven)?

JavaScript에서 비동기를 처리하는 기술로는 Promise, Async, Await가 있다. 모두 콜백을 기반으로 한다.
Node.js는 기본적으로 싱글 스레드 논 블로킹(Single Thread Non-blocking)이다. 하나의 스레드로 동작하지만, 비동기 방식으로 처리하여 요청들을 서로 블로킹하지 않는다는 것이다. 즉, 동시에 많은 요청들이 들어온다면 요청들을 비동기로 수행함으로써 싱글스레드일지라도 논블로킹이 가능하다. 이것에 대한 자세한 설명은 JavaScript 작동 원리 게시글에 설명이 있다.

Node.js는 완전한 싱글 스레드(Single Thread)인가?

Node.js는 싱글스레드가 맞다.
하지만 js코드를 실행시킬때 오직 하나의 스레드가 관여하는 것은 아니다.
일부 Blocking 작업들은 libuv의 스레드 풀(Thread pool)에서 수행되기 때문이다.

Node.js의 구조

자바스크립트 작동 원리 게시글에서는 런타임이 브라우저인 경우를 가정하고 설명한 것이였다.
그렇다면 Node.js가 런타임일 경우 어떻게 달라지는지 알아보자. 아래는 Node.js의 전체적인 구조를 나타내는 그림이다. 저기에 추가로 Node.js의 라이브러리 들을 추가해주면 될 것 같다.

  • Node.js는 Javascript와 C++언어로 구성되어 있고. V8엔진도 70% 이상의 C++로 구성되어 있으며, libuv는 100%의 C++언어로 구성된 라이브러리라고 한다. JavaScript의 작동 원리에 대해 공부할때 자바스크립트 엔진이 JS 코드를 인터프리팅 해준다고 했는데 여기서는 V8 엔진에서 Javascript를 C++로 해석해준다고 한다. 또한 Node.js의 코어 라이브러리 중 C++으로 작성된 내장 모듈 crypto의 경우에도 process.binding()을 통해 자바스크립트 환경에서 사용될 수 있는 것이라고 한다.

이벤트 루프는 Node.js의 libuv 내에서 있다. Node.js는 싱글스레드이기 때문에 하나의 이벤트 루프를 갖으며, 하나의 스레드가 모든 것을 처리합니다.
JavaScript 코드들은 대부분 콜백 함수로 구성되며 libuv의 이벤트 루프에서 관리되고 처리된다.
이벤트 루프는 여러 개의 페이즈(Phase)들을 갖고 있으며, 해당 페이즈들은 각자만의 큐(Queue)를 갖는다. 이벤트 루프는 라운드 로빈(round-robin) 방식으로 노드 프로세스가 종료될때까지 일정 규칙에 따라 여러개의 페이즈들을 계속 순회한다. 페이즈들은 각각의 큐들을 관리하고, 해당 큐들은 FIFO(First In First Out) 순서로 콜백함수들을 처리한다.

라운드 로빈(round-robin)
3학년 1학기 Operating System에서 Scheduling Algorithm에서 배웠다. 프로세스들 사이에 우선순위를 두지 않고, 순서대로 시간단위로 CPU를 할당하는 방식이였다. 시간 단위동안 수행한 프로세스는 준비 큐의 끝으로 다시 들어가게 된다. Context Switching으로 인한 오버헤드가 큰 반면, 응답시간이 짧아지는 장점이 있어 실시간 시스템에 유리하고, 할당되는 시간이 클 경우 비선점 FIFO기법과 같아지는 스케줄링 알고리즘이였다.

Node.js libuv 스레드 풀(Thread Pool)

그래서 libuv에서 Non-blocking I/O모델이 어떻게 작동되는지 알아보자.
Node.js에서의 논블로킹 I/O 모델은 Input과 Output에 관련된 작업(http, Database CRUD, third party api, filesystem) 등의 블로킹 작업들을 백그라운드에서 수행하고, 이를 비동기 콜백함수로 이벤트 루프에 전달하는 것을 말한다.
이때, OperatingSystem 커널 혹은 libuv의 스레드 풀을 의미한다.
위의 Node.js 구조 그림에서 Worker Threads가 여기서 말하는 libuv 스레드 풀에 해당한다. I/O들은 OS 커널 혹은 libuv 내의 스레드 풀에서 담당한다. libuv는 OS 커널에서 어떤 비동기 작업들을 지원해주는지 알고 있기때문에, 작업 종류에 따라 커널 혹은 스레드 풀로 분기한다. 작업이 완료되면 이벤트루프에게 이를 알려주고, 이벤트 루프에 콜백함수로 등록된다. libuv의 스레드 풀은 커널이 지원하지 않는 나머지 작업들을 수행한다. 위에서 Node.js가 완전한 싱글스레드는 아니라고 했던 이유가 바로 libuv에 있는 스레드 풀은 멀티스레드로 이루어져 있기 때문이다. 여기서 이뤄지는 작업의 대표적인 예로는 파일시스템이 있다. 스레드 풀도 마찬가지로 해당 작업을 수행하면, 이벤트 루프에 콜백함수를 전달한다.

Node.js 이벤트 루프(Event Loop)

위에서 이벤트 루프는 여러개의 Phase로 구성되고 각 Phase에서 각각의 콜백 큐를 관리하고 이 큐들을 라운드 로빈(RR)의 스케줄링 알고리즘을 통해 순회하여 처리한다고 했다.

이때, Phase에는 6가지가 있다.
1. timers phase
setTimeout() 또는 setInterval()을 사용하여 타이머를 설정하면 관련 콜백이 타이머 큐에 배치된다. 이벤트 루프의 타이머 페이즈 동안 타이머가 만료되면 해당 콜백이 실행을 위해 타이머 큐에서 콜 스택으로 로드된다.
2. I/O Callbacks Phase
비동기 I/O 작업(예: 네트워크 요청, 파일 읽기)이 완료된 후 해당 콜백이 I/O 콜백 큐에 배치된다. I/O 콜백 페이즈 동안 이러한 콜백은 실행을 위해 I/O 콜백 큐에서 콜 스택으로 로드된다.
3. Idle, Prepare Phase
일반적으로 외부 사용을 위해 콜백을 큐에 직접 로드하는 작업이 포함되지 않는다. 이는 시스템에서 사용되는 내부 페이즈이며 개발에 직접적인 영향이 끼치진 않는다.
4. Poll Phase
폴링 페이즈 동안 이벤트 루프는 새로운 I/O 이벤트 및 타이머를 확인한다. 만료된 타이머나 완료된 I/O 작업이 있는 경우 해당 콜백은 해당 큐에서 콜 스택으로 로드한다.
5. Check Phase
setImmediate()에 의해 예약된 콜백은 확인 페이즈에 배치된다. 확인 페이즈 동안 이러한 콜백은 확인 큐에서 콜 스택으로 로드됩니다.
6. Close Callbacks Phase
닫기 이벤트(예: socket.on('close', ...)와 관련된 콜백은 닫기 콜백 큐에 배치된다. 닫기 콜백 페이즈 동안 이러한 콜백은 닫기 콜백 큐에서 콜 스택으로 로드된다.

Node.js 런타임과 브라우저 런타임의 가장 큰 차이점은 libuv의 Thread Pool을 통해 처리하냐 Web APIs를 통해 처리하냐인 것 같다.
Node.js의 작업 중 하나가 완료되면 커널이 Node.js에게 알려주어 콜백 함수를 이벤트 루프의 대응되는 큐에 추가하고 특정 Phase에 차례(Tick)가 오면 해당 큐에서 콜백함수를 꺼내서 콜 스택에 추가시키고 콜 스택에서 콜백 함수를 처리하는 프로세스이다.

Reference

https://medium.com/@vdongbin/node-js-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC-single-thread-event-driven-non-blocking-i-o-event-loop-ce97e58a8e21
https://psyhm.tistory.com/9
https://jwprogramming.tistory.com/17

profile
기록

0개의 댓글