
Node.js의 오래된 고민, 동시성
Node.js를 사용하는 개발자라면 한 번쯤 이런 상황을 겪어봤을 것입니다. 서버가 대부분의 시간에는 빠르게 응답하는데, 특정 요청이 들어오면 갑자기 모든 요청의 응답 시간이 느려지는 현상. 이는 Node.js의 근본적인 아키텍처 특성 때문입니다. Node.js는 싱글 스레드 이벤트 루프 위에서 동작합니다. 네트워크 I/O나 파일 읽기처럼 비동기적으로 처리할 수 있는 작업은 이벤트 루프가 효율적으로 다루지만, CPU를 오래 점유하는 연산(JSON 파싱, 암호화, 데이터 변환 등)이 들어오면 이벤트 루프 전체가 블록됩니다. 그 동안 다른 모든 요청은 대기해야 하죠.
이 문제를 해결하기 위해 Node.js에 도입된 것이 바로 Worker Threads입니다. Node.js 10.5에서 실험적으로 등장하고 12 버전에서 안정화된 이 기능은, 메인 스레드와 별도의 JavaScript 실행 환경(V8 Isolate)을 가진 워커 스레드를 생성할 수 있게 해줍니다. Inngest 팀은 이 Worker Threads를 실전에 적용한 경험을 공유하면서, "문제가 많지만 우리 상황에는 잘 맞았다"는 솔직한 평가를 내놓았습니다.
Worker Threads가 뭐고, 기존 방식과 뭐가 다른가
먼저 Node.js에서 동시성을 확보하는 기존 방법들을 살펴봅시다. 가장 전통적인 방법은 child_process를 사용해 완전히 별도의 Node.js 프로세스를 생성하는 것입니다. 이 방식은 각 프로세스가 독립된 메모리 공간을 가지므로 안전하지만, 프로세스 생성 비용이 크고 메모리 사용량이 높습니다. Cluster 모듈은 여러 프로세스가 같은 포트를 공유하며 들어오는 요청을 분산 처리하는 방식으로, 주로 HTTP 서버의 수평 확장에 사용됩니다.
Worker Threads는 이 둘의 중간 지점에 있습니다. 별도의 프로세스를 생성하는 것이 아니라, 같은 프로세스 안에서 별도의 스레드를 생성합니다. 각 워커 스레드는 자체 V8 인스턴스와 이벤트 루프를 가지지만, SharedArrayBuffer를 통해 메모리를 공유할 수 있고, MessagePort를 통해 효율적으로 데이터를 주고받을 수 있습니다. 프로세스보다 가볍고, 데이터 전달이 빠르다는 장점이 있죠.
Inngest가 Worker Threads를 선택한 이유
Inngest는 이벤트 기반 함수 실행 플랫폼을 만드는 회사로, 사용자가 제출한 함수를 실행하고 그 결과를 관리하는 작업을 처리합니다. 이 과정에서 CPU 집약적인 작업(함수 코드의 파싱, 실행 상태 관리, 직렬화/역직렬화 등)이 메인 이벤트 루프를 블록하는 문제가 있었습니다.
그들이 Worker Threads를 선택한 핵심 이유는 메인 이벤트 루프의 반응성을 유지하면서 CPU 작업을 오프로드하기 위해서였습니다. 별도 프로세스를 쓰는 것보다 통신 오버헤드가 적고, 메모리 공유가 가능해서 대용량 데이터를 다루는 상황에서도 효율적이었다고 합니다.
그래서 문제가 뭔데?
하지만 Inngest 팀이 "문제투성이"라고 표현한 데는 이유가 있습니다. Worker Threads의 주요 고통점들은 다음과 같습니다.
첫째, 디버깅이 어렵습니다. 워커 스레드에서 발생한 에러의 스택 트레이스가 메인 스레드로 깔끔하게 전달되지 않는 경우가 많습니다. 크래시가 발생했을 때 어떤 워커에서, 어떤 맥락에서 문제가 생겼는지 추적하기가 까다롭죠.
둘째, 데이터 전달의 복잡성입니다. 메인 스레드와 워커 간 데이터를 주고받을 때는 기본적으로 구조화된 복제(Structured Clone)가 일어납니다. 이는 JSON.stringify/parse와 유사하게 객체를 깊은 복사하는 것으로, 대용량 데이터의 경우 이 복사 비용이 무시할 수 없습니다. SharedArrayBuffer를 쓰면 복사를 피할 수 있지만, 동기화 문제를 직접 관리해야 하는 부담이 생깁니다.
셋째, npm 패키지 호환성 문제입니다. 모든 npm 패키지가 Worker Threads 환경에서 정상 동작하는 것은 아닙니다. 전역 상태에 의존하거나 네이티브 애드온을 사용하는 패키지는 워커 스레드에서 예상치 못한 동작을 할 수 있습니다.
다른 선택지와의 비교
이런 문제들 때문에 많은 팀이 아예 다른 언어로 CPU 집약적 부분을 분리하는 방법을 택합니다. Rust로 작성한 로직을 napi-rs나 Neon을 통해 Node.js 네이티브 애드온으로 연결하거나, Go나 Rust로 별도 마이크로서비스를 구성하는 식이죠. 최근에는 Bun이 워커 스레드보다 더 효율적인 스레딩 모델을 제공한다고 주장하고 있고, Deno도 Web Worker API를 통한 멀티스레딩을 지원합니다.
그럼에도 Inngest가 Worker Threads를 고수한 이유는 실용적입니다. 기존 Node.js/TypeScript 코드베이스를 그대로 유지할 수 있고, 별도 언어의 빌드 파이프라인이나 인프라를 추가할 필요가 없으며, 팀 전체가 하나의 언어로 의사소통할 수 있다는 점이 결정적이었습니다.
한국 개발자에게 주는 시사점
한국에서도 Node.js를 메인 백엔드로 사용하는 스타트업과 중견 기업이 많습니다. 대부분의 웹 API 서버에서는 I/O 바운드 작업이 주를 이루므로 싱글 스레드로도 충분하지만, 서비스가 성장하면서 PDF 생성, 이미지 처리, 복잡한 데이터 집계 같은 CPU 작업이 끼어들기 시작하면 이벤트 루프 블록 문제가 현실이 됩니다.
이런 상황에서 Worker Threads는 기존 코드베이스를 크게 변경하지 않으면서 적용할 수 있는 현실적인 해법입니다. 다만, 프로덕션에 적용할 때는 Inngest의 경험처럼 디버깅과 에러 핸들링에 충분히 대비해야 합니다. 워커 풀(Worker Pool)을 직접 구현하기보다는 piscina나 workerpool 같은 검증된 라이브러리를 사용하는 것도 추천합니다.
마무리
Node.js Worker Threads는 완벽한 솔루션은 아니지만, "Node.js 생태계 안에서 CPU 문제를 해결해야 한다"는 제약 조건 하에서는 가장 현실적인 선택지입니다. 중요한 것은 도구의 한계를 정확히 인지한 상태에서 사용하는 것이죠.
여러분의 Node.js 프로젝트에서 CPU 바운드 작업은 어떻게 처리하고 계신가요? Worker Threads, 별도 마이크로서비스, 혹은 아예 다른 런타임을 사용하는지 경험을 공유해주세요.
🔗 출처: Hacker News
TTJ 코딩클래스 정규반
월급 외 수입,
코딩으로 만들 수 있습니다
17가지 수익 모델을 직접 실습하고, 1,300만원 상당의 자동화 도구와 소스코드를 받아가세요.
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공