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의 강력한 기능 중 하나로, 리소스 집약적인 작업을 효율적으로 처리하고 다른 프로그램과 통합하는 데 필수적입니다. 적절히 사용하면 애플리케이션의 성능과 확장성을 크게 향상시킬 수 있습니다.

results matching ""

    No results matching ""