콘텐츠로 이동

Queue / Worker Basics

분류: Layer 6 - 운영 심화: 관측성 & 복원력

Queue는 처리할 작업 메시지를 보관하는 버퍼이고, Worker는 그 버퍼에서 작업을 꺼내 실제 처리를 수행하는 별도 실행 단위이다. Queue/Worker 패턴의 목적은 “사용자 요청을 받는 흐름”과 “느리거나 실패할 수 있는 작업을 처리하는 흐름”을 분리하는 것이다.

이 패턴을 이해할 때 중요한 단어는 두 개다.

  • 버퍼링: 갑자기 몰린 작업을 바로 처리하지 못해도 Queue에 잠시 쌓아 둔다.
  • 분리: API 서버와 Worker를 따로 배포하고, 따로 장애 격리하고, 따로 확장한다.

회원가입, 주문 생성, 이미지 업로드 같은 API 요청 안에는 “지금 끝나야 하는 일”과 “나중에 처리해도 되는 일”이 섞인다. DB에 회원을 저장하는 일은 응답 전에 끝나야 하지만, 환영 이메일 발송은 보통 몇 초 뒤에 끝나도 된다. 이 둘을 같은 요청 안에서 동기로 처리하면 가장 느린 외부 호출이 전체 응답 시간을 결정한다.

예를 들어 회원가입 API가 DB 저장 80ms, 이메일 API 2초, CRM 동기화 800ms를 모두 기다리면 사용자는 최소 2.88초를 기다린다. 이메일 API가 5초 타임아웃을 채우면 DB 저장은 성공했는데 API 응답은 실패하는 어색한 상태도 생긴다. 사용자는 “가입 실패”라고 보지만, 실제 DB에는 계정이 생겼을 수 있다.

Queue/Worker는 이 문제를 “작업 접수”와 “작업 완료”를 분리해서 푼다. API 서버는 DB 저장 후 이메일 작업을 Queue에 넣고 “가입 접수 완료”를 응답한다. Worker는 Queue에서 이메일 작업을 꺼내 나중에 처리한다. 이메일 API가 느려져도 API 서버의 응답 시간은 크게 흔들리지 않는다.

선행 방식의 한계 — 서버 과부하에서 Queue/Worker가 등장한 이유

섹션 제목: “선행 방식의 한계 — 서버 과부하에서 Queue/Worker가 등장한 이유”

Queue/Worker 이전의 단순한 방식은 API 요청 안에서 모든 일을 끝내는 것이다. 이 방식은 코드가 단순하지만, 세 가지 한계를 만든다.

  1. 느린 의존성이 사용자 응답 시간을 끌고 간다. 이메일, 결제 승인, 이미지 처리, 외부 CRM API처럼 네트워크를 타는 작업은 p95(95번째 백분위 지연)가 흔들린다.
  2. 작업 처리량과 요청 처리량을 따로 늘릴 수 없다. 이메일 발송만 느린데도 API 서버 전체를 증설하게 된다.
  3. 프로세스가 죽으면 in-memory 작업도 사라진다. setTimeout, in-process job, 단순 Promise 분리는 서버 재시작과 장애에 약하다.

Queue/Worker의 핵심 철학은 느린 일을 숨기는 것이 아니라, 느린 일을 견딜 수 있는 위치로 옮기는 것이다. Queue는 내구성 있는 버퍼가 되고, Worker는 실패해도 다시 시작할 수 있는 소비자가 된다. 단, 이 대가로 “즉시 완료” 대신 “나중에 완료”, “정확히 한 번” 대신 “중복될 수 있으므로 안전하게 처리”라는 사고방식이 필요하다.

Amazon SQS(Simple Queue Service, AWS의 관리형 메시지 큐)는 이 패턴의 대표 구현이다. 이 문서는 Queue/Worker 일반 모델에서 출발하되, 운영 수치와 실패 신호는 SQS를 기준 예시로 자주 사용한다. SQS Standard Queue는 메시지를 여러 Availability Zone에 중복 저장해 단일 서버나 AZ 장애로 메시지가 사라질 가능성을 낮춘다. 메시지 보존 기간은 기본 4일, 최대 14일까지 설정할 수 있다. 대신 Standard Queue는 순서와 정확히 한 번 처리를 강하게 보장하지 않고, 최소 1회 전달(at-least-once delivery)을 선택한다.

출처: Amazon SQS queue types - AWS 공식 문서, SQS queue parameters - AWS 공식 문서

Queue/Worker 문서는 약어가 많이 나온다. 아래 표의 용어는 이후 문장에서 다시 길게 풀지 않고 사용한다.

용어첫 정의왜 필요한가
Queue처리할 작업 메시지를 저장하는 대기열Worker가 당장 처리하지 못하는 작업을 잃지 않고 보관한다.
WorkerQueue에서 메시지를 꺼내 실제 작업을 수행하는 프로세스, 컨테이너, Lambda 함수API 서버와 별도 속도로 처리량을 조절한다.
ProducerQueue에 메시지를 넣는 쪽. 보통 API 서버나 이벤트 발행자작업 생성 흐름과 처리 흐름을 분리한다.
ConsumerQueue에서 메시지를 읽는 쪽. Worker와 거의 같은 의미로 쓴다메시지 처리를 병렬화하고 장애를 격리한다.
Message / JobQueue에 들어가는 작업 단위. 예: { orderId, type: "send-email" }Worker가 무엇을 해야 하는지 알려 주는 최소 입력이다.
SQSAmazon SQS(Simple Queue Service). AWS가 운영하는 관리형 Queue 서비스Queue 서버 운영 없이 내구성 있는 메시지 큐를 쓸 수 있다.
Visibility TimeoutWorker가 메시지를 가져간 뒤 다른 Worker에게 잠시 숨기는 시간처리 중인 메시지가 동시에 여러 Worker에게 잡히는 것을 줄인다.
At-least-once delivery메시지를 최소 한 번은 전달하겠다는 보장. 같은 메시지가 두 번 올 수 있다메시지 유실보다 재전달을 우선하는 Queue의 기본 안전장치다.
Idempotency같은 작업을 여러 번 실행해도 최종 결과가 한 번 실행한 것과 같게 만드는 성질at-least-once 전달에서 중복 이메일, 중복 결제 같은 부작용을 막는다.
DLQDead Letter Queue. 반복 실패한 메시지를 격리하는 별도 Queue계속 실패하는 메시지가 정상 메시지 처리를 막지 않게 한다.
Poison message데이터 형식, 상태, 코드 버그 때문에 처리할 때마다 실패하는 메시지재시도만 반복하면 Worker 처리량을 갉아먹으므로 격리가 필요하다.
RedriveDLQ에 격리된 메시지를 원본 Queue로 다시 보내는 작업원인을 고친 뒤 실패 메시지를 재처리한다.
Backpressure소비 속도보다 생산 속도가 빠를 때 시스템이 압력을 드러내거나 속도를 늦추는 메커니즘무한 적체, 비용 폭증, 메시지 만료를 막는다.
Concurrency동시에 처리하는 작업 수. Worker 수와 Worker 내부 병렬 처리 수를 모두 포함한다처리량과 중복 위험, 외부 API 부하를 함께 결정한다.

