Node.js 백엔드(Web)의 기본 동작 원리
1. Node.js의 Program Lifecycle (프로그램 생명주기)
Node.js 애플리케이션은 다음과 같은 단계로 동작한다.
- 초기화 단계:
- 필요한 모듈을 로드 (require 또는 import 사용).
- 서버, 데이터베이스 연결을 설정한다.
- 이벤트 루프 실행 단계:
- 비동기 I/O 작업, 네트워크 요청, 타이머 등이 실행된다.
- 작업이 완료되면 콜백 함수가 이벤트 루프에서 처리된다.
- 종료 단계:
- 특정 종료 조건(예: process.exit())이 발생하면 종료된다.
- SIGINT(Ctrl + C) 같은 시그널이 감지되면 종료 이벤트가 발생한다.
2. 이벤트 루프(Event Loop) 원리
Node.js는 단일 스레드(single-threaded) 기반의 비동기 이벤트 루프 모델을 사용하여 많은 동시 요청을 효율적으로 처리할 수 있다.
이벤트 루프의 단계
이벤트 루프는 다음과 같은 6단계로 구성된다.
- Timers (타이머 처리): setTimeout, setInterval의 콜백이 실행되는 단계.
- I/O Callbacks: 완료된 I/O 이벤트의 콜백이 실행된다 (예: 파일 읽기, 네트워크 요청 등).
- Idle, Prepare: 내부적으로 처리할 작업이 있는 경우 실행된다 (주로 V8 엔진 관련 내부 작업).
- Poll (폴링 단계):
- 새로운 I/O 이벤트를 확인하고, 대기 중인 I/O 요청을 처리한다.
- 타이머가 만료되지 않은 경우 이벤트 루프가 대기 상태로 들어간다.
- Check (즉시 실행): setImmediate()로 예약된 콜백이 실행된다.
- Close Callbacks: 소켓 연결 종료 등의 콜백이 실행된다.
이벤트 루프 흐름 예제
console.log('Start');
setTimeout(() => {
console.log('setTimeout callback');
}, 0);
setImmediate(() => {
console.log('setImmediate callback');
});
console.log('End');
출력 결과 (실행 환경에 따라 다를 수 있음):
Start
End
setImmediate callback
setTimeout callback
- setTimeout과 setImmediate는 거의 동시에 실행되지만, setImmediate가 먼저 실행될 가능성이 높다.
- 이는 이벤트 루프의 Check 단계가 Poll 단계보다 후순위로 실행되기 때문이다.
3. Thread (스레드)와 Node.js
Node.js는 기본적으로 **싱글 스레드(single-threaded)**로 동작하지만, 내부적으로는 멀티 스레드 작업을 처리할 수 있는 구조를 가지고 있다.
싱글 스레드의 의미
- Node.js의 메인 이벤트 루프는 하나의 싱글 스레드에서 실행된다.
- 하지만 내부적으로 Worker Threads(멀티 스레드 지원) 또는 libuv를 활용하여 비동기 작업을 처리할 수 있다.
Worker Threads 사용 예제
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', message => console.log(`Received: ${message}`));
worker.postMessage('Hello from Main Thread');
} else {
parentPort.on('message', message => {
console.log(`Worker received: ${message}`);
parentPort.postMessage('Hello from Worker');
});
}
- Worker Threads를 사용하면 Node.js에서도 멀티 스레드 작업을 실행할 수 있다.
- 단, 일반적인 서버 요청 처리에는 사용되지 않으며, CPU 집약적인 작업(예: 이미지 처리, 암호화 등)에 적합하다.
4. 비동기 처리 (Async)와 콜백, 프로미스, async/await
Node.js는 비동기 프로그래밍을 기본적으로 사용하여 I/O 작업을 효율적으로 처리한다.
1) 콜백(callback) 방식
const fs = require('fs');
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
단점: 콜백 지옥(Callback Hell)이 발생할 수 있다.
2) 프로미스(Promise) 방식
const fs = require('fs').promises;
fs.readFile('file.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));
장점: 콜백보다 가독성이 좋다.
3) async/await 방식 (가장 권장됨)
const fs = require('fs').promises;
async function readFileAsync() {
try {
const data = await fs.readFile('file.txt', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}
}
readFileAsync();
장점: 비동기 코드를 동기 코드처럼 작성할 수 있어 가독성이 높다.
5. Node.js 백엔드 아키텍처에서 이벤트 루프가 중요한 이유
Node.js의 이벤트 루프와 비동기 처리 방식은 백엔드에서 매우 중요한 역할을 한다.
Node.js가 적합한 경우
✅ I/O 중심의 애플리케이션 (예: API 서버, 채팅 서버, 파일 업로드 서비스).
✅ 실시간 데이터 처리 (예: WebSocket, 게임 서버).
✅ 대량의 동시 요청을 처리해야 하는 경우.
Node.js가 적합하지 않은 경우
❌ CPU 집약적인 작업 (예: 이미지/영상 처리, 머신러닝 연산).
❌ 블로킹 연산이 많은 경우 (예: 복잡한 수학 계산).
이런 경우 Worker Threads 또는 Python, Rust 등의 언어와 결합하여 사용하는 것이 좋다.
결론
Node.js는 이벤트 루프, 싱글 스레드, 비동기 처리 등의 개념을 활용하여 높은 성능을 유지하는 백엔드 환경을 제공한다.
이러한 개념을 잘 활용하면 확장성 높은 백엔드 서버를 구축할 수 있다! 🚀