2026년 08월 17일

BullMQ Consumer의 HTTP 콜백 실패를 재시도하는 구조

메시지 큐를 사용하는 시스템에서 컨슈머가 외부 HTTP 엔드포인트를 호출하는 구조는 흔히 볼 수 있습니다. BullMQ는 Node.js 환경에서 Redis 기반의 잡 큐를 구현하는 라이브러리로, 잡 처리 중 HTTP 요청이 실패했을 때 이를 어떻게 재시도하고 실패를 기록할지 설계하는 것이 안정성에 직결됩니다. 이 글에서는 BullMQ 워커가 HTTP 콜백을 호출할 때 발생할 수 있는 실패 시나리오와, 이를 구조적으로 재시도하는 방법을 단계별로 설명합니다.

BullMQ와 HTTP 콜백 구조의 기본 개념

BullMQ에서 잡(job)은 큐(queue)에 추가되고, 워커(worker)가 해당 잡을 꺼내 처리합니다. 이 처리 과정에서 외부 HTTP API를 호출해 결과를 전달하거나, 웹훅 방식으로 처리 결과를 알리는 패턴이 자주 사용됩니다.

HTTP 콜백 호출이 실패하는 원인은 다양합니다.

  • 대상 서버의 일시적인 오류 (5xx 응답)
  • 네트워크 타임아웃
  • DNS 해석 실패
  • 인증 토큰 만료 등 클라이언트 측 오류 (4xx 응답)

이 중 일시적인 오류는 재시도로 해결될 가능성이 높지만, 4xx 계열의 오류는 재시도해도 동일하게 실패하는 경우가 많습니다. 따라서 재시도 전략을 설계할 때는 이 두 가지 유형을 구분하는 것이 중요합니다.

BullMQ의 기본 재시도 메커니즘

BullMQ는 잡이 실패했을 때(워커 함수에서 예외가 throw되면) 자동으로 재시도할 수 있는 옵션을 제공합니다. 잡을 큐에 추가할 때 attempts와 backoff 옵션을 설정하면 됩니다.

await queue.add('http-callback', { url: 'https://example.com/webhook', payload: { id: 1 } }, {
  attempts: 5,
  backoff: {
    type: 'exponential',
    delay: 1000,
  },
});

attempts는 최초 시도를 포함한 총 시도 횟수를 의미합니다. backoff 옵션에서 exponential 타입을 사용하면 재시도 간격이 지수적으로 증가합니다. 1초, 2초, 4초, 8초 순으로 대기 후 재시도하게 되어 대상 서버에 부하를 덜 줄 수 있습니다.

fixed 타입을 사용하면 매 재시도마다 동일한 간격으로 대기합니다. 어떤 타입이 적합한지는 외부 서비스의 특성과 SLA에 따라 다르게 결정해야 합니다.

워커에서 HTTP 요청 실패를 제어하는 방법

기본 재시도 옵션만으로는 부족한 경우가 있습니다. 특히 HTTP 응답 코드에 따라 재시도 여부를 다르게 처리해야 할 때는 워커 내부에서 직접 로직을 작성해야 합니다.

import { Worker, UnrecoverableError } from 'bullmq';
import axios from 'axios';

const worker = new Worker('http-callback', async (job) => {
  const { url, payload } = job.data;

  try {
    const response = await axios.post(url, payload, { timeout: 5000 });
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error) && error.response) {
      const status = error.response.status;

      // 4xx 오류는 재시도해도 의미 없으므로 즉시 실패 처리
      if (status >= 400 && status < 500) {
        throw new UnrecoverableError(`클라이언트 오류: ${status}`);
      }
    }
    // 그 외 오류는 throw하여 BullMQ의 재시도 로직에 위임
    throw error;
  }
}, { connection: redisConnection });

UnrecoverableError는 BullMQ에서 제공하는 특수한 에러 클래스입니다. 이 오류를 throw하면 남은 재시도 횟수와 상관없이 잡이 즉시 실패 상태(failed)로 이동합니다. 이를 활용하면 불필요한 재시도를 줄이고 실패 원인을 명확하게 구분할 수 있습니다.

실패한 잡을 모니터링하고 처리하는 전략

모든 재시도가 소진되거나 UnrecoverableError가 발생하면 잡은 failed 상태가 됩니다. 이후 처리 방식은 시스템의 요구사항에 따라 달라집니다.

이벤트 리스너로 실패 감지하기

BullMQ 워커는 failed 이벤트를 통해 실패한 잡을 감지할 수 있습니다.

worker.on('failed', (job, err) => {
  if (job) {
    console.error(`잡 ${job.id} 실패:`, err.message);
    // 알림 발송, 데이터베이스 기록 등 후속 처리
  }
});

이 이벤트 핸들러 안에서 Slack 알림, 이메일 발송, 외부 로깅 시스템 연동 등 다양한 후속 처리를 추가할 수 있습니다.