프론트엔드의 Web Worker처럼 “무거운 일을 메인 흐름 밖으로 보낸다”는 감각은 출발점으로만 유용하다. 백엔드 Queue/Worker에서는 브라우저 탭 안의 스레드 분리가 아니라, 네트워크와 장애를 넘어서는 내구성, 재시도, 중복 처리, 운영 지표가 핵심이다.

Queue/Worker의 기본 흐름은 단순하다. Producer가 메시지를 넣고, Queue가 보관하고, Worker가 가져가 처리하고, 성공하면 삭제한다. 실패하면 일정 시간 뒤 메시지가 다시 보이거나 DLQ로 이동한다.

Queue/Worker 메시지 처리 흐름
flowchart TD
Producer["Producer(API 서버)"] --> Send["SendMessage: 작업 메시지 등록"]
Send --> Store["Queue가 메시지 보관"]
Store --> Receive["Worker가 ReceiveMessage로 수신"]
Receive --> Hidden["Visibility Timeout 동안 숨김"]
Hidden --> Work["작업 처리"]
Work --> Result{"처리 성공?"}
Result -->|"예"| Delete["DeleteMessage: Queue에서 제거"]
Result -->|"아니오 또는 Worker 종료"| Timeout["Visibility Timeout 만료"]
Timeout --> Retry{"maxReceiveCount 초과?"}
Retry -->|"아니오"| Store
Retry -->|"예"| DLQ["DLQ로 격리"]

이 흐름에서 초심자가 가장 자주 놓치는 지점은 ReceiveMessage가 메시지를 삭제하지 않는다는 점이다. Worker가 메시지를 가져가면 Queue는 그 메시지를 잠시 숨긴다. Worker가 성공 후 DeleteMessage를 호출해야 최종 삭제된다. Worker가 죽거나, 예외가 나거나, 삭제 호출이 실패하면 Visibility Timeout 만료 후 메시지는 다시 보인다.

maxReceiveCount는 같은 메시지가 몇 번 다시 보인 뒤 DLQ로 격리될지 정하는 값이다. 이 값은 “몇 번까지 다시 기회를 줄 것인가”와 “poison message가 정상 처리를 얼마나 오래 방해하게 둘 것인가” 사이의 선택이다.

동기 처리와 Queue/Worker 처리를 같은 입력으로 비교해 보자.

단계동기 처리Queue/Worker 처리
1API가 회원 DB 저장을 한다.API가 회원 DB 저장을 한다.
2API가 이메일 API 호출을 기다린다.API가 send-welcome-email 메시지를 Queue에 넣는다.
3이메일 API가 느리면 사용자 응답도 늦어진다.API는 “가입 완료, 이메일 발송 예정”으로 빠르게 응답한다.
4이메일 실패가 API 실패로 보일 수 있다.Worker가 이메일 실패를 재시도하거나 DLQ로 격리한다.
5트래픽이 몰리면 API 서버가 이메일 대기 시간까지 떠안는다.Worker만 늘려 이메일 처리량을 조절할 수 있다.

사용자에게 꼭 필요한 결과가 “회원 계정 생성”이라면 이메일은 Queue에 넣을 수 있다. 반대로 “결제 승인 성공 여부”처럼 사용자가 즉시 알아야 하고 실패 시 주문 생성 자체를 막아야 하는 작업은 Queue 뒤로 숨기면 안 된다. Queue는 실패를 없애는 도구가 아니라, 실패를 사용자 응답 흐름 밖에서 다룰 수 있게 만드는 도구다.

학습용 최소 코드: Producer와 Worker

섹션 제목: “학습용 최소 코드: Producer와 Worker”

아래 코드는 전체 설정 파일이 아니라 역할을 보여 주는 최소 예시다. 핵심은 Producer가 작업을 만들고, Worker가 같은 작업을 나중에 처리한다는 점이다.

// Producer: API 요청 흐름 안에서 작업을 Queue에 등록한다.
@Injectable()
export class OrderService {
constructor(private readonly sqsService: SqsService) {}
async createOrder(dto: CreateOrderDto) {
const order = await this.orderRepository.save(dto);
await this.sqsService.send("email-queue", {
id: `order-${order.id}-email`,
body: JSON.stringify({
jobId: `order:${order.id}:email`,
orderId: order.id,
userId: order.userId,
to: order.email,
template: "order_created",
}),
});
return order;
}
}
// Worker: Queue에서 받은 메시지를 별도 프로세스에서 처리한다.
@Injectable()
export class EmailConsumer {
@SqsMessageHandler("email-queue", false)
async handleEmailJob(message: Message) {
const payload = JSON.parse(message.Body!);
await this.emailService.send({
to: payload.to,
template: payload.template,
orderId: payload.orderId,
});
}
}

이 예시는 아직 완성된 운영 코드가 아니다. 실제 Worker는 메시지 파싱 실패, 이메일 API 실패, 중복 처리, 로깅, 관측 지표를 다뤄야 한다. 그러나 학습 단계에서는 “API는 메시지를 넣고 끝난다”, “Worker는 메시지를 읽어 처리한다”, “성공해야 메시지가 삭제된다”를 먼저 잡으면 된다.

SQS Standard Queue처럼 at-least-once delivery를 택한 Queue에서는 같은 메시지가 두 번 전달될 수 있다. Worker가 이메일을 보낸 직후 네트워크 오류로 DeleteMessage에 실패하면 Queue 입장에서는 성공을 확인하지 못했다. Visibility Timeout이 지나면 같은 메시지가 다시 나온다.

그래서 Worker는 메시지 ID만 믿기보다 비즈니스 관점의 idempotency key를 가져야 한다. 예를 들어 order:{orderId}:email:order_created 같은 키를 두고, 이미 처리 완료된 키라면 이메일을 다시 보내지 않는다. 자세한 재시도·백오프·멱등성 설계는 다음 문서인 Retry / Backoff / Idempotency에서 다루지만, Queue/Worker를 쓰는 순간 “중복될 수 있다”는 전제를 먼저 받아들여야 한다.

