Node.js 인터뷰 질문 76
질문: Node.js가 단일 스레드 언어임에도 불구하고 어떻게 높은 동시성을 처리할 수 있는지 설명해주세요.
답변:
Node.js는 단일 스레드 이벤트 루프 모델을 기반으로 하지만, 비동기 I/O와 다양한 메커니즘을 통해 높은 동시성을 달성할 수 있습니다. 이제 Node.js가 단일 스레드 모델에서 어떻게 효율적으로 동시성을 처리하는지 살펴보겠습니다.
1. 이벤트 루프와 비동기 I/O
Node.js의 핵심은 이벤트 루프와 비동기 I/O 모델입니다. 이 모델은 다음과 같이 작동합니다:
// 비동기 파일 읽기 예시
const fs = require("fs");
console.log("시작");
// 비동기 작업 예약
fs.readFile("example.txt", "utf8", (err, data) => {
if (err) {
console.error("오류:", err);
return;
}
console.log("파일 내용:", data);
});
console.log("종료");
// 출력 순서:
// 시작
// 종료
// 파일 내용: ...
비동기 작업이 발생하면 Node.js는 작업을 libuv 라이브러리를 통해 운영 체제의 커널이나 스레드 풀에 위임합니다. 이후 작업이 완료되면 콜백 함수가 이벤트 루프의 태스크 큐에 추가되어 실행됩니다.
2. libuv와 스레드 풀
Node.js는 내부적으로 libuv 라이브러리를 사용하여 비동기 I/O 작업을 처리합니다. libuv는 운영 체제의 비동기 메커니즘을 활용하며, 필요한 경우 스레드 풀을 사용합니다.
// 파일 시스템 작업은 내부적으로 스레드 풀을 사용
const fs = require("fs");
const crypto = require("crypto");
// 스레드 풀 크기 설정 (기본값은 4)
process.env.UV_THREADPOOL_SIZE = 8;
// CPU 집약적인 작업 (스레드 풀 활용)
function calculateHash() {
const start = Date.now();
// 암호화 연산은 CPU 집약적이라 스레드 풀에서 실행됨
crypto.pbkdf2("비밀번호", "salt", 100000, 512, "sha512", () => {
console.log(`해시 계산 완료: ${Date.now() - start}ms`);
});
}
// 여러 해시 계산 병렬 실행
for (let i = 0; i < 8; i++) {
calculateHash();
}
3. 클러스터 모듈
Node.js는 단일 스레드지만, 클러스터 모듈을 사용하여 여러 프로세스를 생성하고 로드 밸런싱할 수 있습니다:
const cluster = require("cluster");
const http = require("http");
const numCPUs = require("os").cpus().length;
if (cluster.isMaster) {
console.log(`마스터 프로세스 ${process.pid} 실행 중`);
// CPU 코어 수만큼 워커 생성
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.log(`워커 ${worker.process.pid} 종료됨`);
// 워커가 종료되면 새 워커 시작
cluster.fork();
});
} else {
// 워커 프로세스는 HTTP 서버 생성 및 요청 처리
http
.createServer((req, res) => {
res.writeHead(200);
res.end(`워커 ${process.pid}가 응답함\n`);
})
.listen(8000);
console.log(`워커 ${process.pid} 시작됨`);
}
4. Worker Threads 모듈
Node.js 10부터 도입된 Worker Threads 모듈을 사용하면 동일한 프로세스 내에서 병렬 JavaScript 실행이 가능합니다:
const { Worker, isMainThread, parentPort } = require("worker_threads");
if (isMainThread) {
// 메인 스레드 코드
console.log("메인 스레드 실행");
const worker = new Worker(__filename);
worker.on("message", (msg) => {
console.log(`워커로부터 메시지: ${msg}`);
});
worker.postMessage("메인에서 워커로 메시지 전송");
} else {
// 워커 스레드 코드
console.log("워커 스레드 실행");
// 메인 스레드로부터 메시지 수신
parentPort.on("message", (msg) => {
console.log(`메인으로부터 메시지: ${msg}`);
// CPU 집약적인 작업 수행
const result = fibonacci(42);
// 결과를 메인 스레드로 반환
parentPort.postMessage(`Fibonacci(42) = ${result}`);
});
}
// CPU 집약적인 작업 예시
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
5. 비차단 코드 작성의 중요성
Node.js에서 동시성을 최대화하려면 차단(blocking) 코드를 피하는 것이 중요합니다:
// 나쁜 예: 차단 코드
function blockingOperation() {
const start = Date.now();
// CPU를 점유하는 무거운 연산
while (Date.now() - start < 5000) {
// 5초 동안 CPU를 차단
}
return "complete";
}
// 좋은 예: 비차단 코드
function nonBlockingOperation() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("complete");
}, 5000);
});
}
// 비차단 코드 사용 예시
async function handleRequest() {
console.log("요청 시작");
const result = await nonBlockingOperation();
console.log("결과:", result);
console.log("요청 종료");
}
6. Node.js 이벤트 루프 페이즈
이벤트 루프는 다음 페이즈로 구성됩니다:
- timers:
setTimeout()
,setInterval()
콜백 실행 - pending callbacks: 일부 시스템 작업 콜백(TCP 오류 등) 실행
- idle, prepare: 내부용
- poll: I/O 콜백 실행 및 새 I/O 이벤트 감시
- check:
setImmediate()
콜백 실행 - close callbacks:
socket.on('close', ...)
등 close 이벤트 콜백 실행
// 이벤트 루프 페이즈 실행 순서 예시
console.log("스크립트 시작");
// timer 페이즈에서 실행
setTimeout(() => {
console.log("setTimeout 실행");
}, 0);
// check 페이즈에서 실행
setImmediate(() => {
console.log("setImmediate 실행");
});
// poll 페이즈 이후 실행될 I/O 작업
fs.readFile(__filename, () => {
// I/O 완료 후 타이머 등록
setTimeout(() => {
console.log("I/O 내부 setTimeout");
}, 0);
// I/O 완료 후 immediate 등록
setImmediate(() => {
console.log("I/O 내부 setImmediate");
});
// 현재 이벤트 루프 반복 마지막에 실행
process.nextTick(() => {
console.log("I/O 내부 nextTick");
});
});
// 현재 페이즈 완료 직후 실행
process.nextTick(() => {
console.log("nextTick 실행");
});
console.log("스크립트 종료");
요약
Node.js가 단일 스레드임에도 높은 동시성을 달성하는 방법:
- 이벤트 루프와 비동기 I/O: 비차단 작업 처리를 위한 핵심 아키텍처
- libuv와 스레드 풀: 운영 체제의 비동기 메커니즘 및 내부 스레드 풀 활용
- 클러스터 모듈: 여러 Node.js 프로세스를 사용한 멀티코어 활용
- Worker Threads: CPU 집약적 작업을 위한 멀티스레딩 지원
- 비차단 코드 작성: 이벤트 루프 차단을 방지하는 코딩 패턴
- 이벤트 루프 이해: 효율적인 작업 스케줄링을 위한 이벤트 루프 메커니즘
이러한 메커니즘을 통해 Node.js는 단일 스레드 모델을 유지하면서도 효율적인 리소스 사용과 높은 동시성을 제공합니다. 대부분의 웹 애플리케이션은 I/O 바운드 작업이 많기 때문에 이 모델은 특히 웹 서버와 API 서버에 효과적입니다.