Node.js 인터뷰 질문 36
질문: Node.js의 child_process
모듈에 대해 설명하고 언제 어떻게 사용하나요?
답변:
Node.js의 child_process
모듈은 새로운 프로세스를 생성하여 명령을 실행할 수 있게 해주는 강력한 기능입니다. 이 모듈을 사용하면 OS 명령어를 실행하거나, 외부 프로그램을 호출하거나, 여러 Node.js 프로세스를 병렬로 실행하여 CPU 집약적인 작업을 효율적으로 처리할 수 있습니다.
기본 개념
child_process
모듈은 부모 프로세스(Node.js 애플리케이션)에서 자식 프로세스를 생성하고 통신할 수 있는 여러 메소드를 제공합니다. 이 모듈은 시스템 명령어를 실행하거나 다른 언어로 작성된 스크립트(Python, C++ 등)를 호출하는 데 유용합니다.
주요 메소드
child_process
모듈은 자식 프로세스를 생성하는 네 가지 주요 메소드를 제공합니다:
1. spawn()
새로운 프로세스를 비동기적으로 생성하고, 명령의 출력을 스트림으로 처리합니다. 대용량 데이터를 반환하는 장기 실행 프로세스에 적합합니다.
const { spawn } = require("child_process");
// 'ls -la' 명령 실행
const ls = spawn("ls", ["-la"]);
// 출력 스트림 처리
ls.stdout.on("data", (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on("data", (data) => {
console.error(`stderr: ${data}`);
});
ls.on("close", (code) => {
console.log(`자식 프로세스 종료 코드: ${code}`);
});
2. exec()
셸을 통해 명령을 실행하고 결과를 버퍼에 저장합니다. 작은 양의 데이터를 반환하는 명령에 적합합니다.
const { exec } = require("child_process");
exec("ls -la", (error, stdout, stderr) => {
if (error) {
console.error(`실행 오류: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});
3. execFile()
셸 없이 직접 실행 파일을 실행합니다. exec()
보다 더 효율적이며 셸 주입 공격에 덜 취약합니다.
const { execFile } = require("child_process");
execFile("node", ["--version"], (error, stdout, stderr) => {
if (error) {
console.error(`실행 오류: ${error}`);
return;
}
console.log(`Node.js 버전: ${stdout}`);
});
4. fork()
새로운 Node.js 프로세스를 생성하고 부모와 자식 프로세스 간 통신 채널을 설정합니다. 이는 spawn()
의 특수한 형태로, Node.js 모듈을 실행하도록 설계되었습니다.
// 부모 프로세스 (main.js)
const { fork } = require("child_process");
const child = fork("child.js");
// 자식 프로세스에 메시지 전송
child.send({ hello: "world" });
// 자식 프로세스로부터 메시지 수신
child.on("message", (message) => {
console.log("부모가 메시지를 받음:", message);
});
// 자식 프로세스 (child.js)
process.on("message", (message) => {
console.log("자식이 메시지를 받음:", message);
// 부모 프로세스에 응답
process.send({ response: "안녕하세요!" });
});
동기식 메소드
child_process
모듈은 위의 각 메소드에 대해 동기식 버전도 제공합니다:
spawnSync()
execSync()
execFileSync()
이 메소드들은 자식 프로세스가 완료될 때까지 Node.js 이벤트 루프를 차단하므로, 성능에 민감한 코드에서는 주의해서 사용해야 합니다.
const { execSync } = require("child_process");
try {
const output = execSync("ls -la");
console.log(`출력: ${output}`);
} catch (error) {
console.error(`오류 발생: ${error}`);
}
사용 사례
child_process
모듈은 다음과 같은 상황에서 유용합니다:
1. 시스템 명령 실행
OS 수준의 명령을 실행할 때 사용합니다.
const { exec } = require("child_process");
// 디스크 사용량 확인
exec("df -h", (error, stdout, stderr) => {
if (error) {
console.error(`실행 오류: ${error}`);
return;
}
console.log(`디스크 사용량:\n${stdout}`);
});
2. CPU 집약적인 작업 처리
Node.js는 단일 스레드이므로, CPU 집약적인 작업은 이벤트 루프를 차단할 수 있습니다. 이러한 작업을 자식 프로세스로 오프로드하면 주 애플리케이션의 응답성을 유지할 수 있습니다.
// 부모 프로세스 (main.js)
const { fork } = require("child_process");
const http = require("http");
const server = http.createServer((req, res) => {
if (req.url === "/compute") {
// CPU 집약적인 작업을 자식 프로세스로 오프로드
const compute = fork("compute.js");
compute.send("start");
compute.on("message", (result) => {
res.end(`계산 결과: ${result}`);
});
} else {
res.end("안녕하세요!");
}
});
server.listen(3000);
// 자식 프로세스 (compute.js)
process.on("message", (msg) => {
if (msg === "start") {
// CPU 집약적인 계산 수행
const result = fibonacci(40); // 높은 숫자로 피보나치 계산
process.send(result);
}
});
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
3. 다른 언어로 작성된 스크립트 실행
Node.js는 JavaScript에 최적화되어 있지만, 때로는 특정 작업에 다른 언어가 더 적합할 수 있습니다. child_process
를 사용하면 Python, R, C++ 등으로 작성된 스크립트나 프로그램을 실행할 수 있습니다.
const { spawn } = require("child_process");
// Python 스크립트 실행
const pythonProcess = spawn("python", [
"data_analysis.py",
"--input",
"data.csv",
]);
pythonProcess.stdout.on("data", (data) => {
console.log(`Python 출력: ${data}`);
});
pythonProcess.on("close", (code) => {
console.log(`Python 프로세스 종료 코드: ${code}`);
});
4. 워커 풀 구현
여러 CPU 코어를 활용하기 위해 워커 프로세스 풀을 구현할 수 있습니다.
// 워커 풀 예제
const { fork } = require("child_process");
const os = require("os");
class WorkerPool {
constructor(workerPath, numWorkers = os.cpus().length) {
this.workers = [];
this.freeWorkers = [];
// 워커 초기화
for (let i = 0; i < numWorkers; i++) {
const worker = fork(workerPath);
this.workers.push(worker);
this.freeWorkers.push(worker);
worker.on("message", (result) => {
this.freeWorkers.push(worker);
if (this.taskQueue.length > 0) {
const { task, callback } = this.taskQueue.shift();
this.runTask(task, callback);
}
worker.callback(null, result);
});
worker.on("error", (error) => {
worker.callback(error);
this.freeWorkers.push(worker);
});
}
this.taskQueue = [];
}
runTask(task, callback) {
if (this.freeWorkers.length === 0) {
// 사용 가능한 워커가 없으면 작업을 대기열에 추가
this.taskQueue.push({ task, callback });
return;
}
const worker = this.freeWorkers.pop();
worker.callback = callback;
worker.send(task);
}
close() {
for (const worker of this.workers) {
worker.kill();
}
}
}
// 사용 예
const pool = new WorkerPool("worker.js", 4);
pool.runTask({ data: "some data" }, (err, result) => {
if (err) throw err;
console.log(result);
});
환경 변수 및 작업 디렉토리 설정
자식 프로세스를 생성할 때 환경 변수와 작업 디렉토리를 설정할 수 있습니다.
const { spawn } = require("child_process");
const child = spawn("node", ["script.js"], {
cwd: "/path/to/working/directory",
env: { ...process.env, NODE_ENV: "production" },
});
입력 전달 및 출력 처리
자식 프로세스에 입력을 전달하고 출력을 처리하는 방법은 다음과 같습니다:
const { spawn } = require("child_process");
// 'grep' 명령 실행
const grep = spawn("grep", ["keyword"]);
// 표준 입력에 데이터 쓰기
grep.stdin.write("첫 번째 줄에 keyword가 있습니다.\n");
grep.stdin.write("두 번째 줄에는 없습니다.\n");
grep.stdin.end(); // 입력 종료
// 표준 출력 읽기
grep.stdout.on("data", (data) => {
console.log(`일치하는 항목: ${data}`);
});
프로세스 종료 및 신호 처리
자식 프로세스를 종료하거나 신호를 보내는 방법은 다음과 같습니다:
const { spawn } = require("child_process");
const child = spawn("some-command", ["args"]);
// 5초 후 프로세스 종료
setTimeout(() => {
console.log("자식 프로세스 종료 중...");
child.kill(); // 기본적으로 SIGTERM 신호 전송
}, 5000);
// 특정 신호 전송
// child.kill('SIGINT'); // SIGINT 신호 전송 (Ctrl+C와 동일)
child.on("exit", (code, signal) => {
if (code) {
console.log(`자식 프로세스가 코드 ${code}로 종료됨`);
} else if (signal) {
console.log(`자식 프로세스가 신호 ${signal}에 의해 종료됨`);
}
});
오류 처리
자식 프로세스 작업 시 적절한 오류 처리가 중요합니다:
const { spawn } = require("child_process");
try {
// 존재하지 않는 명령 실행 시도
const child = spawn("non-existent-command");
child.on("error", (error) => {
console.error(`프로세스를 시작할 수 없음: ${error.message}`);
});
child.stderr.on("data", (data) => {
console.error(`stderr: ${data}`);
});
child.on("close", (code) => {
console.log(`자식 프로세스 종료 코드: ${code}`);
});
} catch (error) {
console.error(`예외 발생: ${error.message}`);
}
보안 고려사항
child_process
를 사용할 때는 보안 문제를 주의해야 합니다. 특히 사용자 입력을 직접 명령으로 실행하는 것은 셸 주입 공격에 취약할 수 있습니다.
const { execFile } = require("child_process");
// 나쁜 예 (보안 위험)
// const userInput = req.body.command;
// exec(`ls ${userInput}`, ...); // 셸 주입 취약점!
// 좋은 예 (더 안전함)
const userInput = "some-directory"; // 검증된 입력
execFile("ls", [userInput], (error, stdout, stderr) => {
if (error) {
console.error(`실행 오류: ${error}`);
return;
}
console.log(`출력: ${stdout}`);
});
child_process
와 클러스터 모듈의 차이점
Node.js는 child_process
외에도 cluster
모듈을 제공합니다. 이 두 모듈의 주요 차이점은 다음과 같습니다:
child_process
: 임의의 명령을 실행하고, 다른 언어로 작성된 스크립트를 실행하거나, 완전히 별개의 작업을 수행하는 데 적합합니다.cluster
: 동일한 Node.js 코드의 여러 인스턴스를 실행하여 로드 밸런싱하는 데 특화되어 있습니다. 주로 웹 서버 성능 향상을 위해 사용됩니다.
// cluster 모듈 예제
const cluster = require("cluster");
const http = require("http");
const numCPUs = require("os").cpus().length;
if (cluster.isMaster) {
console.log(`마스터 프로세스 ${process.pid} 실행 중`);
// 워커 생성
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.log(`워커 ${worker.process.pid} 종료됨`);
});
} else {
// 워커는 HTTP 서버를 공유합니다
http
.createServer((req, res) => {
res.writeHead(200);
res.end(`워커 ${process.pid}가 응답함\n`);
})
.listen(8000);
console.log(`워커 ${process.pid} 시작됨`);
}
Node.js v14 이후의 최신 기능
Node.js v14부터 child_process
에 몇 가지 개선 사항이 도입되었습니다:
- AbortController 지원: 타임아웃이나 사용자 중단에 따라 자식 프로세스를 쉽게 종료할 수 있습니다.
const { spawn } = require("child_process");
const { AbortController } = require("abort-controller");
const controller = new AbortController();
const { signal } = controller;
// AbortSignal로 프로세스 생성
const child = spawn("sleep", ["5"], { signal });
// 2초 후 프로세스 중단
setTimeout(() => {
controller.abort();
}, 2000);
child.on("error", (err) => {
// AbortError 확인
if (err.name === "AbortError") {
console.log("프로세스가 중단됨");
} else {
console.error("다른 오류:", err);
}
});
요약
child_process
모듈은 Node.js에서 외부 프로세스를 생성하고 통신하는 기능을 제공합니다.- 주요 메소드로는
spawn()
,exec()
,execFile()
,fork()
가 있으며, 각각 다른 용도에 적합합니다. - CPU 집약적인 작업 처리, 다른 언어로 작성된 스크립트 실행, OS 명령 실행 등에 유용합니다.
- 워커 풀을 구현하여 여러 CPU 코어를 활용할 수 있습니다.
- 사용 시 보안 위험을 주의해야 하며, 특히 셸 주입 공격에 대한 방어가 필요합니다.
cluster
모듈과 달리,child_process
는 임의의 명령 실행에 적합합니다.
child_process
모듈은 Node.js의 강력한 기능 중 하나로, 리소스 집약적인 작업을 효율적으로 처리하고 다른 프로그램과 통합하는 데 필수적입니다. 적절히 사용하면 애플리케이션의 성능과 확장성을 크게 향상시킬 수 있습니다.