Search
Duplicate

Node.js 동작원리

카테고리
작성일
2021/10/06 01:21
수정일
2021/11/29 05:23
발간일
상태
작성 완료
(Single thread, Event-driven, Non-Blocking I/O, Event loop)

1. 싱글 스레드 (Single-Thread)

프로세스 (process) : 메모리에 올라와 실행되고 있는 프로그램의 인스턴스. 쓰레드(Thread) : 프로세스 내에서 할당받은 실행의 단위. 스레드는 프로세스 당 CPU의 코어 개수만큼 생성. * 스레드는 프로세스 내의 메모리 공간을 공유하지만, 각각의 프로세스는 별도의 메모리 공간을 갖습니다.
싱글 쓰레드는 프로세스 내에서 하나의 쓰레드가 하나의 요청만을 수행한다. 해당 요청이 수행될 때 다른 요청을 함께 수행할 수 없는데, 이를 싱글쓰레드 블로킹(SingleThread-Blocking) 모델이라고 한다. 진행되고 있는 작업이 끝날 때 까지 다음 작업이 대기하고 있기 때문(블로킹)이다.
반면에 멀티쓰레드는 쓰레드 풀에서 실행의 요청만큼 쓰레드를 매칭하여 작업을 수행한다. 그렇다면 싱글쓰레드보다 멀티쓰레드가 더 좋지 않을까?라는 생각이든다. 멀티쓰레드는 효율성 측면에서 큰 단점을 갖고 있다. 쓰레드 풀에 쓰레드가 늘어날수록 CPU 비용을 소모하고, 만약 요청이 적다면 놀고있는 쓰레드가 생기기도하며, 병렬로 처리하는 작업을 처리하기 위해 작업전환(Context Switching) 시간이 걸려 오히려 처리시간이 길어지는 경우도 있다.
Node.js는 싱글쓰레드 논블로킹(Single Thread Non-Blocking)모델로 구성되어 있다. 하나의 쓰레드로 동작하지만, 비동기 I/O 작업을 통해 요청작업들을 서로 블로킹하지 않습니다. 즉, 동시에 많은 작업들을 비동기로 수행함으로써 싱글쓰레드일지라도 작업이 처리될 때까지 대기하지 않고 처리가 가능하다.

Node.js 는 완전한 싱글쓰레드인가

Node.js에서 Javascript를 실행하는 쓰레드는 메인쓰레드 단 하나이기 때문이지만 Node.js가 완전하게 싱글쓰레드를 기반으로 동작하지않는다. 일부 Blocking 작업들은 libuv의 쓰레드 풀(Thread pool)에서 수행되기 때문입니다.
Node.js 내부 구조
Node.js의 특성인 이벤트 기반, 논블로킹 I/O 모델들은 모두 libuv 라이브러리에서 구현

libUV

비동기 라이브러리
네트워크, 파일 I/O등 비동기 처리지원
비동기는 시스템마다 제공하는 API 이용
(윈도우: IOCP, 리눅스: epoll, 맥: kqueue) 스프링의 경우 리눅스 환경에서 select 사용
시스템에서 제공하는 API가 없거나 특수한 경우 쓰레드풀 이용
네트워크, 소켓 작업은 시스템 API를 이용하며, 파일은 쓰레드풀 이용
(파일의 경우 시스템 API는 제공하나 추상화 문제로 시스템 API가 아니라 별도의 쓰레드를 이용)
쓰레드풀은 따로 설정하지 않는다면 4개를 기본값으로 생성. 이는 uv_threadpool로 설정할 수 있다.

2. 이벤트 기반 ( Event-Driven) - 이벤트 루프 동작원리

1.
이벤트가 발생하였을때, 저장해둔 작업을 수행하는 방식( ex>클릭 리스너, 네트워크 요청에 대한 처리 등등)
2.
이벤트 리스너에 미리 콜백함수를 저장해놓고, 입력된 이벤트에 따라 해당하는 작업을 수행한다.
3.
발생한 이벤트는 순차적으로 처리하며, 발생한 이벤트가 없다면, 대기한다.
4.
이벤트 루프가 이벤트 처리 순서를 관리해준다.

이벤트 루프(Event Loop)