메시지에는 무엇을 넣어야 하나

섹션 제목: “메시지에는 무엇을 넣어야 하나”

Queue 메시지는 “Worker가 나중에 독립적으로 처리할 수 있는 최소 입력”이어야 한다. 너무 적게 넣으면 Worker가 처리 시점에 필요한 상태를 다시 조회해야 하고, 너무 많이 넣으면 오래된 스냅샷으로 잘못 처리할 수 있다.

좋은 메시지는 보통 아래 정보를 가진다.

필드예시이유
jobIdorder:123:email:created중복 처리 방지와 추적에 쓴다.
typesend_order_created_emailWorker가 어떤 작업인지 분기한다.
비즈니스 IDorderId: 123, userId: 7최신 상태가 필요하면 DB에서 다시 읽을 기준이 된다.
작은 payloadtemplate: "order_created"Worker가 바로 처리할 수 있는 최소 입력을 준다.
schema versionversion: 1메시지 형식 변경 시 구버전 메시지를 해석할 수 있게 한다.
trace/correlation IDrequestId: "..."API 요청 로그와 Worker 로그를 이어 본다.

반대로 아래 메시지는 위험하다.

{
"user": {
"id": 7,
"email": "old@example.com",
"marketingOptIn": true,
"address": "...",
"grade": "VIP"
},
"order": {
"id": 123,
"status": "CREATED",
"items": ["...large snapshot..."]
}
}

이 메시지는 Worker가 편하게 처리할 수 있지만, Queue에 몇 시간 머무는 동안 사용자의 이메일이나 수신 동의 상태가 바뀔 수 있다. 이메일 발송처럼 최신 동의 상태가 중요한 작업은 userIdtemplate만 넣고 Worker가 처리 직전에 최신 사용자 상태를 다시 읽는 편이 안전하다. 반대로 감사 로그처럼 “그 시점의 상태”가 중요하면 snapshot을 넣는 것이 맞다. 메시지 설계는 “나중에 처리해도 어떤 상태를 기준으로 처리해야 하는가”를 먼저 묻는다.

작업 상태를 어디까지 저장할까

섹션 제목: “작업 상태를 어디까지 저장할까”

Queue는 메시지를 보관하지만, 비즈니스 상태를 모두 대신하지는 않는다. 사용자가 “내 이미지 변환이 끝났나요?”라고 묻는 기능이 필요하면 Queue 메시지 존재 여부가 아니라 DB의 job 상태를 봐야 한다.

일반적인 상태 모델은 단순하다.

상태의미전환 조건
queued작업이 Queue에 들어갔다API가 작업을 접수하고 메시지 전송 성공
processingWorker가 작업을 시작했다Worker가 메시지를 받고 상태 업데이트
succeeded작업이 성공했다Worker 처리 성공 후 결과 저장
failed_retryable다시 시도할 수 있는 실패외부 API timeout, 일시적 DB 오류
failed_permanent재시도해도 의미 없는 실패잘못된 payload, 삭제된 사용자, 검증 오류

모든 작업에 DB 상태 테이블이 필요한 것은 아니다. 단순 알림처럼 사용자가 상태를 조회하지 않고, 중복 부작용이 작고, DLQ로 실패 분석이 충분하면 Queue만으로도 시작할 수 있다. 하지만 사용자에게 진행률을 보여 주거나, 실패를 수동 재처리하거나, 결과 파일 URL을 저장해야 한다면 Queue와 별도로 job state를 둔다.

Queue는 추상적인 안정성 패턴이지만, 운영 판단은 숫자로 한다. 중요한 숫자는 적체량, 처리율, 가장 오래된 메시지 나이, Visibility Timeout, 메시지 보존 기간이다.

처리율은 대략 아래처럼 계산한다.

Worker 처리율 = Worker 수 × Worker당 concurrency ÷ 평균 작업 시간
적체 증가율 = 메시지 유입률 - Worker 처리율
적체 소진 시간 = 현재 적체량 ÷ max(Worker 처리율 - 메시지 유입률, 0)

예를 들어 Worker 5대가 있고, 각 Worker가 동시에 4개 작업을 처리하며, 평균 작업 시간이 2초라면 처리율은 5 × 4 ÷ 2 = 초당 10개다.

상황계산해석
유입률 8개/s, 적체 10,000개순처리율 2개/s → 약 83분느리지만 언젠가 따라잡는다.
유입률 10개/s, 적체 10,000개순처리율 0개/s현재 적체가 줄지 않는다. Worker 증설이나 작업 시간 개선이 필요하다.
유입률 15개/s, 적체 10,000개초당 5개씩 더 쌓임Queue는 버퍼가 아니라 병목을 감추는 저장소가 된다.

ApproximateNumberOfMessagesVisible은 대기 중인 메시지 수이고, ApproximateAgeOfOldestMessage는 가장 오래 기다린 메시지의 나이다. 메시지 수가 높아도 오래된 메시지 나이가 낮으면 순간 피크일 수 있다. 반대로 메시지 수가 적어도 가장 오래된 메시지 나이가 계속 증가하면 특정 poison message나 처리 불능 상태를 의심해야 한다.

Visibility Timeout은 Worker가 메시지를 가져간 뒤 다른 Worker에게 보이지 않게 숨기는 시간이다. 기본값은 30초이고, SQS에서는 최대 12시간까지 설정할 수 있다.

너무 짧으면 처리 중인 메시지가 다시 보이면서 중복 처리된다. 너무 길면 Worker가 죽었을 때 재시도가 늦어진다. 따라서 “길수록 안전”도 아니고 “짧을수록 빠른 복구”도 아니다. 작업 시간 분포와 실패 복구 시간을 같이 봐야 한다.

Lambda와 SQS를 Event Source Mapping(Lambda가 SQS를 폴링해 함수를 호출하도록 묶는 설정)으로 연결할 때는 AWS가 더 보수적인 기준을 권장한다.

SQS Visibility Timeout >= Lambda 함수 타임아웃 × 6
SQS Visibility Timeout >= Lambda 함수 타임아웃 × 6 + MaximumBatchingWindowInSeconds
예시:
Lambda 타임아웃: 5분(300초)
Batch window: 0초 → 권장 Visibility Timeout: 300 × 6 = 1800초(30분)
Batch window: 60초 → 권장 Visibility Timeout: 300 × 6 + 60 = 1860초(31분)

