요즘IT
위시켓
최근 검색어
전체 삭제
최근 검색어가 없습니다.

메시지 큐(Message Queue)는 컴퓨터 시스템에서 쓰이는 비동기 통신 프로토콜의 한 종류입니다. 이를 활용하면 응용 프로그램에서 다른 응용 프로그램으로 메시지를 보낼 수 있으며, 해당 메시지는 수신자인 응용 프로그램이 검색하고 처리할 때까지 대기열에 저장됩니다.

회원가입을 하면 원하는 문장을
저장할 수 있어요!

다음

회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!

확인

개발

Node.js 환경에서 손쉽게 메시지 큐 도입하기(feat. BullMQ)

년차,
어떤 스킬
,
어떤 직무
독자들이 봤을까요?
어떤 독자들이 봤는지 궁금하다면?
로그인

메시지 큐(Message Queue)는 컴퓨터 시스템에서 쓰이는 비동기 통신 프로토콜의 한 종류입니다. 이를 활용하면 응용 프로그램에서 다른 응용 프로그램으로 메시지를 보낼 수 있으며, 해당 메시지는 수신자인 응용 프로그램이 검색하고 처리할 때까지 대기열에 저장됩니다.

 

이러한 메시지 큐는 서버리스 및 마이크로서비스 아키텍처의 중요한 요소입니다. 서비스 간의 비동기 통신을 용이하게 만들어 서비스의 성능(performance), 신뢰성(reliability), 그리고 확장성(scalability)을 올려주기 때문이죠.

 

여기 두 개의 마이크로 서비스를 예로 들어 보겠습니다. 사용자 가입을 처리할 때, 사용자 서비스(User Service)가 환영 이메일을 보내기 위해 이메일 서비스(Email Service)를 호출해야 한다고 하겠습니다. 이메일 서비스가 잘 작동하고 있다면, 사용자는 가입과 동시에 환영 이메일을 받을 수 있습니다.

 

이상적인 경우 <출처: Akshay Jain Medium>

 

그런데 이메일 서비스가 응답에 실패하거나 일시적으로 응답하지 않았다면 어떻게 될까요? 오류 처리가 제대로 이루어진다고 했을 때, 사용자의 가입 처리는 완료될 것입니다. 다만 이메일 서비스가 다운되어 있었기 때문에 사용자가 이메일을 받지 못하겠죠.

 

실패하는 경우 <출처: Akshay Jain Medium>

 

만약 사용자 등록 시점마다 이메일을 보내는 것이 중요한 비즈니스라면, 이는 타협할 수 없는 요구사항입니다. 그렇다면 이 문제를 어떻게 해결해야 할까요? 메시지 큐를 이용해 해결할 수 있습니다.

 

메시지 큐를 쓰면 사용자 서비스는 환영 이메일 전송 요청을 이메일 서비스에 직접 하지 않습니다. 요청 데이터를 메시지 큐에 저장할 뿐이죠. 이메일 서비스는 가능할 때마다 메시지 큐에서 데이터를 꺼내와 이메일을 전송합니다. 즉, 메시지 큐를 매개로 서비스 간 비동기 통신이 이뤄지는 것이죠.

 

메시지 큐 도입 <출처: Akshay Jain Medium>

 

 

BullMQ

분산 환경에서 대량의 메시지를 처리할 때 메시지 큐를 관리하는 것은 어려운 작업입니다. 그럴 때 Redis를 기반으로 구축된 Node.js 라이브러리, BullMQ의 도움을 받을 수 있습니다.

 

BullMQ는 메시지 큐의 관리를 간소화해 주며 빠르고 견고한 시스템을 제공합니다. 그뿐만 아니라 복잡한 작업 수행에 최적화된 여러 기능을 가지고 있죠.

 

<출처: BullMQ>

 

Queues, Workers and Jobs

BullMQ의 작동 방식을 이해하기 위해, 몇 가지 개념을 설명하고 가겠습니다. Job이란 해야 할 일을 명세한 데이터입니다. 이런 Job을 리스트로 관리해 주는 것이 Queue입니다. 이러한 Queue에서 Job을 꺼내 일을 처리하는 존재를 Worker라고 합니다.

 

가입 환영 이메일 처리를 다시 예로 들어 보겠습니다. 가입 요청을 받은 사용자 서비스는 Queue에 Job을 생성합니다. 이메일 서비스에 존재하는 Worker는 곧 가능한 시점에 Job을 꺼내 처리합니다.

 

웹 어플리케이션의 BullMQ Job 할당 과정 <출처: QQQ Medium>

 

Queue에서 Job이 처리되는 과정

