event loop란 무엇인가?

이벤트 루프는 Node.js에서 여러 비동기 작업들을 모아서 관리하고 순서대로 실행할 수 있게 하는 도구이다.

ex) file.readFile('file.txt', callback) 

 

event loop의 내부 동작 과정

이벤트 루프는 위와 같은 과정을 거쳐 동작한다. 각 박스들은 특정 작업을 수행하기 위한 페이즈(phase)를 의미하고 다음 페이즈로 넘어가는 것을 틱(Tick)이라고 한다. 각 페이즈마다 FIFO 큐가 있는데 함수가 호출되면 큐에 쌓이게 된다. 이벤트 루프는 싱글 스레드이기 때문에 이전 페이즈가 끝나야 혹은 페이즈의 최대 콜백 수에 도달하면 다음 페이즈가 실행되게 된다.  

 

각 phase들의 역할

timer: setTimeout()이나 setInterval()에 의해 예약된 콜백을 실행한다. 이때 타이머는 콜백이 실행될 정확한 시간을 지정하는 것이 아니라 해당 콜백이 실행될 수 있는 최소 시간을 지정한다. 콜백은 지정된 시간이 지난 후 최대한 빨리 실행되지만 다른 콜백들의 실행에 의해 지연될 수 있다. 

pending callbacks: TCP 오류 유형과 같은 일부 시스템 작업에 의한 콜백을 실행

idle, prepare: 내부적으로만 사용되고 이 단계에서 이벤트 루프는 아무 작업도 수행하지 않음. 

poll: close 콜백, 타이머에 의해 예약된 콜백, setImmediate()을 제외한 거의 모든 I/O관련 콜백들이 실행(파일 읽기/쓰기, 네트워크 요청 등 다양한 비동기 작업이 수행되는 곳)

check: setImmediate() 콜백을 실행

close callbacks: socket.on('close', fn) 또는 process.exit()과 같은 종료 이벤트와 관련된 콜백 실행. 각 pahse 사이 모든 작업들이 수행됐는지 확인하고 완전히 종료.

 

이전에 이벤트 루프에서는 비동기 작업을 OS 커널에 맡기거나 워커스레드에거 넘겨 처리한다고 하였다. 특정 함수(작업)이 들어오면 이벤트 루프는 각 phase를 훑어가며 들어온 작업을 할당하고(큐에 콜백 할당) 해당 phase에서 OS 커널이나 워커스레드에 넘겨 비동기 작업을 처리하는(콜백실행) 방식으로 동작한다. 아래의 예시와 함께 보면 더 이해가 수월할 것이다. 

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

 

이 코드를 실행하면 이벤트루프는 어떻게 동작할까?

1. 이벤트 루프는 fs.readFile이라는 작업은 poll phase에 추가하고 poll phase에서는 OS 커널에 작업을 넘겨 처리한다. 이때 OS 커널에서 작업을 처리할 때는 논블로킹 방식으로 하기 때문에 fs.readFile() 뒤에 코드가 있다면 readFile 작업이 수행될 때까지 처리될 수 있다.

2. poll phase에서 fs.readFile()를 읽어들인 후에 콜백을 실행하는데 이때 순차적으로 setTimeout() 콜백함수가 timer에 추가되고 이후 setImmediate() 콜백함수가 check에 추가된다. 

3. check phase로 넘어가 check 큐에 대기중이던 setImmediate()의 콜백이 실행된다.

4. close callbacks phase에서 timer에 작업에 남아있는 것을 확인하고 timer로 다시 이동한다.

5. timer 큐에 남아있던 setTimeout()의 콜백함수를 실행한다.

6. 그 뒤의 비어있는 phase들을 거쳐 close callbacks phase에 오면 모든 작업들이 수행됨을 확인하고 작업을 종료한다.

 

이렇게 작업의 수행과정을 살펴보면 위 코드에서 setImmediate()가 setTimeout()보다 먼저 실행됨을 확인할 수 있다.