이 공식은 Lambda가 배치 처리 중 스로틀링되거나 재시도될 시간을 확보하기 위한 것이다. 직접 폴링하는 NestJS Worker라면 공식 자체보다 원리가 중요하다. p99(99번째 백분위) 처리 시간, graceful shutdown 시간, 외부 API timeout, 재시도 비용을 보고 숨김 시간을 잡는다.

작업 시간이 입력마다 크게 달라지면 ChangeMessageVisibility로 특정 메시지의 숨김 시간을 연장하는 선택지도 있다. 다만 이것은 “긴 작업을 정상 완료하게 돕는 장치”이지 무제한 보류 장치가 아니다. Worker timeout보다 Visibility Timeout이 짧으면 중복 처리 위험이 커지고, 반대로 숨김 시간이 과도하게 길면 Worker가 죽었을 때 복구가 늦어진다.

출처: Creating and configuring an Amazon SQS event source mapping - AWS Lambda 공식 문서, Amazon SQS Visibility Timeout - AWS 공식 문서

SQS 메시지 보존 기간은 기본 4일, 최대 14일이다. Worker가 며칠 동안 밀릴 수 있는 배치라면 Queue가 메시지를 영구히 보관해 준다고 가정하면 안 된다. 적체 소진 시간이 보존 기간을 넘을 수 있으면 retention을 늘리거나, Kafka나 DB job table처럼 재처리 가능한 저장소를 검토한다.

Long Polling은 Worker가 메시지를 받을 때 Queue에 메시지가 생길 때까지 최대 20초 기다리는 방식이다. Short Polling은 메시지가 없어도 즉시 빈 응답을 돌려주므로 호출 수가 늘어난다. 직접 폴링 Worker에서는 waitTimeSeconds: 20처럼 설정해 빈 응답 비용을 줄일 수 있다.

출처: Amazon SQS short and long polling - AWS 공식 문서

Worker 처리량을 올리는 방법은 Worker 수를 늘리는 것만이 아니다. 한 번에 여러 메시지를 가져오는 batch size, Worker 내부 concurrency, 외부 API 호출 병렬도를 같이 본다.

조정값늘리면 좋아지는 점늘리면 나빠지는 점
Worker 수Queue를 더 빨리 비운다배포 비용, DB 연결 수, 외부 API 호출량이 늘어난다.
Worker 내부 concurrency같은 컨테이너로 더 많은 작업을 처리한다CPU, 메모리, event loop 지연, 장애 전파 범위가 커진다.
Batch sizeQueue API 호출 수가 줄고 처리량이 오른다배치 안 일부 실패 처리와 Visibility Timeout 계산이 복잡해진다.
Visibility Timeout긴 작업의 중복 처리가 줄어든다Worker 장애 시 재시도가 늦어진다.
Retry 횟수일시 장애를 더 잘 흡수한다poison message가 오래 리소스를 잡아먹을 수 있다.

예를 들어 평균 500ms 작업이라면 concurrency 20도 감당할 수 있어 보인다. 하지만 각 작업이 DB connection을 하나씩 잡고 외부 이메일 API를 호출한다면 Worker 10대만으로도 동시 외부 호출이 200개가 된다. Queue 처리량 계산은 CPU뿐 아니라 DB pool, 외부 API rate limit, 네트워크 timeout을 함께 넣어야 한다.

Queue를 넣으면 API 응답 latency는 줄어들지만, 사용자 관점의 전체 완료 시간은 새로 생긴다.

전체 완료 시간 = API 처리 시간 + Queue 대기 시간 + Worker 처리 시간 + 후속 저장/알림 시간

환영 이메일은 전체 완료 시간이 1분이어도 괜찮을 수 있다. 반면 업로드한 이미지의 썸네일을 사용자가 바로 보려는 화면에서는 1분 대기가 UX 장애일 수 있다. Queue가 맞는지는 “API p95가 줄었는가”만이 아니라 “사용자가 기대하는 완료 시간 안에 들어오는가”로 봐야 한다.

작은 판단 기준을 세우면 다음과 같다.

  • 사용자가 결과를 즉시 기다리는 화면이면 Queue 대기 시간을 UX 예산에 포함한다.
  • 작업 완료를 이메일, 푸시, 상태 페이지로 알릴 수 있으면 Queue 대기를 숨기기 쉽다.
  • 오래 걸리는 작업이면 queued, processing, succeeded, failed 상태를 노출한다.
  • 완료 시간이 약속된 SLA(Service Level Agreement, 사용자나 팀에 약속한 서비스 수준)를 넘으면 Worker 증설보다 작업 쪼개기, 우선순위 Queue, 별도 빠른 경로를 검토한다.

SQS에는 Standard Queue와 FIFO Queue가 있다. 둘 다 Queue지만 설계 철학이 다르다.

Standard Queue와 FIFO Queue 선택

Standard Queue

고가용성과 높은 처리량을 우선하고, 중복 전달과 best-effort ordering을 허용한다.

이메일, 알림, 이미지 처리처럼 멱등성으로 중복을 방어할 수 있을 때

FIFO Queue

MessageGroupId 안의 순서와 deduplication ID(중복 억제용 식별자) 기반 중복 억제를 우선한다.

재고 차감, 주문 상태 전이처럼 순서 오류를 비즈니스 로직으로 흡수하기 어려울 때

Standard Queue의 기본 선택 이유는 처리량과 단순성이다. 중복 전달과 순서 흔들림을 Worker 로직으로 흡수할 수 있다면 Standard Queue가 대개 운영하기 쉽다.

FIFO Queue는 순서가 중요한 작업에서 유용하지만, “전체 Queue의 모든 메시지가 전역 순서로 빠르게 처리된다”는 뜻은 아니다. FIFO의 순서 보장은 MessageGroupId 단위다. 모든 메시지를 하나의 group에 넣으면 순서는 단순해지지만 병렬성이 거의 사라진다. 사용자별 순서만 중요하다면 MessageGroupId = userId, 주문별 순서만 중요하다면 MessageGroupId = orderId처럼 순서 경계를 좁혀야 한다.

FIFO Queue는 기본 처리량이 API 작업별 초당 300회, 배치 10개 사용 시 초당 3,000개 메시지 수준으로 제한된다. 고처리량 FIFO 모드를 켜면 더 늘릴 수 있지만, 순서 보장은 여전히 message group 설계에 묶인다. 따라서 선택 원칙은 간단하다.

  • 멱등성 구현이 쉽고 순서가 중요하지 않으면 Standard Queue.
  • 순서 오류가 비즈니스 장애로 직결되면 FIFO Queue.
  • FIFO를 쓰더라도 message group을 너무 넓게 잡으면 Worker를 늘려도 처리량이 늘지 않는다.