Queue는 다양한 유형의 Job을 관리하며 이 작업을 언제, 어떻게 처리할지 결정합니다. Job은 Queue에 추가된 시점부터 다음 중 하나의 상태에 머무릅니다.

 

  • Wait”: Job이 처리되기 전, 일반적으로 진입하는 상태입니다.
  • Prioritized”: 높은 우선순위를 가진 Job이 처리되기 위해 진입하는 상태입니다.
  • Delayed”: 대기 시간을 부여받은 Job이 처리를 기다리는 상태입니다. Job의 priority 옵션에 따라 “Wait” 또는 “Prioritized” 상태로 진입할 수 있습니다.

 

Job이 실제로 처리되기 시작하면 곧 “Active” 상태로 진입합니다. 처리가 성공적으로 완료되면 “Completed” 상태로, 예외가 발생하여 실패하게 된다면 “Failed” 상태로 진입합니다. 

 

이로써 Queue에 진입한 Job의 라이프사이클(Lifecycle)이 끝납니다. 물론 처리에 실패한 Job은 재시도할 수 있습니다. 이 부분은 다음 장에서 살펴보겠습니다.

 

<출처: viblo.asia>

 

 

BullMQ의 주요 기능

BullMQ는 부하 개선 및 복잡한 작업의 수행에 최적화된 여러 기능을 제공합니다. BullMQ에서 제공하는 기능은 무엇이며, 어떠한 상황에서 쓸 수 있는지 알아보겠습니다.

 

Concurrency

메시지 큐라고 반드시 Job을 한 번에 하나씩만 처리하지는 않습니다. 이때 Concurrency로 여러 개의 Job을 병렬로 처리하는 Worker 개수를 조정해 처리 속도를 향상할 수 있습니다. 여기서 Concurrency는 하나의 Node.js 인스턴스에서 실행되는 Worker의 개수를 의미합니다. 만약, PM2나 다른 머신에서 Node.js 인스턴스를 실행하면 그만큼 Worker 개수가 늘어납니다. 예를 들어 Concurrency가 5, Node.js 인스턴스가 3개라면 총 15개 Worker가 병렬로 Job을 처리하게 됩니다.

 

Rate Limiting

Concurrency를 늘린다면, Job의 처리 속도는 올라갑니다. 그러나 Job이 한 번에 몰리면 CPU에 부하가 발생해 전체 서비스에 영향을 끼칠 수도 있습니다. 모든 Job을 언제나 실시간으로 처리할 필요는 없습니다. 따라서 Rate Limit으로 부하를 조정할 수 있습니다. 예를 들어, 초당 10개의 Job만 처리하도록 Rate Limit을 설정하면, Concurrency가 높게 설정되어 있더라도 제한이 걸려 있기 때문에 일정 사용량만 쓰게 됩니다.

 

FIFO / LIFO

BullMQ는 기본적으로 FIFO(First-In, First-Out)방식으로 동작합니다. 만약 Concurrency가 1보다 높게 설정되어 있다면 Worker들은 Queue에서 순서대로 Job을 꺼내어 병렬로 처리합니다. 다만 이때 각 Worker의 처리 시간이 다를 수 있으므로 끝나는 시간은 순서를 보장하지 않습니다. BullMQ는 설정하기에 따라 LIFO(Last-In, Last-Out)방식의 동작도 지원합니다.

 

Priorities

Queue에 Job을 추가할 때, priority 설정으로 우선순위를 부여할 수 있습니다. 숫자가 낮을수록 더 높은 우선순위를 가집니다. 만약 priority를 따로 설정하지 않으면 자동으로 가장 높은 우선순위를 갖게 됩니다.

 

Delayed Jobs

Queue에 Job을 추가할 때, Worker가 이를 바로 처리하지 않고 대기 시간을 갖도록 할 수도 있습니다. 대기 시간은 실시간으로 바뀔 수도 있습니다. 예를 들어, 사용자의 요청이 1분간 발생하지 않은 다음에야 어떠한 이벤트를 처리하려고 한다면, 요청이 들어올 때마다 대기 시간을 변경하는 식으로 이를 만족할 수 있습니다.

 

Scheduled and repeatable jobs according to cron specifications

Queue에 crontab 스펙을 정의하면 반복적인 Job을 추가할 수 있습니다. 이때는 Job을 하나만 추가해도 정의한 일정에 따라 계속 새로운 Job이 추가됩니다. 예를 들어, */15 * * * * 를 설정하면 15분마다 Job이 추가됩니다. 이처럼 매 15분마다 반복되는 작업을 4시 7분에 추가한다고 하겠습니다. 먼저 4시 15분에 실행되는 “Delayed” Job이 만들어질 겁니다. 4시 15분에 해당 Job이 처리되면, 곧 4시 30분에 실행되는 “Delayed” Job이 다시 추가됩니다.

 

Retries of failed jobs

Worker에서 Job을 처리하다 예외가 발생하면, Job 은 “Failed” 상태에 진입합니다. 이때 Queue 설정에 따라 해당 Job을 영원히 보관하거나 자동으로 제거할 수 있습니다. 만약 재시도하는 것이 바람직하다면 Job을 추가할 때, 재시도 횟수와 방법(back-off strategy)을 설정해 줄 수 있습니다. 반면 특정한 경우에 재시도를 원치 않는다면, 예외 발생 시 “UnrecoverableError”를 던져 재시도를 중단할 수 있습니다.

 

