[ 백엔드 공부하기 : Node.js ]
NodeJS의 기본 동작 원리와 이벤트 루프, 브라우저 환경을 벗어난 JS 실행.
∇백엔드_NodeJS :
NodeJS의 기본 동작 원리 * 이벤트 루프 * 브라우저 바깥에서의 JS 실행.
목 차
1. 기본 동작 원리
2. 결론정리
◎ Node.JS
√ NodeJS의 기본적인 "컨셉"은 JS의 실행 원리와 비슷합니다.
√ NodeJS의 공식문서에서는 [ "비동기 이벤트 주도 JavaSccript 런타임" 으로써 NodeJS는
확장성 있는 네트워크 애플리케이션을 만들 수 있도록 설계되었습니다. ] 라고 소개되어 있습니다.
√ 한마디로 정리하면,
Node.js는 JavaScript를 브라우저 바같에서도 실행 가능하도록 하는 JavaScript의 런타임"!!
Ⅰ. 기본 동작 원리.
◆ Node는 "싱글 스레드로 이뤄진 이벤트 기반 async & non-blocking js - 런타임" 입니다.
◎ Node의 내부적인 설계가 주로
'파일 단위'를 "모듈 단위"로 나눌 수 있고, 특정 모듈은 기능 단위가 아닌 "시나리오 단위" 로 구성되었다는 점에서
"객체지향적인 설계(OOP)" 를 넘어서 "관점 지향적인 설계 방식(Aspect Oriented Programming)" 이라고 합니다.
1 ) Node.js 의 내부 구조.
① JS로 이루어진 라이브러리 부분.
② JS-C++ 을 연결해주기 위한 "Binding"
③ 거의 C++ 로 이루어진 V8엔진 부분
④ C++로 구성된 이벤트 루프의 핵심 부분인 libuv
2 ) standard library & node bindings
○ " Node Standard Library" 는 JS 언어로 만들어진 라이브러리입니다.
==>> 이를 C/C++ 로 바인딩하는 역할이 'Node Bindings" 입니다.
∇ file I/O를 도와주는 모듈 "fs"를 살펴보면
∇ InternalBinding이 무엇인가?
: JS와 같은 고수준의 언어(코드)는 직접적으로 OS system call을 할 수 없으며,
(= 정확하게는 태생적으로 고수준언어는 저수준 언어를 이해하기 쉽게 바인딩 되어 있는 상태이기 때문)
이를 위해 저수준의 code로 NativeModule을 컨트롤 해야 합니다.
== 내부의 C/C++ 코드를 바인딩한다고 보면 된다고 합니다.
∇ 결론적으로 보면 const binding = internalBinding('fs'); 에서 바인딩 되는 것은
node -> src -> node_file.cc 이며, 실제 저수준의 구현 function & method를 확인할 수 있습니다.
∇ 가령 fs module의 디렉토리를 만드는 method "mkdir"는 MKDir이라는 function이 바인딩 되어있는 것을 확인
가능하고 저수준 코드에서도 확인 가능합니다.
◇ 실제 우리가 작성한 JS 코드가 node runtime에서는
아래 사진과 같은 흐름으로 libuv에 도달하게 됩니다.
◇ 우리는 node run time 덕분에 JS 코드만으로도 C/C++에 쉽게 닿을 수 있고,
C/C++을 사용한 코딩처럼 할 수 있게 되는 것입니다.
3 ) libuv
◆ "이벤트 기반 비동기 처리"를 가능하게 해주는 핵심 부분 입니다.
:: 비동기 I/O를 지원하는 "C언어 Library"로 윈도우, 리눅스 커널을 Wrapping하여 추상화한 구조.
- 커널의 비동기 API로 지원할 수 없는 작업을 비동기화 하기 위한 "별도의 Thread Pool"을 가지고 있고
"Event Loop" & "Event Queue"를 관리합니다.
- MKDir은 MKDirpSync 를 호출하고, 이는 내부적으로 env -> event_loop() 라는 argument를 넘깁니다.
- 헤더파일 선언부 [ node -> src -> node_file.h ]를 보면 아래 코드를 볼 수 있습니다.
int MKDirpSync(uv_loop_t* loop,
uv_fs_t* req,
const std::string& path,
int mode,
uv_fs_cb cb = nullptr);
- 'uv_fs_t' 같은 것을 Watcher 라고 하며, [ node -> deps -> uv -> include -> uv.h ] 에 존재.
- node는 파일시스템, 네트워킹, 스레드, 프로세스, 이벤트 루프, I/O 폴링, 시스템 워쳐(Watcher), TTY, DNS 과
관련된 Watcher 구조체를 확인 가능합니다.
- Watchers가 갖는 구조체 이름은 uv_type_t 로 정의돼 있는데 이름 내에 type이라는 이름 대신
앞선 목록에 해당하는 각각의 구조체가 적용된다.
4 ) libuv 구조 & Event Loop
● 커널이 지원하는 비동기 작업을 "libuv"에게 요청하면
"libuv"는 대신 커널에게 이 작업을 비동기적으로 요청해줍니다.
● 만약, 커널이 지원하지 않는 비동기 작업을 "libuv"에게 요청하면,
"libuv" 는 내부에 가지고 있는 스레드 풀에게 이 작업을 요청해줍니다.
● 우리가 봐야할 점은, "libuv" 는 "Event Loop"를 가지고 있다는 점 입니다.
● JS가 브라우저에서 작동할 때 활용되는 Event Loop처럼, "libuv"-" Event Loop" 도 각 요청을 특성에 맞게
"커널"이나 "Thread-Pool"에 위임 하고, 실행 대기중인 callback을 Event Queue에 모았다가
Main Thread에 의해 실행가능하도록 call stack 으로 옮기는 역할을 합니다.
★ Event Loop Phases
● Event Loop는 모든 Callback을 하나의 Event Queue에 담아서 관리하지 않습니다.
● Event Loop는 위와 같은 "Phases" 라고 부르는 단계들로 구성.
√ 각 단계는 실행할 콜백의 FIFO 큐를 가집니다.
- 각 단계는 자신만의 방법에게 제한적이므로, 보통 이벤트 루프가 해당 단계에 진입하면
해당 단계에 한정된 작업을 수행하고 큐를 모두 소진하거나 콜백의 최대 개수를 실행할 때까지
해당 단계의 큐에서 콜백을 실행합니다.
- 큐를 모두 소진하거나 콜백 제한에 이르면 이벤트 루프는 다음 단계로 이동 !
● Node.JS가 실행되면 스레드가 생성되고 이벤트 루프가 생성됩니다.
- 이벤트 루프는 6개의 페이즈를 라운드 로빈 방식으로 순회하며 동작합니다.
① Timers
- Timer 단계는 Event Loop의 시작 단계입니다.
- 'setTimeout()' , 'setInterval()' 과 같은 '타이머에 관련된' callback을 처리합니다.
- 타이머들이 호출 되자마자 Event Queue에 들어가는 것이 아니고
내부적으로 'min-heap' 형태로 타이머를 구성하고 있다가 "발동 단계" 가 되면 그때
'Event Queue' 로 'callback'을 이동시킵니다.
- 'min-heap' 는 이진트리 형태인 최소 힙구조 라서 가장 빠른 타이머를 체크할 수 있게 세팅된 자료구조.
- 타이머 관련된 로직은 해당 페이즈에 진입해야만 "실행될 기회를 얻을 수 있습니다."
② pending i/o callbacks
- 이 단계에서는 'pending_queue'에 들어있는 'callback' 들을 실행합니다.
- 'pending_queue' 에는 이전 이벤트 루프 반복에서 수행되지 못했던 I/O 콜백
이전 루프에서 완료된 'callback ( ex - Network I/O가 끝나고 "응답" 받은 경우 등 )
또는 'Error callback' 등이 쌓이게 됩니다.
- 또한, TCP 오류와 같은 시스템 작업의 콜백 함수도 실행됩니다.
③ idle, prepare
- Node.js 내부용으로만 사용됩니다.
- "Event Loop"가 매번 순회할 때마다" 실행되며, 'poll' 을 위한 준비작업을 하는 단계.
④ Poll
- 이 페이즈는 새로운 I/O 이벤트를 다루며 'watcher_queue' 의 콜백들을 실행합니다.
여기서 "Watcher"는 구조체.
- 'watcher_queue' 에는 I/O에 대한 거의 모든 콜백들이 담깁니다.
- 'setTimeout' , 'setImmediate', 'close' 콜백 등을 제외한 모든 콜백이 여기서 실행.
- 만약, Queue 가 비어있지 않다면 배정받은 시간동안 'Queue' 가 모두 소진될 때까지
모든 'callback'을 'call stack'으로 올려 실행시킵니다.
∇별도의 규칙
- check 단계를 탐색하여, setImmediate() 가 있는지 확인.
- 있는 경우, check 단계로 넘어갑니다.
- 없는 경우, timer 단계에서 실행할 timer 함수가 있는지 확인합니다.
- 있는 경우, timer 함수를 실행할 수 있는 시간까지 대기한 후, timer 단계로 넘어갑니다.
- 대기하는 동안 poll 큐에 콜백 함수가 쌓이면 즉시 실행합니다.
⑤ Check
- setImmediate() 의 콜백 함수가 실행됩니다.
- setImmediate() 를 사용하여 수행한 callback 만 Event Queue에 쌓이고 call stack으로 올라갑니다.
⑥ Close callbacks
- close 이벤트에 따른 콜백 함수를 실행합니다.
- Close 단계는 'socket.on('close', () => {} ) 같은 clost type의 callback을 관리하는 단계.
- uv_close() 를 부르면서 종료된 핸들러의 콜백들을 처리하는 페이즈.
+ nextTickQueue
- 'libuv' 에 "포함되어 있지 않고" Node.js에 구현되어 있으며,
이벤트 루프의 페이즈와 상관없이 동작하는 queue입니다.
- nextTickQueue 는 process.nextTick 에 의해 생성된 콜백들을 저장하는 큐이며,
다른 I/O 작업이나 타이머 작업보다 먼저 처리됩니다.
== 즉, 현재 실행중인 스크립트가 완전히 끝나고 다른 이벤트 루프 단계로 넘어가기 전에 실행됩니다.
- 긴급하게 처리해야 할 작업을 등록할 때 사용되며,
이 큐의 작업이 너무 많으면 I/O 작업이 지연될 수 있습니다.
+ microTaskQueue
- 이 역시 'libuv' 에 "포함되어 있지 않고" Node.js에 구현되어 있으며,
이벤트 루프의 페이즈와 상관없이 동작하는 queue입니다.
- Promise의 Resolve 된 프라미스 콜백을 가지고 있는 큐입니다.
- 현재 실행 중인 스크립트가 완료된 후, 그리고 이벤트 루프의 다음 단계로 넘어가기 전에 실행되며
이 큐는 nextTickQueue 가 비워진 후에 처리됩니다.
● 'Timer Phase' -> ' Pending Callbacks Phase ' -> ' Idle, Prepare Phase ' -> ' Poll Phase '
-> ' Check Phase' -> ' Close Callbacks Phase ' -> 'Timer Phase ' 의 기본적인 순서를 따르며,
이 순회구조를 하나의 'tick'이라고 부릅니다.
Ⅱ. 결론 정리.
1. nodejs 는 JS 코드를 c/c++로 바인딩하고, V8 api / libuv api를 이용하여
core( node > src) 를 구현합니다.
2. V8과 libuv는 각기 별개로 움직이지 않습니다.
- nodejs는 하나(싱글스레드) 의 이벤트루프로만 동작합니다.
- JS의 실행은 'Main Thread' 에 의해서만 수행되고 1개의 call stack을 가집니다.
3. call stack 실행은 동기적 blocking 이기 때문에 이를 극복하기 위해 Single Thread 와 궁합이 좋은
" 비동기 callback" 프로그래밍 방식인 Event Loop를 추상화 한 libuv library를 사용합니다.
4. node 인스턴스가 생성될 때 start 함수에서 do-while 문으로 uv_run() 이 호출되고 있습니다
libuv는 js엔진이 아니며, libuv 내부에 있는 event loop가 파라미터로 넘겨받은
V8::Isolate, V8::Context 를 이용해 JS 로직을 처리합니다.
5. libuv 내의 'Event Loop' 는 "Main Thread에 상주" 하여 JS 비동기 실행을 합니다.
요청의 특징에 따라서 '커널 비동기 함수' 또는 'libuv' 내의 'Thread Pool' 에 작업을 "위임" 하며
'callback' 을 실행하기 위해 'Event Queue'에 적재된 'callback' 을 empty 상태의 'call stack' 으로 이동시킵니다.
6. 'Event Loop' 는 앞서 살펴본 각 페이스 단계의 특성을 지키며 다음 큐로 넘기고 실행합니다.
☆ Node는 왜 싱글 스레드를 고집하는가???
1.비동기 I/O 처리: Node.js는 비동기 I/O 모델을 사용하여, 요청을 처리하는 동안 다른 작업을 수행할 수 있습니다.
이는 서버의 성능을 높이고, 많은 클라이언트 요청을 동시에 처리할 수 있게 합니다.
2.단순성: 싱글 스레드 모델은 코드의 복잡성을 줄여줍니다.
멀티 스레드 환경에서는 스레드 간의 동기화 문제나 경쟁 조건이 발생할 수 있지만,
싱글 스레드에서는 이러한 문제를 피할 수 있습니다.
3.자원 효율성: 스레드를 생성하고 관리하는 데 드는 오버헤드가 줄어들어, 메모리 사용량이 적고 성능이 향상됩니다.
4.이벤트 기반 아키텍처: Node.js는 이벤트 루프를 기반으로 하여, 이벤트가 발생할 때만 작업을 수행합니다.
이는 CPU 자원을 효율적으로 사용할 수 있게 해줍니다.
'Back_End > Node.js' 카테고리의 다른 글
[ 백엔드 공부하기 : Node.js ] NodeJS의 스레드 방식은 싱글? 멀티? (0) | 2024.12.08 |
---|---|
[ 백엔드 공부하기 : Node.js ] NodeJS의 NPM이란? (0) | 2024.12.07 |
[ 백엔드 공부하기 : Node.js ] NodeJS에 활용되는 JavaScript의 기본 동작 원리와 V8엔진. (2) | 2024.12.05 |
[ 백엔드 공부하기 : Node.js ] Node.js는 서버단에서 어떻게 JS를 실행할까. (0) | 2024.11.24 |
[ 백엔드 공부하기 : Node.js ] Node의 역할. (0) | 2024.11.17 |