출처: Amazon SQS FIFO queue quotas - AWS 공식 문서, Amazon SQS queue types - AWS 공식 문서

주문 상태 변경 이벤트를 FIFO Queue에 넣는다고 하자.

MessageGroupId = "orders"
messages = [
order-1: CREATED,
order-2: CREATED,
order-3: CREATED,
...
]

모든 메시지가 같은 orders group에 들어가면 Queue는 순서를 지키기 위해 사실상 한 줄로 처리한다. Worker를 20대로 늘려도 같은 group의 앞 메시지가 끝나기 전에는 뒤 메시지를 병렬 처리하기 어렵다. “FIFO니까 안전하다”는 판단은 맞지만, “FIFO니까 Worker를 늘리면 처리량도 쉽게 늘어난다”는 판단은 틀릴 수 있다.

더 나은 설계는 순서가 필요한 경계를 좁히는 것이다.

MessageGroupId = orderId
order-1: CREATED -> PAID -> SHIPPED
order-2: CREATED -> CANCELLED
order-3: CREATED -> PAID

이렇게 하면 같은 주문 안의 상태 순서는 지키면서, 서로 다른 주문은 병렬 처리할 수 있다. FIFO의 핵심 질문은 “전체 순서가 필요한가, 특정 엔티티 안의 순서만 필요한가”다.

7. Backpressure: Queue는 압력을 없애지 않는다

섹션 제목: “7. Backpressure: Queue는 압력을 없애지 않는다”

Backpressure는 소비자가 처리할 수 있는 속도보다 생산자가 더 빨리 메시지를 만들 때, 그 압력을 지표나 제한으로 드러내는 메커니즘이다. Queue가 있으면 API 서버는 빠르게 응답할 수 있지만, Worker 처리량이 부족한 사실이 사라지는 것은 아니다. 압력은 Queue depth, 오래된 메시지 나이, DLQ, 비용, 외부 API rate limit 형태로 이동한다.

좋은 Queue/Worker 설계는 “쌓일 수 있다”에서 끝나지 않고 “얼마나 쌓이면 위험한가”를 정한다.

관찰 값의미먼저 볼 질문
ApproximateNumberOfMessagesVisible 증가대기 메시지가 늘어난다유입률이 늘었는가, Worker 처리율이 줄었는가?
ApproximateNumberOfMessagesNotVisible 증가Worker가 잡고 숨긴 메시지가 많다처리 시간이 길어졌는가, Worker가 멈췄는가?
ApproximateAgeOfOldestMessage 증가오래 기다리는 메시지가 있다poison message인가, 전체 처리율 부족인가?
DLQ 메시지 증가반복 실패한 메시지가 격리된다코드 버그, 데이터 문제, 외부 서비스 장애 중 무엇인가?
외부 API 429 증가Worker가 외부 의존성을 과도하게 때린다concurrency를 낮추거나 backoff를 넣어야 하는가?

Backpressure 대응은 보통 세 갈래다. Worker를 늘려 처리율을 높인다. Producer에서 생성 속도를 제한한다. 작업을 더 작게 쪼개거나 외부 API 호출을 줄여 평균 처리 시간을 낮춘다. Queue depth만 보고 Worker를 무작정 늘리면 외부 API rate limit이나 DB 병목을 더 심하게 만들 수 있다.

장애 상황에서 바로 Worker 수를 늘리면 눈앞의 queue depth는 줄 수 있다. 하지만 병목이 DB나 외부 API라면 Worker 증설은 병목을 더 때리는 행동이다. 대응 순서는 보통 아래처럼 잡는다.

  1. 유입 증가인지 처리율 감소인지 분리한다. 배포 이후 Worker 에러가 늘었는지, 트래픽 이벤트로 메시지 생성이 늘었는지 먼저 본다.
  2. 외부 의존성 한계를 확인한다. 이메일 API, 결제 API, DB pool, Redis 연결 수가 이미 포화라면 Worker 증설보다 concurrency 제한이 먼저다.
  3. 작업 실패율을 본다. 실패율이 높으면 retry가 새 메시지처럼 부하를 만든다. retry storm은 queue depth를 빠르게 키운다.
  4. 일시적 피크면 Worker를 늘린다. 처리율이 병목보다 낮고 외부 의존성 여유가 있으면 autoscaling이 맞다.
  5. 지속적 피크면 생산 속도나 작업 구조를 바꾼다. API rate limit, 우선순위 Queue, batch 처리, 작업 분할을 검토한다.

Queue가 backpressure를 잘 드러내려면 알람도 “메시지 수” 하나로 끝내지 않는다. 메시지 수, oldest age, DLQ 수, Worker error rate, 외부 API 429, 처리 시간 p95를 함께 본다. 메시지 수만 보면 큰 배치 직후의 정상 피크와 진짜 처리 불능을 구분하기 어렵다.

8. 언제 Queue를 쓰고, 언제 피할까

섹션 제목: “8. 언제 Queue를 쓰고, 언제 피할까”

Queue 도입 기준은 “비동기가 멋있어 보이는가”가 아니라 “작업의 완료 시점과 실패를 사용자 응답에서 분리해도 되는가”다.

Queue 도입 판단 기준

  • 작업이 실패해도 사용자에게 '접수됨' 상태로 설명할 수 있다.
  • Worker가 죽어도 작업을 잃지 않고 재시도해야 한다.
  • 작업 처리량을 API 요청 처리량과 독립적으로 스케일링해야 한다.
  • queue depth, visibility timeout, DLQ, redrive를 관측하고 운영할 준비가 있다.

네 항목 중 앞의 두 항목이 특히 중요하다. 실패해도 나중에 복구할 수 없는 작업을 Queue 뒤로 숨기면 장애가 늦게 발견된다.

상황Queue 적합도이유
회원가입 후 환영 이메일높음이메일 발송이 몇 초 늦어져도 가입 자체는 완료될 수 있다.
이미지 리사이즈와 썸네일 생성높음원본 업로드 후 파생 파일은 비동기로 만들어도 된다.
외부 CRM 동기화높음외부 서비스 장애를 내부 API 응답과 분리할 수 있다.
카드 승인 결과 확인낮음승인 실패 시 주문 생성 자체를 막아야 하므로 즉시 결과가 필요하다.
매일 자정 정산보통 낮음이벤트가 아니라 시간 자체가 트리거라면 Cron Job이나 Scheduler가 단순하다.
단일 서버 안의 짧은 CPU 작업낮음Worker Thread나 in-process async로 충분할 수 있다.
장기 이벤트 재생이 필요한 데이터 파이프라인SQS보다 Kafka 쪽SQS는 장기 이벤트 로그와 offset replay를 목적으로 하지 않는다.