Threaded (sandboxed) processing functions

Worker가 Job을 처리할 때 CPU 소모가 많다면, 싱글 스레드로 동작하는 Node.js 이벤트 루프가 굉장히 바빠질 수 있습니다. 이는 곧 다른 Job이 처리되지 않는 상황을 부를 수 있습니다. 이를 막기 위해 CPU 소모가 많은 Job들은 별도의 process 또는 thread로 분리해 동작시킬 수 있습니다.

 

Parent-Child dependencies

Job들 사이 종속성(Parent-Child)을 두는 것도 가능합니다. Child Job이 다 처리되고 난 다음에만 Parent Job이 처리되게 하는 것이죠. 예를 들어, 사용자 100명의 데이터를 수집하는 Job이 다 끝난 다음에, 해당 데이터에 대한 통계를 내는 Job을 바로 실행하도록 조정할 수 있습니다.

 

 

BullMQ와 함께 쓰면 유용한 것들

BullMQ는 MIT 라이선스로 제약사항이 거의 없지만, 대시보드나 알람(Alert) 등 편의 기능을 무료로 제공하진 않습니다. 이런 기능을 쓰려면 Taskforce라는 큐 관리 도구에서 돈을 내야 합니다. 만약 비용이 부담된다면, 비록 Taskforce보다 화려하진 않아도 MIT 라이선스로 무료 제공되는 툴들이 있습니다.

 

@bull-board

@bull-board는 Queue와 Job의 상태를 시각화해 보여주는 툴입니다. Job 삭제 등 조치 기능 역시 간단한 UI로 제공합니다.

 

<출처: github bull-board>

 

Bull Queue Exporter & Grafana

메트릭 수집을 위한 Exporter & Grafana 역시 무료로 사용할 수 있습니다. Grafana 대시보드로는 Queue에서 처리되는 Job을 모니터링 할 수 있습니다. 또한 “Wait” 상태에 머무는 Job이 특정 수치를 넘어가면 Slack 등과 연계해 알람을 받을 수 있습니다.

 

<출처: github UpHabit>

 

 

마치며

메시지 큐는 기본적으로 아래 상황에서 자주 쓰입니다.

 

  • 메인 스레드가 블로킹 없이 시간이 오래 걸리는 작업을 처리할 때 (예: 이메일 보내기, 이미지 처리, 보고서 생성, 주문 처리 등)
  • 특정 시간 또는 간격으로 작업을 실행해야 할 때 (예: 매일 데이터베이스 정리, 이메일 뉴스레터 발송 등)
  • 부하 방지를 위해 작업 속도를 제어해야 할 때

 

BullMQ는 기본적인 메시지 큐 기능에 추가로 여러 기능을 제공하여 복잡한 비즈니스 요구사항을 쉽게 풀어낼 수 있는 솔루션입니다. 또한, Redis 기반으로 동작하기 때문에 Kafka나 RabbitMQ 등 구성이 복잡하고 리소스가 많이 필요한 Message Broker보다 쉽게 접근할 수 있습니다. BullMQ 도입으로 Rest API로는 풀 수 없는 요구사항과 기술적 문제를 해결해 보는 건 어떨까요?

 

요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.

좋아요

댓글

공유

공유

댓글 1
IT 작가를 지망하는 개발자
29
명 알림 받는 중

작가 홈

IT 작가를 지망하는 개발자
29
명 알림 받는 중
현재 IT 경력 13년 차고, VOIP 통신 분야 개발 경력 및 협업 솔루션을 개발하고 있습니다.

한 회사에서 13년 동안 일하면서, 요구사항에 맞춰 학습 및 개발을 하다 보니

프론트와 백엔드를 가리지 않고 개발 경험을 쌓고,

win32 native Api, .net, angular, react, node 등 다양한 프레임들을 경험했습니다.

저는 현재 node 개발자(백엔드 6년차)로 있고,

주로 Database(PostgreSQL)와 API 처리 부분을 담당하면서, 학습하면서 발전하려고 노력하고 있습니다.

좋아요

댓글

스크랩

공유

공유

지금 회원가입하고,
요즘IT가 PICK한 뉴스레터를 받아보세요!

회원가입하기
요즘IT의 멤버가 되어주세요! 요즘IT의 멤버가 되어주세요!
요즘IT의 멤버가 되어주세요!
모든 콘텐츠를 편하게 보고 스크랩해요.
모든 콘텐츠를 편하게 보고 스크랩 하기
매주 PICK한 콘텐츠를 뉴스레터로 받아요.
매주 PICK한 콘텐츠를 뉴스레터로 받기
로그인하고 무료로 사용하기