'NodeJS' 카테고리의 다른 글

Node.js는 무엇인가?  (15) 2024.09.19

브라우저 환경

JavaScript를 실행하려면 JS 엔진이 필요한데 브라우저에는 기본적으로 JS 엔진이 탑재되어 있다.

 

Chrome - V8

Firefox - SpiderMonkey

Safari - JavascriptCore

Internet Explorer - Chakra

 

최초의 JS 엔진은 단순한 interpreter이었지만 위와 같은 최신 JS 엔진들은 성능 문제로 JIT Compiliation(Just-In-Time)을 사용한다.

 

JIT Compilitation이란?

Interpreter : 코드를 한줄씩 번역 후 실행 => interpreter는 코드를 한줄씩 해석하고 실행하기 때문에 heavy한 기능을 수행하기에는 너무 느림

Compiler : 코드를 한번에 기계어로 변환한 뒤 실행

JIT Compiliation : 코드가 실행되는 시점에 실시간으로 interpreter 방식으로 기계어 코드 생성, 해당 코드가 컴파일 대상이 되면 코드를 컴파일하고 캐싱함. 동일 코드 다시 나오면 컴파일하지 않고 캐싱한 부분 재사용

interpreter의 장점(한줄씩) + compiler의 장점(캐싱-컴파일한 부분 저장해두는 것)을 합친 방식이라 보면 됨

 

브라우저 밖의 외부 환경

runtime(환경)이란?

런타임이란 프로그래밍 언어가 구동되는 환경을 말한다. 여러 웹 브라우저들도 JS의 런타임 환경이 되고 웹 브라우저 외부에서는 Node.js 와 같은 런타임을 사용한다. Node.js는 프로그래밍 언어도, 프레임워크도 아닌 자바스크립트 런타임이다. 

출처: https://goldenrabbit.co.kr/2024/03/19/node-js-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%EB%B0%B1%EC%97%94%EB%93%9C-%EC%9E%85%EB%AC%B8%ED%95%98%EA%B8%B0-%E2%9D%B6/

 

Node.js 시스템은 위와같이 V8, Node.js API, Node.js 바인딩, Libuv로 구성되어 있다.

V8: 자바스크립트 코드를 실행하는 엔진으로 1+1과 같은 간단한 작업을 실행한다.

Node.js API: 파일 시스템. http, crypto 암호화 처리 등 복잡한 기능을 수행하는 여러 api, 이 api들은 자바스크립트로 작성된 것도 있지만 C++이나 C와 같은 low level 언어로 작성된 것도 있다.

Node.js 바인딩: V8에서 처리하지 못하는 C, C++, 파일시스템 등과 같은 코드들은 libuv에서 처리해야됨. Node.js 바인딩이 이 libuv에 작업을 넘겨주는 역할을 수행한다. 

Libuv: event loop를 기반로 비동기 I/O task를 처리하는 다중 플랫폼  C 라이브러리다. 플랫폼 별로 가장 빠른 비동기 IO 인터페이스로 통일된 코드를 돌릴 수 있다는 장점이 있다. 이는 특정 함수를 실행할 때 플랫폼(widow, mac 등)별로 따로 코드를 짜야되지만 libuv는 그럴 필요 없이 libuv 하나만 있으면 알아서 플랫폼에 최적화된 코드를 실행해준다는 의미이다.

Node.js는 v8이 코드를 해석하고 Node.js API들 중 하나의 함수를 호출한 뒤 Node.js 바인딩이 libuv에서 원하는 작업을 처리하게 하는 방식으로 동작한다(API에서 바인딩을 호출하고 바인딩에서 libuv를 호출하는 구조).

 

libuv는 어떠한 방식으로 동작하는가?

node.js는 자바스크립트 언어를 사용하는데 자바스크립트 언어는 싱글스레드이기 때문에 한 번에 하나의 작업만 처리할 수 있다. 그렇다면 node.js는 비동기로 여러 작업을 한번에 수행할 수 없는 것일까?