아래 질문을 위에서부터 내려가면 Queue가 필요한지 더 빨리 판단할 수 있다.

1. 사용자가 이 작업의 성공/실패를 즉시 알아야 하는가?
├─ 예: 동기 처리 또는 별도 상태 화면이 필요하다.
└─ 아니오:
2. 작업이 실패해도 나중에 재시도하면 되는가?
├─ 아니오: Queue 뒤로 숨기면 실패가 늦게 발견된다.
└─ 예:
3. 작업량이 API 서버 처리량과 다르게 출렁이는가?
├─ 아니오: in-process async나 scheduler가 더 단순할 수 있다.
└─ 예:
4. 중복 처리와 DLQ 운영을 감당할 수 있는가?
├─ 아니오: 먼저 작업 상태와 운영 지표를 설계한다.
└─ 예: Queue/Worker 후보가 된다.

이 트리는 Queue를 금지하려는 것이 아니다. 오히려 Queue를 넣었을 때 생기는 새 책임을 미리 드러내기 위한 것이다. Queue를 넣으면 API 코드는 가벼워지지만, 메시지 스키마, retry, DLQ, redrive, idempotency, 지표 알람이 새 설계 대상이 된다.

SQS, BullMQ, RabbitMQ, Kafka의 선택 감각

섹션 제목: “SQS, BullMQ, RabbitMQ, Kafka의 선택 감각”

세부 구현은 팀의 인프라와 운영 역량에 따라 다르다. 처음에는 “어떤 문제가 중심인가”로 좁혀 본다.

도구강점약점잘 맞는 사용처
SQS관리형, AWS 통합, 운영 부담 낮음메시지 재생과 복잡한 라우팅에는 약함이메일, 알림, 이미지 처리, 단순 비동기 작업
BullMQRedis 기반, 로컬 개발과 NestJS 연동이 쉬움Redis 메모리와 job 기록 관리가 필요작은 팀의 백그라운드 job, 로컬/내부 도구
RabbitMQExchange 기반 라우팅과 프로토콜 지원브로커 운영이 필요복잡한 라우팅, 여러 소비자 패턴
Kafka파티션, offset, 장기 보존, 이벤트 재생운영 난도가 높고 단순 작업 큐로는 무거움데이터 파이프라인, 이벤트 스트리밍, 재처리

SQS와 Kafka를 같은 “비동기 메시징”으로 묶으면 판단이 흐려진다. SQS는 작업을 맡기고 성공하면 삭제하는 Queue에 가깝다. Kafka는 이벤트 로그를 보존하고 여러 Consumer Group(같은 스트림을 나누어 읽는 소비자 묶음)이 각자 offset(어디까지 읽었는지의 위치)을 움직이며 다시 읽을 수 있는 스트림에 가깝다. “작업을 한 번 처리하고 끝낼 것인가”와 “이벤트 기록을 여러 시스템이 다시 읽어야 하는가”를 먼저 나눈다.

Queue/Worker 장애는 대부분 “메시지가 사라졌다”보다 “메시지가 계속 쌓인다”, “같은 메시지가 반복된다”, “실패 메시지가 격리되지 않는다”로 나타난다. 긴 명령어보다 먼저 증상과 원리를 연결해 보자.

실패 신호개념적으로 의심할 지점
Queue에 메시지가 계속 쌓인다생산 속도가 소비 속도보다 빠르다Worker 수, concurrency, 외부 API 지연, backpressure
NotVisible 메시지가 높게 유지된다Worker가 잡은 메시지를 끝내지 못한다긴 처리 시간, hang, Visibility Timeout, graceful shutdown
DLQ에 메시지가 쌓인다재시도해도 실패하는 메시지가 있다poison message, 코드 버그, 외부 서비스 장애
같은 이메일이 두 번 발송된다같은 작업이 중복 처리됐다at-least-once delivery, DeleteMessage 실패, idempotency 부재
가장 오래된 메시지 나이가 계속 증가한다일부 메시지가 오래 방치된다처리율 부족, FIFO message group 병목, poison message
Redrive 후 다시 DLQ로 돌아온다원인을 고치지 않고 재처리했다메시지 형식, 데이터 정합성, 코드 배포 상태
메시지가 처리 전에 만료된다retention 기간보다 적체 시간이 길다보존 기간, Worker 처리율, 배치 크기

DLQ는 실패를 버리는 곳이 아니다. DLQ는 정상 처리 흐름을 보호하면서, 사람이 원인을 분석할 수 있게 실패 메시지를 격리하는 곳이다. Redrive는 원인 해결 후 재처리하는 행위다. 원인을 고치지 않은 Redrive는 같은 poison message를 다시 원본 Queue로 밀어 넣는 것에 가깝다.

Worker가 아래처럼 email 필드가 항상 있다고 가정한다고 하자.

const payload = JSON.parse(message.Body!);
await emailService.send(payload.email, payload.template);

어떤 메시지가 { "userId": "1", "template": "welcome" }처럼 email 없이 들어오면 이 메시지는 처리할 때마다 실패한다. Worker를 1대에서 10대로 늘려도 해결되지 않는다. 처리량 문제가 아니라 데이터 검증과 격리 문제이기 때문이다. 이때 DLQ가 없다면 같은 메시지가 계속 재노출되어 정상 메시지 처리까지 방해할 수 있다.

DLQ로 보내기 전 재시도 횟수를 정하는 값이 maxReceiveCount다. 너무 낮으면 일시 장애 메시지가 빨리 DLQ로 빠지고, 너무 높으면 poison message가 오래 Worker를 괴롭힌다.

간단한 기준은 실패 종류별로 생각하는 것이다.

실패 종류예시재시도 가치설정 감각
일시 장애이메일 API timeout, 네트워크 오류높음retry/backoff와 함께 몇 차례 재시도
rate limit외부 API 429중간즉시 반복보다 backoff와 concurrency 제한이 중요
payload 오류필수 필드 누락, JSON 파싱 실패낮음빠르게 DLQ로 보내고 코드/데이터 수정
비즈니스 불일치삭제된 사용자, 취소된 주문낮음 또는 수동 판단DLQ나 별도 실패 상태로 사람이 판단

