Node.js 워커 스레드로 멀티스레딩 구현하기

작성일 :

Node.js 워커 스레드로 멀티스레딩 구현하기

Node.js는 기본적으로 싱글 스레드 이벤트 루프를 사용합니다. 이는 대부분의 I/O 작업에 효율적이지만 CPU 집약적인 작업에 대해서는 성능이 떨어질 수 있습니다. 이 문제를 해결하기 위해 Node.js는 워커 스레드(worker threads) 모듈을 제공합니다. 이 글에서는 워커 스레드 모듈을 활용하여 Node.js에서 멀티스레딩을 구현하는 방법에 대해 알아보겠습니다.

워커 스레드 모듈 이해하기

워크 스레드는 Node.js 버전 10.5.0에서 도입되었으며, 이를 통해 메인 스레드와 분리된 별도의 스레드에서 작업을 실행할 수 있습니다. 이는 CPU 집약적인 작업을 메인 스레드로부터 분리하여 애플리케이션의 반응성을 유지하는 데 도움이 됩니다.

워크 스레드는 기본적으로 worker_threads 모듈을 사용하여 생성됩니다. 이 모듈은 Worker 클래스를 제공하며, 새로운 스레드를 생성하고 관리하는 데 사용됩니다.

기본적인 워커 스레드 예제

다음은 기본적인 워커 스레드 예제입니다. 이 예제에서는 메인 스레드와 워커 스레드 간의 메시지 교환을 수행합니다.

javascript
// main.js
const { Worker } = require('worker_threads');

function runService(workerData) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0)
        reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

runService('Hello, Worker!')
  .then((result) => console.log(result))
  .catch((err) => console.error(err));
javascript
// worker.js
const { parentPort, workerData } = require('worker_threads');

parentPort.postMessage(`Worker received: ${workerData}`);

이 예제에서 main.jsworker.js를 워커 스레드로 생성하고, 메인 스레드와 워커 스레드 간의 메시지 교환을 수행합니다. 워커 스레드는 메인 스레드로부터 데이터를 받아서 처리하고 결과를 반환합니다.

워커 스레드에서 CPU 집약적인 작업 수행

이제 CPU 집약적인 작업을 워커 스레드에서 수행하는 방법을 살펴보겠습니다. 예를 들어, 큰 소수(prime)를 찾는 작업입니다.

javascript
// main.js
const { Worker } = require('worker_threads');

function findLargePrime() {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./prime-worker.js');
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0)
        reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

findLargePrime()
  .then((result) => console.log(`Found a large prime: ${result}`))
  .catch((err) => console.error(err));
javascript
// prime-worker.js
const { parentPort } = require('worker_threads');

function isPrime(num) {
  for (let i = 2; i <= Math.sqrt(num); i++) {
    if (num % i === 0) return false;
  }
  return true;
}

function findLargePrime() {
  let num = 2;
  while (true) {
    if (isPrime(num)) return num;
    num++;
  }
}

parentPort.postMessage(findLargePrime());

위 예제에서는 prime-worker.js가 큰 소수를 찾는 작업을 수행합니다. 이 작업은 CPU 집약적이며, 메인 스레드와 분리된 워커 스레드에서 실행됨으로써 메인 스레드가 다른 작업을 계속해서 수행할 수 있도록 합니다.

다중 워커 스레드 관리

복잡한 애플리케이션에서는 한 번에 여러 워커 스레드를 생성하고 관리해야 할 필요가 있습니다. 이를 위해 우리는 여러 워커 스레드를 생성하고 처리하는 예제를 살펴보겠습니다.

javascript
// main.js
const { Worker } = require('worker_threads');
const numWorkers = 4;

function runService(workerData) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0)
        reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

for (let i = 0; i < numWorkers; i++) {
  runService(`Data ${i + 1}`)
    .then((result) => console.log(result))
    .catch((err) => console.error(err));
}

위 예제는 4개의 워커 스레드를 생성하고 각각의 워커가 데이터를 처리하도록 합니다. 이를 통해 CPU 집약적인 작업을 병렬로 수행하여 성능을 최적화할 수 있습니다.

결론

Node.js의 워커 스레드 모듈을 사용하여 CPU 집약적인 작업을 메인 스레드와 분리함으로써 애플리케이션의 성능을 향상시킬 수 있습니다. 위에서 설명한 기본적인 예제를 통해 워커 스레드를 사용하는 방법을 이해하고, 다양한 상황에 맞게 응용할 수 있습니다. 이를 통해 더욱 효율적이고 반응성 높은 Node.js 애플리케이션을 개발할 수 있을 것입니다.