Dead Letter Queue(DLQ) 패턴 적용

실패한 잡을 별도의 큐로 옮겨 나중에 수동으로 처리하거나 분석하는 패턴을 Dead Letter Queue라고 합니다. BullMQ에는 DLQ 기능이 기본 내장되어 있지 않지만, failed 이벤트 핸들러에서 직접 구현할 수 있습니다.

import { Queue } from 'bullmq';

const dlQueue = new Queue('http-callback-failed', { connection: redisConnection });

worker.on('failed', async (job, err) => {
  if (job && job.attemptsMade >= (job.opts.attempts ?? 1)) {
    await dlQueue.add('retry-later', {
      originalJobId: job.id,
      data: job.data,
      error: err.message,
      failedAt: new Date().toISOString(),
    });
  }
});

이렇게 하면 최종 실패한 잡의 데이터와 오류 정보를 별도 큐에 보존하여, 나중에 수동으로 재처리하거나 원인을 분석하는 데 활용할 수 있습니다.

재시도 횟수와 백오프 전략 설계 가이드

재시도 전략은 시스템 특성에 맞게 조정해야 합니다. 다음 기준을 참고해 설정값을 결정하세요.

항목권장 값설명
attempts3~5회너무 많으면 처리 지연 및 큐 적체 발생
backoff 타입exponential서버 복구 시간을 고려한 점진적 대기
초기 delay1,000~3,000ms대상 서비스 응답 특성에 따라 조정
timeout3,000~10,000ms요청당 최대 허용 대기 시간

재시도 횟수가 너무 많으면 일시적인 장애 상황에서 큐에 잡이 쌓이고 전체 처리 속도가 느려지는 문제가 생깁니다. 반대로 너무 적으면 복구 가능한 오류도 실패로 기록될 수 있습니다. 일반적으로 지수 백오프를 사용할 때 3~5회 재시도 설정이 균형 있는 선택입니다.

잡 데이터에 재시도 컨텍스트 포함하기

잡이 재시도될 때 워커 함수에서 현재 시도 횟수를 확인하고, 이에 따라 동작을 다르게 처리하고 싶다면 job.attemptsMade 속성을 활용할 수 있습니다.

const worker = new Worker('http-callback', async (job) => {
  const { url, payload } = job.data;

  // 재시도 횟수에 따라 타임아웃을 늘리는 예시
  const timeout = 3000 + job.attemptsMade * 2000;

  const response = await axios.post(url, payload, { timeout });
  return response.data;
}, { connection: redisConnection });

첫 번째 시도에서는 3초, 두 번째에서는 5초, 세 번째에서는 7초로 타임아웃을 늘려가는 방식입니다. 대상 서버가 응답이 느린 상황에서 유용하게 활용할 수 있습니다.

동시성과 재시도 전략의 상호작용

BullMQ 워커의 동시성(concurrency) 설정도 재시도 동작에 영향을 줍니다. 동시 처리 잡 수가 많을수록 재시도 중인 잡과 새로운 잡이 함께 처리됩니다. 외부 HTTP 서비스에 장애가 발생한 상황에서 모든 잡이 동시에 재시도를 반복하면 대상 서버에 불필요한 부하를 주거나, Redis 연결이 과부하될 수 있습니다.

이를 방지하기 위해 워커의 동시성 수치를 적절히 제한하고, 필요하다면 rate limiter 옵션을 함께 활용하는 것이 좋습니다.

const worker = new Worker('http-callback', processor, {
  connection: redisConnection,
  concurrency: 5,
  limiter: {
    max: 10,
    duration: 1000,
  },
});

limiter 옵션은 지정된 시간 동안 처리할 수 있는 잡의 최대 수를 제한합니다. 위 예시는 초당 최대 10개의 잡을 처리하도록 제한합니다.

전체 구조 요약

지금까지 설명한 내용을 정리하면, BullMQ에서 HTTP 콜백 실패를 안정적으로 처리하는 구조는 다음 흐름으로 구성됩니다.

  1. 잡 추가 시 attempts와 backoff 옵션으로 기본 재시도 정책 설정
  2. 워커 내부에서 HTTP 응답 코드에 따라 재시도 가능/불가능 오류 구분
  3. 복구 불가능한 오류에는 UnrecoverableError를 사용해 즉시 실패 처리
  4. failed 이벤트 리스너로 최종 실패 잡 감지 및 알림
  5. 필요 시 Dead Letter Queue로 실패 잡 데이터 보존
  6. 동시성과 rate limiter를 조합해 대상 서비스 부하 제어

이 구조를 기반으로 시스템의 요구사항과 외부 서비스의 안정성 특성에 맞게 각 설정값을 조정하면, HTTP 콜백 실패에도 데이터 손실 없이 안정적인 잡 처리 파이프라인을 운영할 수 있습니다.