AWS 문서와 실무 예시는 maxReceiveCount를 5 이상으로 잡는 경우가 많지만, 숫자 자체보다 재시도 간격과 실패 원인이 중요하다. maxReceiveCount = 10이어도 backoff 없이 즉시 재시도하면 짧은 시간에 같은 실패를 10번 반복할 뿐이다. 재시도 정책은 다음 문서의 주제지만, 이 문서에서는 DLQ가 “무한 재시도 방지 장치”라는 점을 기억한다.

Queue/Worker의 가장 흔한 배포 형태는 API 서버와 Worker를 별도 서비스로 두는 것이다.

API 서버 (ECS Service, 최소 2대)
└─ SendMessage
SQS email-queue
Email Worker (ECS Service, 1~10대 오토스케일)

스케일링 기준도 분리된다.

  • API 서버는 HTTP 요청 수, CPU, latency를 보고 늘린다.
  • Worker는 queue depth, oldest message age, 처리 시간, 외부 API rate limit을 보고 늘린다.
  • Worker 증설이 외부 API나 DB를 더 때릴 수 있으므로 concurrency 상한을 함께 둔다.

예를 들어 ApproximateNumberOfMessagesVisible > 100이면 Worker ECS 태스크를 2개 늘리고, < 10이 오래 유지되면 줄일 수 있다. 다만 이 숫자는 예시일 뿐이다. 평균 작업 시간이 100ms인 Queue와 30초인 Queue의 100 messages는 전혀 다른 의미다.

로컬 개발에서 BullMQ를 쓰는 경우

섹션 제목: “로컬 개발에서 BullMQ를 쓰는 경우”

AWS SQS는 로컬 개발과 테스트에서 불편할 수 있다. Redis 기반 BullMQ를 로컬 job queue로 쓰고, 운영에서는 SQS를 쓰는 팀도 있다. 이때 중요한 것은 API 코드가 “작업 등록” 인터페이스에만 의존하고, 실제 Queue 구현은 환경에 따라 바뀌게 하는 것이다.

// BullMQ Producer의 핵심 모양
await this.emailQueue.add("welcome", {
jobId: `user:${userId}:welcome-email`,
userId,
email,
});
// BullMQ Worker의 핵심 모양
@Processor("email")
export class EmailConsumer extends WorkerHost {
async process(job: Job<{ userId: string; email: string }>) {
await this.emailService.sendWelcome(job.data.email);
}
}

BullMQ는 Redis에 job 상태와 기록을 저장한다. 완료·실패 기록을 무한히 남기면 Redis 메모리를 압박하므로 removeOnComplete, removeOnFail 같은 보존 정책도 설계 대상이다. SQS의 message retention을 정하듯, Redis job 기록도 “얼마나 남겨야 디버깅에 충분한가”로 결정한다.

직접 폴링 Worker는 메시지가 없을 때도 Queue를 계속 조회할 수 있다. Long Polling은 메시지가 생길 때까지 최대 20초 기다려 빈 응답 비용을 줄인다.

SqsModule.register({
consumers: [
{
name: "email-queue",
queueUrl: process.env.SQS_EMAIL_QUEUE_URL,
region: "ap-northeast-2",
waitTimeSeconds: 20,
},
],
});

Lambda를 SQS Trigger로 연결한 경우에는 Event Source Mapping이 Queue를 폴링한다. NestJS Worker처럼 직접 폴링하는 경우에 Long Polling 설정을 더 직접적으로 신경 쓴다.

긴 실습 절차보다 “무엇을 보면 어떤 개념이 보이는가”에 집중한다.

  • 코드베이스에서 Queue 사용 흔적 찾기:
    Terminal window
    rg "SqsModule|BullModule|RabbitMQ|@SqsMessageHandler|Queue<|WorkerHost" src/
  • AWS SQS 콘솔에서 Queue 목록을 보고 Standard인지 FIFO인지 확인한다. Queue URL이나 이름이 .fifo로 끝나면 FIFO다.
  • 각 Queue의 Messages Available, Messages in Flight, Age of Oldest Message를 보고 대기, 처리 중, 오래된 메시지를 구분한다. Messages in Flight는 CloudWatch의 ApproximateNumberOfMessagesNotVisible과 같은 관점, 즉 Worker가 가져가 숨겨진 메시지 수를 보는 지표다.
  • DLQ가 연결되어 있는지 확인하고, DLQ 메시지 수가 0보다 크면 어떤 실패가 격리됐는지 샘플 payload를 본다.
  • Worker 로그에서 한 메시지가 시작, 성공, 실패, 재시도, DLQ 이동 중 어디까지 보이는지 따라간다.
  • Visibility Timeout과 Worker p95 처리 시간을 비교해 “처리 중 재노출” 가능성을 생각해 본다.
개념 A개념 B차이점
동기 처리비동기 처리동기는 완료까지 기다리고, 비동기는 접수 후 백그라운드에서 처리한다.
QueuePub/SubQueue는 보통 한 메시지를 한 Consumer가 처리하고, Pub/Sub은 여러 Subscriber에게 같은 이벤트를 배포한다.
RetryDLQRetry는 다시 시도하는 것이고, DLQ는 반복 실패한 메시지를 격리하는 곳이다.
Visibility TimeoutMessage RetentionVisibility Timeout은 처리 중 숨김 시간이고, retention은 Queue가 메시지를 보관하는 총 기간이다.
Standard QueueFIFO QueueStandard는 높은 처리량과 중복 가능성을 받아들이고, FIFO는 group 단위 순서를 우선한다.
BackpressureAutoscalingBackpressure는 압력을 드러내는 원리이고, autoscaling은 그 압력에 대한 대응 중 하나다.
SQSKafkaSQS는 작업 큐에 가깝고, Kafka는 재생 가능한 이벤트 로그에 가깝다.

Queue/Worker 복습 체크

  • Queue와 Worker를 각각 정의하고, Producer/Consumer 흐름을 그릴 수 있다.
  • 동기 요청 안에서 느린 작업을 처리할 때 생기는 응답 지연과 불일치를 설명할 수 있다.
  • Visibility Timeout이 메시지 삭제가 아니라 숨김 시간이라는 점을 설명할 수 있다.
  • At-least-once delivery 때문에 idempotency가 필요한 이유를 설명할 수 있다.
  • DLQ, poison message, redrive의 관계를 설명할 수 있다.
  • Queue depth와 oldest message age를 보고 처리율 부족과 poison message 가능성을 구분할 수 있다.
  • Standard Queue와 FIFO Queue의 선택 기준과 MessageGroupId 병목을 설명할 수 있다.
  • Queue가 적합하지 않은 동기 작업과 cron 작업 반례를 들 수 있다.

