모든 Node.js 개발자는 조만간 똑같은 문제에 직면합니다. 사용자가 버튼을 클릭하면, 라우트 핸들러가 무거운 작업을 처리하기 시작하고, HTTP 요청은 그 상태로 멈춰 있게 됩니다. 대량의 이메일을 보내거나, 서드파티 CRM에 레코드를 동기화하거나, PDF 보고서를 생성하는 중일 수도 있습니다. 브라우저는 로딩 중 상태로 돌아가고, 모바일 앱은 타임아웃이 발생합니다. 사용자는 불만을 갖게 되고, 서버는 낭비해서는 안 될 연결 슬롯을 소모하게 됩니다. 해결책은 해당 작업을 요청 경로에서 분리하여 Redis 기반의 백그라운드 작업 큐(background job queue)로 옮기는 것입니다. Node.js 생태계에서 이 분야를 주도하는 두 라이브러리는 Bull과 BullMQ입니다. 이 둘 사이의 선택은 승자를 가리는 문제가 아니라, 현재 프로젝트의 상태와 나아갈 방향을 이해하는 문제에 가깝습니다.
오리지널 워크호스
Bull은 수년 동안 Node.js 백그라운드 처리의 표준이었습니다. 안정적이고 검증되었으며, 수많은 프로덕션 애플리케이션에서 실행되고 있습니다. 작업을 나중에 실행하도록 예약하거나, 실패한 임포트를 자동으로 재시도하거나, 뉴스레터 발송보다 결제 웹훅이 먼저 실행되도록 엄격한 우선순위를 지정해야 하는 경우, Bull은 문제없이 이를 처리합니다. API가 콜백(callback) 중심이어서, Promise가 아직 생소했던 오래된 코드베이스에 자연스럽게 녹아듭니다. 오랫동안 Bull에 의존해 온 팀은 무엇을 기대할 수 있는지 정확히 알고 있습니다. 이 라이브러리는 Redis에 상태를 유지하므로, Node 프로세스가 재시작되어도 작업은 유지됩니다. 이러한 신뢰성 덕분에 많은 기업이 이미 잘 작동하는 시스템을 변경해야 한다는 압박을 느끼지 않았습니다.
BullMQ가 바꾸는 것들
BullMQ는 그 후속작입니다. TypeScript로 처음부터 다시 구축되었으며, 전체 API 구조가 async/await를 중심으로 설계되었습니다. 지난 몇 년간 현대적인 Node.js 코드를 작성해 왔다면, 문법이 즉시 익숙하게 느껴질 것입니다. 하지만 차이점은 타입 정의나 Promise 체인보다 더 깊은 곳에 있습니다. BullMQ는 큐(queue)와 워커(worker) 사이의 깔끔한 분리를 강제합니다. Bull에서는 큐가 워커 실행기 역할을 겸하는 경우가 많습니다. 반면 BullMQ에서는 한 파일에 큐를 정의하고 다른 파일에 워커를 정의합니다. 이러한 분리는 실제 프로덕션 시스템이 확장되는 방식을 반영합니다. API 서버는 큐에 작업을 추가하기만 하고, 워커 컨테이너 군단은 작업만 처리하도록 배포할 수 있습니다. 시스템이 커져도 아키텍처의 가독성이 유지됩니다.
승패를 가르는 기능들
BullMQ가 진정으로 앞서 나가는 지점은 Bull이 제공하지 못하는 기능에 있습니다. 실제 애플리케이션에서 가장 중요한 세 가지 추가 기능은 다음과 같습니다.
작업 흐름 (Job Flows)
복잡한 워크플로우가 단일 백그라운드 함수에 들어가는 경우는 드뭅니다. 이미지 처리 파이프라인을 구축한다고 가정해 봅시다. 사용자가 원본 사진을 업로드하면, 백엔드는 썸네일을 만들고, 압축된 미리보기를 생성하고, OCR 스캔을 실행한 다음, 모든 준비가 완료되었음을 프론트엔드에 알려야 합니다. Bull을 사용한다면 이 모든 단계를 하나의 크고 취약한 핸들러에 몰아넣었을 것입니다. BullMQ는 작업 흐름(job flows)을 도입하여 부모 작업과 자식 작업을 명시적으로 체이닝할 수 있게 해줍니다. 썸네일 생성과 OCR 작업이 모두 성공한 후에만 알림 단계가 실행되도록 의존성을 정의할 수 있습니다. OCR이 실패하면 썸네일을 다시 처리할 필요 없이 해당 부분만 재시도할 수 있습니다. 로직이 모듈화되고 관찰 가능해지며, 새벽 3시에 무언가 고장 났을 때 디버깅하기가 훨씬 쉬워집니다.
그룹별 속도 제한 (Group Rate Limiting)
멀티테넌트(multi-tenant) SaaS 애플리케이션을 운영 중이라면, 한 고객이 워커에 과부하를 주는 상황을 걱정해 본 적이 있을 것입니다. 단일 테넌트가 만 개의 내보내기 작업을 큐에 쌓아 다른 모든 사용자의 작업을 마비시킬 수 있습니다. BullMQ는 그룹별 속도 제한(group rate limiting) 기능을 추가하여 테넌트별 또는 API 키별로 처리 속도를 조절할 수 있게 합니다. 예를 들어, 테넌트 A가 분당 50개의 외부 API 호출을 수행할 수 있도록 허용하면서, 테넌트 B에게는 독립적인 동일한 할당량을 부여할 수 있습니다. 큐는 특정 머신에서만 로컬로 제한하는 것이 아니라, 모든 워커 인스턴스에 걸쳐 전역적으로 이러한 제한을 준수합니다. 이는 갑자기 필요해지기 전까지는 그 가치를 체감하기 어려운 안전장치와 같습니다.
현대적인 인터페이스
BullMQ는 레거시 콜백 시그니처를 버리고 현대적인 API를 채택했습니다. 에러 핸들링은 표준 Promise 패턴을 따릅니다. TypeScript 정의는 별도의 커뮤니티 패키지에서 나중에 추가된 것이 아니라, 일급 시민(first-class)으로서 기본적으로 제공됩니다. 새로 시작하는 프로젝트(greenfield project)라면 개발자 경험(DX)이 눈에 띄게 매끄러울 것입니다. 에디터는 큐 옵션을 자동 완성해주고, 린터(linter)는 누락된 작업 이름을 잡아냅니다. 인지적 부하가 줄어듭니다.
변하지 않는 요소: Redis
이 결정에서 실질적인 위안이 되는 점은 인프라입니다. Bull과 BullMQ 모두 작업 상태, 메타데이터, 스케줄을 Redis에 저장합니다. 내부 키 구조는 다르지만, 기반 기술은 동일합니다. 이미 Bull을 위해 Redis를 운영 중이라면, BullMQ를 도입하기 위해 새로운 데이터베이스로 교체하거나 배포 토폴로지를 다시 고민할 필요는 없습니다. 마이그레이션의 과제는 서버 비용이 아니라 애플리케이션 코드에 있습니다.
마이그레이션의 현실
그렇다고 해서 Bull에서 BullMQ로 옮기는 것이 단순히 교체만 하면 되는 작업은 아닙니다. API 호출 방식이 바뀌고, 이벤트 이름도 다릅니다. 프로세서를 정의하고 동시성을 처리하는 방식이 상당히 달라지기 때문에, 큐와 통신하는 모든 파일을 수정해야 할 것입니다. 더 중요한 점은, 스위치를 켠다고 해서 기존 작업들이 새 시스템에서 자동으로 완료되기를 기대할 수 없다는 것입니다. 동일한 Redis 인스턴스에 BullMQ 워커를 실행하기 전에, 기존 Bull 큐를 완전히 비워야(drain) 합니다. 그렇지 않으면 동일한 키스페이스(keyspace) 내에서 서로 다른 두 형식이 충돌할 위험이 있습니다. 점검 시간(maintenance window)을 확보하거나 블루-그린(blue-green) 전환 방식을 계획하십시오. 이는 상당한 노력이 필요한 작업이며, 그 노력이 충분한 가치를 만들어내야 합니다.
선택의 기준
현재 Bull 설정이 문제없이 잘 돌아가고 있다면, 그대로 두십시오. 안정성은 그 자체로 가치가 있습니다. 백그라운드 큐는 인프라이지, 유행을 따르는 도구가 아닙니다. 만약 부모-자식 워크플로우(parent-child workflows)나 테넌트별 속도 제한(per-tenant rate limits)이 절실하여 팀이 현재 아키텍처와 싸우고 있다면, 마이그레이션은 타당한 선택입니다. 더 깔끔한 관심사 분리(separation of concerns)와 현대적인 API는 시간이 지나면서 그 노력에 대한 보답을 해줄 것입니다.
새로운 프로젝트라면 선택은 더 간단합니다. BullMQ로 시작하십시오. BullMQ는 정기적인 업데이트를 제공하며, 최신 JavaScript 표준을 즉시 지원하고, 6개월 만에 라이브러리의 한계에 부딪히지 않고도 복잡한 작업 흐름을 구축할 수 있는 여유를 제공합니다. 유지 관리자들이 이미 넘어선 API 위에 기술 부채를 쌓는 일을 피할 수 있습니다.
핵심 요약
작업 큐의 존재 목적은 HTTP 응답 속도를 빠르게 유지하고 사용자의 인내심을 지켜주는 것입니다. Bull은 여전히 그 역할을 훌륭히 수행합니다. BullMQ는 현대적인 Node.js 애플리케이션이 구축되고 확장되는 방식에 맞는 구조로 그 역할을 수행합니다. 문제는 단순히 어떤 라이브러리가 더 나은가가 아닙니다. 현재 겪고 있는 고통이 마이그레이션을 할 만큼 가치가 있는지, 그리고 다음 프로젝트가 다음 투자 유치나 제품 출시 전까지 교체할 필요가 없는 견고한 기반을 갖출 자격이 있는지가 핵심입니다.