그렇지 않다! node.js는 libuv에 있는 Event loop를 통해 비동기로 여러 작업을 처리할 수 있다. 작업 처리의 순서는 아래와 같다.

1, 우선 V8엔진에서 libuv로 전달된 작업은 libuv 내부 이벤트 큐에 추가된다.

2. 이벤트 큐에 쌓인 요청은 차례대로 이벤트 루프에 전달되고 운영체제 커널에 넘겨져 비동기 처리 작업이 수행된다(논블로킹 처리).

3. 운영체제 내부적으로 비동기 처리가 힘든경우(DB, DNS lookup, Crypto, 파일처리 등) 스레드 풀에 넘겨져 작업이 수행된다(블로킹 처리).

4. 완료된 작업은 이벤트 루프로 전달되고 이벤트 루프에서는 콜백으로 전달된 요청에 대한 완료 처리를 한 뒤 호출된 부분으로 다시 넘긴다.

이러한 과정을 통해 node.js는 비동기 작업을 수행한다.

근데 위 그림을 보면 블로킹 처리, 논블로킹 처리가 모두 존재하는데 이 부분이 살짝 헷갈린다. 

출처: 패스트캠퍼스 10개 프로젝트로 끝내는 Node.js의 모든 것(Express & Nest.js) 초격차 패키지 Online 강의

 위 그림에서 main thread는 event loop를 담당하는 일꾼이다. 이는 운영체제 커널에서 지원하는 비동기 작업들을 받으면 커널의 비동기 함수들을 호출한다. 하지만 커널에서 지원하지 않는 작업이나 이벤트 루프를 블로킹할 수 있는 시간이 오래 걸리는 몇몇 작업들이 들어오면 스레드 풀의 워커 스레드에 넘겨준다. 위 그림에서 main thred를 제외한 나머지 부분이라고 보면 되겠다. 워커스레드는 기본적으로 4개로 구성되어 있고 128개까지 늘어날 수 있다. 만약 워커 스레드가 4개일 때 5개 이상의 작업이 들어오면 스레드가 작업이 끝날 때까지 대기해야 한다. 이 때문에 위위 그림에서 블로킹 처리라고 적어놓은게 아닐까 싶다. 

 

요약

- 코드가 호출 스택에 쌓인 후 실행되는데 그 작업이 비동기 작업이면 이벤트 루프는 OS커널 or 스레드풀에 작업 위임

- 비동기 작업 수행 뒤 콜백함수 호출

 

다음 글에서는 event loop가 무엇인지 자세히 알아봐야겠다.

 

 

 

https://ko.wikipedia.org/wiki/JIT_%EC%BB%B4%ED%8C%8C%EC%9D%BC

 

JIT 컴파일 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. JIT 컴파일(just-in-time compilation) 또는 동적 번역(dynamic translation)은 프로그램을 실제 실행하는 시점에 기계어로 번역하는 컴파일 기법이다. 전통적인 입장에서 컴

ko.wikipedia.org

https://hyeinisfree.tistory.com/26

 

[Java] JIT 컴파일러란?

JIT 컴파일(just-in-time compilation) 또는 동적 번역(dynamic translation)은 프로그램을 실제 실행하는 시점에 기계어로 번역하는 컴파일 기법이다. 컴파일러 vs 인터프리터 컴파일러와 인터프리터 모두 high

hyeinisfree.tistory.com

https://joe-cho.tistory.com/16

 

Libuv 라이브러리(feat. 이벤트 루프) - 3

✏️ Node.js ➡️ 네트워크 서버 구축에 특화되어 있고, V8엔진은 기존의 인터프리터 언어인 자바스크립트를 컴파일 방식으로 처리해 빠른 속도로 작업을 수행할 수 있도록 합니다. 또한, Livub 라

joe-cho.tistory.com

 

'NodeJS' 카테고리의 다른 글

[Node.js] Event Loop 이해하기  (2) 2024.09.20

+ Recent posts