javascript에서 기본적인 event loop의 과정은 아래와 같다.
function Four() { console.log('fourth') } function run() { console.log('first'); setTimeout(()=> { console.log('second'); },0) new Promise((resolve)=> { resolve('third'); }) .then(console.log); Four(); } setTimeout(run, 3000);
JavaScript
복사
위의 코드의 출력 결과를 순서로만 본다면
#순서 #결과 first first second fourth => third third fourth second
JavaScript
복사
위의 내용을 바탕으로 node.js의 상세적인 이벤트 루프 순서를 보자면 아래와 같다.
Node.js 공식 홈페이지에 나와있는 이벤트 루프의 다이어그램
각 단계는 실행할 콜백의 FIFO 큐를 가집니다. 이벤트 루프가 해당 단계에 진입하면 해당 단계에 한정된 작업을 수행하고 큐를 모두 소진 또는 콜백의 최대 개수를 실행할 때까지 해당 단계의 큐에서 콜백을 실행합니다.
큐를 모두 소진하거나 콜백 제한에 이르면 이벤트 루프는 다음 단계로 이동합니다.
이러한 작업이 또 다른 작업을 스케줄링하거나 poll 단계에서 처리된 새로운 이벤트가 커널에 의해 큐에 추가될 수 있으므로 폴링 이벤트를 처리하면서 poll 이벤트를 큐에 추가할 수 있습니다. 그 결과 오래 실행되는 콜백은 poll 단계가 타이머의 한계 시점보다 훨씬 더 오래 실행되도록 할 수 있습니다. 다음 페이즈로 넘어가는 것을 틱(tick)이라고 합니다. tick이 발생하면 다음 페이즈로 넘어갑니다.
Node.js 시작시 쓰레드 생성 후 이벤트 루프가 생성된다. 이벤트루프는 6개의 페이즈를 라운드 로빈 방식으로 순회하며 동작한다.
라운드 로빈(round robin) : 라운드 로빈은 그룹 내에 있는 모든 요소들을 합리적인 순서에 입각하여 뽑는 방법으로서, 리스트의 맨 위에서 아래로 가며 하나 씩 뽑고, 끝나면 다시 맨 위로 돌아가는 식으로 진행된다. 쉽게 말해 라운드 로빈은 "기회를 차례대로 받기"라고 이해해도 좋을 것이다.
1.
timers
루프의 시작을 알림
setTimeout()과 setInterval() 같은 timer 함수에 등록된 콜백 관리
timer phase는 타이머에 등록한 콜백이 언제 실행할지만 관리하고 실행은 poll phase에서 실행
타이머 콜백은 min-heap 자료구조 기반으로 구성
2. pending callbacks
다음 페이즈를 실행하기 위해 연기된 콜백실행(pending_queue)
각 페이즈는 모든 작업을 다 실행하지 않고 일정량만 실행하고 다음 페이즈로 넘어가기 때문에 이전에 처리하지 못한 작업을 실행하는 페이즈
또한 TCP 오류와 같은 작업들을 실행하기도 함 *nix는 ECONNREFUSED 이벤트를 받으면 해당 페이즈 큐에 등록
3. idle, prepare
idle, prepare 페이즈는 매 틱마다 실행
nodejs 내부 관리를 위해 사용
4. poll
setTimeout, setInterval, setimmediate로 등록한 콜백을 제외한 대부분이 여기서 처리
해당 큐가 비어있지 않다면, 작업을 순차적으로 처리.
비어있다면, 즉시 다음 페이즈로 넘어가지 않가 일정시간 대기. 이때 대기하는 시간은 타이머 페이즈에서 실행할 작업이 있는지 검사후 있다면 넘어간다.
해당 페이즈가 관리하는 큐 이름은 watcher_queue이며, 검사하는 큐는 check_queue, pending_queue, close_callbacks_queue
5. check
setImmediate()로 등록된 콜백 관리
setImmediate()에 의해서만 해당 페이즈에 콜백을 등록
setImmediate() 의 콜백함수 실행. 이벤트루프가 poll 단계에서 작업을 수행한 뒤, poll 단계가 유휴상태가 되었다면 poll 이벤트를 기다리지 않고 check 단계로 이동.
6. close
callbacksclose 이벤트에 따른 콜백함수를 실행합니다. ex) socket.on('close', ...) 
*nextTickQueue, microTaskQueue
해당 큐는 이벤트 루프는 아님
해당 큐는 매 틱마다 실행하며
다른 페이즈와 다르게 실행한도가 없음. 큐가 완전히 비워질때까지 실행하며 nextTickQueue가 microTaskQueue보다 우선순위가 높음
process_nextTick() 호출 시 nextTickQueue에 등록

Node.js의 동작 순서

1. js로 작성된 코드를 nodejs로 실행
2. 이벤트 루프에 진입하기 전에 코드를 실행
3. fs, socket 통신과 같이 libuv를 호출하는 함수가 있으면 libuv는 코드 제어권을 가지지 않고 다음 코드를 실행할 수 있도록 제어권을 넘김 (javascript로 랩핑한 cpp 코드를 호출)
3-1 libuv는 호출된 작업이 동기/비동기 검사후 시스템 API를 이용하거나 쓰레드풀에 생성된 쓰레드에게 작업 위임후 이때 작업이 완료되면 콜백을 큐에 등록
4. 코드들을 마저 실행
5. 코드들을 다 실행했으면 이벤트루프를 만들지, 종료할지 결정(libuv에 의해 콜백이 등록되었다면 이벤트루프 생성, 없으면 프로그램 종료)
6. 이벤트 루프는 timers -> pending callbacks -> idle, prepare -> poll -> check -> close callbacks와 같은 순으로 큐에 등록된 작업을 실행