Queue/Worker의 일반형은 “생산자와 소비자를 분리하고, 사이에 버퍼를 둔다”이다. 이 모델은 여러 레이어에서 반복된다.

레이어생산자버퍼소비자
브라우저메인 스레드postMessage() 채널Web Worker
OS 프로세스부모 프로세스IPC 파이프/소켓자식 프로세스
백엔드 서비스API 서버SQS / RabbitMQ / BullMQWorker 프로세스
이벤트 스트리밍서비스Kafka 토픽Consumer Group

다만 같은 모양이라고 같은 의미는 아니다. 브라우저의 Web Worker는 UI thread 차단을 줄이는 것이 중심이고, SQS Worker는 내구성, 재시도, 중복 처리, 운영 지표가 중심이다. 형태의 유사성은 출발점이고, 판단 기준은 각 레이어의 실패 모델에서 나온다.

Queue/Worker를 이해하면 CQRS나 Event Sourcing도 더 쉽게 읽힌다. CQRS는 쓰기와 읽기의 모델을 분리하고, Event Sourcing은 상태 변경을 이벤트로 저장한다. 둘 다 “지금 처리하는 흐름”과 “나중에 읽거나 처리하는 흐름”을 분리한다는 점에서 Queue/Worker와 같은 사고 모델을 공유하지만, 저장 기간과 재생 가능성, 정합성 요구가 더 강해진다.

출처: CQRS Pattern - Microsoft Azure Architecture Center

Event-Driven Architecture, FIFO Queue, Message Deduplication, Fan-out Pattern, Backpressure, At-least-once vs Exactly-once, Long Polling, BullMQ, CQRS, Event Sourcing, Poison Message, Redrive Policy

  1. Queue는 느리거나 실패할 수 있는 작업을 보관하는 버퍼이고, Worker는 그 작업을 별도로 처리한다.
  2. Queue/Worker는 동기 요청의 응답 지연과 과부하를 줄이지만, 작업 완료 시점을 나중으로 미룬다.
  3. SQS 같은 Queue는 at-least-once delivery를 택하므로 Worker는 idempotency를 가져야 한다.
  4. Visibility Timeout, DLQ, redrive, backpressure는 Queue/Worker의 핵심 운영 개념이다.
  5. Queue는 모든 비동기 문제의 답이 아니며, 즉시 결과가 필요한 작업이나 단순 스케줄 작업에는 더 단순한 대안이 맞을 수 있다.

본문을 이해하는 데 필수는 아니지만, 실제 장애에서 개념을 확인할 때 유용한 짧은 절차다. 원칙은 “원인 해결 전 redrive 금지”, “Worker 증설 전 외부 병목 확인”, “지표를 메시지 생명주기와 연결”이다.

Queue에 메시지가 쌓이고 처리가 안 될 때
  1. ApproximateNumberOfMessagesVisible을 본다. 계속 증가하면 Worker 처리율이 유입률보다 낮다.
  2. ApproximateNumberOfMessagesNotVisible을 본다. 높게 유지되면 Worker가 메시지를 잡고 완료하지 못하는 상태일 수 있다.
  3. Worker 태스크나 프로세스가 실행 중인지 확인한다.
  4. Worker 로그에서 파싱 오류, 외부 API timeout, DB 연결 오류를 찾는다.
  5. 메시지 처리 오류 로그에는 messageId, jobId, correlationId, error class를 함께 남겼는지 확인한다. 이 값이 없으면 DLQ 샘플과 애플리케이션 로그를 연결하기 어렵다.
  6. Visibility Timeout이 실제 처리 시간보다 짧아 재노출이 반복되는지 확인한다.
  7. 처리율 부족이면 Worker 수나 concurrency를 조정하되, 외부 API rate limit과 DB 부하를 같이 본다.
DLQ에 메시지가 쌓였을 때
  1. DLQ 메시지 샘플을 하나 확인해 payload 종류를 본다.
  2. 실패 원인을 세 가지로 나눈다: 파싱/검증 오류, 외부 서비스 장애, 비즈니스 데이터 불일치.
  3. 코드나 외부 의존성 문제가 해결됐는지 확인한다.
  4. 원인 해결 후에만 redrive한다.
  5. 대량 redrive는 다시 retry storm을 만들 수 있으므로 작은 배치나 속도 제한으로 시작한다.
  6. Redrive 후 다시 DLQ로 돌아오면 같은 원인이 남아 있다는 뜻이다.
중복 처리가 발생했을 때
  1. 같은 business id의 작업이 두 번 처리됐는지 확인한다.
  2. 처리 시간과 Visibility Timeout을 비교한다.
  3. Worker가 성공 후 DeleteMessage 전에 종료됐는지 로그를 본다.
  4. idempotency key나 처리 완료 기록이 있는지 확인한다.
  5. FIFO 전환은 마지막 선택지로 검토한다. FIFO도 message group 설계와 처리량 제한을 함께 가진다.
DLQ retention과 BullMQ 기록 보존

Standard Queue에서 DLQ로 이동한 메시지는 원본 enqueue timestamp를 기준으로 나이를 계산한다. 원본 Queue retention이 4일이고 메시지가 3일 동안 재시도된 뒤 DLQ로 이동했는데, DLQ retention도 4일이면 운영자는 약 1일 뒤 메시지를 잃을 수 있다. 분석이 필요한 DLQ는 원본 Queue보다 retention을 길게 잡는 편이 안전하다.

Terminal window
aws sqs get-queue-attributes \
--queue-url https://sqs.ap-northeast-2.amazonaws.com/123456/email-dlq \
--attribute-names MessageRetentionPeriod \
--region ap-northeast-2

예상 출력이 "MessageRetentionPeriod": "1209600"이면 14일이다. "345600"이면 4일이다.

BullMQ에서는 완료·실패 기록이 Redis 메모리를 차지하므로 보존 정책을 둔다.

await this.emailQueue.add(
"welcome",
{ userId, email },
{
removeOnComplete: { count: 1000 },
removeOnFail: { count: 5000 },
attempts: 3,
backoff: { type: "exponential", delay: 1000 },
},
);

SQS에서는 retention과 DLQ를 보고, BullMQ에서는 Redis 메모리와 job 기록 보존 수를 본다. 둘 다 “실패 분석에 필요한 만큼 남기되, 무한히 쌓이게 두지 않는다”는 기준은 같다.

출처: Setting-up dead-letter queue retention in Amazon SQS - AWS 공식 문서