학습하게 된 계기
Node.js는 서버 사이드 JavaScript 환경으로, 비동기적이고 이벤트 기반의 아키텍처를 가지고 있습니다. 이러한 특성 덕분에 Node.js는 높은 동시성을 처리할 수 있으며, I/O 작업이 많은 애플리케이션에서 뛰어난 성능을 발휘합니다. 그러나 이 모든 것의 중심에는 '이벤트 루프'라는 핵심 메커니즘이 있습니다. 이 글에서는 Node.js의 이벤트 루프에 대해 자세히 알아보고, 이를 통해 Node.js가 어떻게 비동기 처리를 수행하는지 이해해 보도록 하겠습니다.
이벤트 루프란?
이벤트 루프는 Node.js가 non-blocking I/O 작업을 수행하면서도 단일 스레드 기반의 자바스크립트가 동시성을 가질 수 있게 해주는 핵심 메커니즘입니다. 이는 마치 효율적인 웨이터가 여러 테이블의 주문을 동시에 처리하는 것과 유사합니다. 웨이터는 한 번에 한 가지 일만 할 수 있지만, 여러 테이블을 돌아다니면서 주문을 받고, 주방에 전달하고, 음식을 서빙하는 작업을 번갈아가며 수행함으로써 효율적으로 일을 처리합니다.
이벤트 루프는 시스템 커널에서 가능한 작업이 있다면 그 작업을 커널에 이관합니다. 이를 통해 자바스크립트가 단일 스레드 기반임에도 불구하고 Node.js가 non-blocking I/O 작업을 수행할 수 있게 됩니다.
이벤트 루프의 단계
이벤트 루프는 6개의 주요 단계(Phase)로 구성되어 있습니다. 각 단계는 특정 작업을 처리하기 위한 콜백 함수들을 담고 있는 큐를 가지고 있습니다. 이벤트 루프는 이 단계들을 순차적으로 반복하면서 작업을 처리합니다.
Timer 단계
이벤트 루프는 Timer 단계에서 시작합니다. 이 단계에서는 `setTimeout`이나 `setInterval`과 같은 함수를 통해 등록된 타이머들을 관리합니다.
타이머는 최소 힙(Min Heap) 자료구조로 관리됩니다. 이는 마치 빵집에서 주문 번호표를 관리하는 것과 유사합니다. 가장 빨리 나와야 할 빵(실행되어야 할 타이머)이 항상 맨 앞에 오도록 정렬되어 있습니다.
예를 들어, 100ms, 200ms, 300ms, 400ms 후에 실행되어야 할 네 개의 타이머 A, B, C, D가 있다고 가정해 봅시다. 이들은 `A → B → C → D` 순서로 힙에 저장됩니다. 만약 250ms가 지난 후 Timer 단계에 진입했다면, A와 B는 실행되지만 C와 D는 아직 실행 시간이 되지 않았으므로 다음 루프를 기다리게 됩니다.
Pending (I/O) 콜백 단계
이 단계에서는 이전 루프 사이클에서 지연된 I/O 콜백들을 처리합니다. 예를 들어, TCP 연결 오류와 같은 시스템 작업의 콜백이 여기서 처리됩니다.
이는 마치 우체부가 전날 배달하지 못한 우편물을 다음 날 가장 먼저 배달하는 것과 비슷합니다.
Idle, Prepare 단계
이 두 단계는 Node.js의 내부 동작을 위한 것으로, 개발자가 직접 다루는 일은 거의 없습니다. Idle 단계는 매 틱(Tick)마다 실행되고, Prepare 단계는 매 폴링 전에 실행됩니다.
이는 레스토랑에서 주방장이 조리를 시작하기 전에 재료를 준비하고 주방을 정리하는 과정과 유사합니다.
Poll 단계
Poll 단계는 이벤트 루프에서 가장 중요한 단계입니다. 이 단계에서는 새로운 I/O 이벤트를 가져와 관련 콜백을 실행합니다. 예를 들어, 파일 읽기 작업이 완료되었다면 그 결과를 처리하는 콜백이 여기서 실행됩니다.
Poll 단계는 두 가지 주요 기능을 수행합니다:
- I/O 작업이 얼마나 오래 블록되고 폴링해야 하는지 계산합니다.
- Poll 큐에 있는 이벤트를 처리합니다.
이는 마치 레스토랑 주방에서 주문이 들어오기를 기다리다가, 주문이 들어오면 즉시 조리를 시작하는 것과 유사합니다. 주문이 없다면 다른 준비 작업을 하거나 잠시 쉬게 됩니다.
Check 단계
Check 단계는 `setImmediate()` 콜백을 위한 특별한 단계입니다. `setImmediate()`는 현재 Poll 단계가 완료된 후 실행될 콜백을 스케줄링하는 데 사용됩니다.
이는 레스토랑에서 "급한 주문"을 처리하는 것과 비슷합니다. 일반적인 주문(Poll 단계)을 모두 처리한 후, 특별히 빨리 처리해야 하는 주문(setImmediate 콜백)을 확인하고 처리하는 것과 같습니다.
Close 콜백 단계
이 단계에서는 'close' 이벤트 타입의 콜백들이 처리됩니다. 예를 들어, socket.on('close', ...)
와 같은 콜백이 여기서 실행됩니다.
이는 레스토랑에서 영업을 마감하고 문을 닫기 전에 마지막으로 정리하는 과정과 유사합니다. 모든 손님이 나가고, 주방을 정리하고, 마지막으로 문을 잠그는 것과 같은 작업들이 이 단계에서 이루어집니다.
nextTickQueue와 microTaskQueue
이 두 큐는 기술적으로 이벤트 루프의 일부는 아니지만, 이벤트 루프의 작동에 중요한 역할을 합니다.
- nextTickQueue:
process.nextTick()
API를 통해 등록된 콜백들을 가지고 있습니다. - microTaskQueue: Promise가 resolve되었을 때 실행될 콜백들을 가지고 있습니다.
이 두 큐의 콜백들은 이벤트 루프의 어느 단계에서든 다음 단계로 넘어가기 전에 모두 처리됩니다. nextTickQueue가 microTaskQueue보다 우선순위가 높습니다.
이는 마치 레스토랑에서 정규 메뉴(이벤트 루프의 각 단계)를 준비하는 중간중간에 특별 요청(nextTick, Promise)을 즉시 처리하는 것과 같습니다. 그리고 이 특별 요청 중에서도 더 급한 요청(nextTick)을 먼저 처리하는 것입니다.
이벤트 루프의 비유
이벤트 루프의 전체적인 동작을 이해하기 위해, 놀이공원의 회전목마를 상상해 보겠습니다.
- 회전목마 자체: 이벤트 루프
- 말들: 각 단계 (Timer, Pending I/O, Poll 등)
- 탑승객: 콜백 함수들
- 탑승과 하차: 콜백의 등록과 실행
- 운영자: Node.js 런타임
회전목마는 계속 돌아갑니다(이벤트 루프의 반복). 각 말(단계)마다 탑승객(콜백)이 타고 내립니다. 운영자(Node.js 런타임)는 전체 과정을 관리하며, 필요에 따라 회전 속도를 조절하거나 새로운 탑승객을 태웁니다.
특별한 VIP 탑승객(nextTick, Promise 콜백)은 회전과 관계없이 즉시 탈 수 있는 특권이 있습니다. 이들은 일반 탑승객보다 우선적으로 처리됩니다.
회전목마에 더 이상 탑승객이 없으면(모든 콜백이 처리되면), 운영자는 회전을 멈추고 놀이공원을 닫습니다(프로그램 종료).
실제 동작 예시
이벤트 루프의 동작을 더 잘 이해하기 위해, 간단한 Node.js 코드 예시를 통해 살펴보겠습니다.
console.log('시작');
setTimeout(() => {
console.log('타이머 1 완료');
}, 0);
setImmediate(() => {
console.log('즉시 실행');
});
process.nextTick(() => {
console.log('다음 틱');
});
new Promise((resolve) => {
resolve('프로미스 완료');
}).then(console.log);
console.log('끝');
이 코드의 실행 결과는 다음과 같습니다:
시작
끝
다음 틱
프로미스 완료
타이머 1 완료
즉시 실행
이 결과를 통해 우리는 다음과 같은 사실을 알 수 있습니다:
- 동기 코드(
console.log('시작')
,console.log('끝')
)가 가장 먼저 실행됩니다. process.nextTick()
이 가장 높은 우선순위를 가집니다.- Promise의
then
핸들러는 nextTick 다음으로 실행됩니다. setTimeout(fn, 0)
은 실제로 0ms 후에 실행되는 것이 아니라, 다음 이벤트 루프 사이클의 Timer 단계에서 실행됩니다.setImmediate()
는 현재 Poll 단계가 완료된 후의 Check 단계에서 실행됩니다.
이벤트 루프의 인사이트를 얻음으로써 나는 개발자로서...
Node.js의 이벤트 루프는 단일 스레드 환경에서 비동기 작업을 효율적으로 처리할 수 있게 해주는 핵심 메커니즘입니다. 이를 통해 Node.js는 높은 처리량과 낮은 지연 시간을 가진 애플리케이션을 개발할 수 있게 해줍니다.
이벤트 루프의 각 단계를 이해하고, nextTick과 microtask의 특별한 위치를 알게 되면, Node.js 애플리케이션의 동작을 더 정확히 예측하고 최적화할 수 있습니다.
비동기 프로그래밍은 때때로 복잡하고 이해하기 어려울 수 있지만, 이벤트 루프의 원리를 잘 이해하면 더 효율적이고 성능이 좋은 Node.js 애플리케이션을 개발할 수 있을 것입니다.
'Dev Framework > Node.js' 카테고리의 다른 글
[NestJS] Node진영에서의 NestJS의 위상 (0) | 2024.09.21 |
---|---|
[NestJS] NestJS는 Spring에서 많은 영감을 받았다. (0) | 2024.09.21 |
[NestJS] NestJS는 처음이지? 어서와. (0) | 2024.09.21 |
[Node.js] ORM에 대해서 (8) | 2024.09.15 |