콘텐츠로 이동

Queue / Worker Basics

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

Queue는 처리할 작업을 대기열에 넣는 것이고, Worker는 그 대기열에서 작업을 꺼내 처리하는 프로세스이다.

모든 요청을 즉시 처리하면 서버가 과부하에 걸린다. 이메일 발송, 이미지 처리, 알림 전송 같은 “지금 당장 안 해도 되는 작업”은 Queue에 넣고 나중에 처리하는 것이 안정적인 시스템의 기본 패턴이다.

프론트엔드 개발자 관점에서: 브라우저의 Web Worker와 Service Worker는 메인 스레드를 차단하지 않기 위해 postMessage()로 메시지를 주고받는다. SQS Queue/Worker 패턴의 개념적 구조가 동일하다 — 메인 흐름(API 서버)이 작업을 큐(SQS)에 넣고, 별도 프로세스(Worker)가 독립적으로 꺼내서 처리한다. Web Worker가 무거운 연산을 메인 스레드에서 분리하듯, SQS Worker는 무거운 작업을 API 요청 흐름에서 분리한다. 차이점은 규모다 — Web Worker는 같은 브라우저 탭 안의 분리이고, SQS는 별도 서버 프로세스(또는 Lambda)로 확장 가능한 분리다.

Queue/Worker 동작 방식 (전체 흐름)

섹션 제목: “Queue/Worker 동작 방식 (전체 흐름)”

비유: 식당 주문 접수와 같다. 손님(API 요청)이 주문하면 종업원(API 서버)이 “접수했습니다” 라고 즉시 응답하고, 주방 메모판(Queue)에 주문서를 붙인다. 요리사(Worker)는 메모판에서 주문서를 하나씩 꺼내 요리(작업 처리)한다. 손님은 주방이 바빠도 기다리지 않아도 된다.

원리: SQS Queue의 내부 동작 — 왜 이렇게 설계되었는가

SQS는 메시지를 수신하면 여러 서버에 중복 저장한다 (고가용성). 이 구조 때문에 “같은 메시지가 두 번 전달될 수 있다”(at-least-once)는 특성이 생긴다. 이는 버그가 아니라 의도된 트레이드오프다 — 고가용성을 위해 중복 가능성을 감수하는 선택.

왜 이렇게 설계되었는가 — CAP 이론 관점

SQS Standard Queue는 CAP 이론에서 CP(일관성+분할 내성)보다 AP(가용성+분할 내성)를 선택했다. 네트워크 분리 상황에서도 메시지를 잃지 않고(가용성), 중복을 감수(일관성 완화)하는 방향이다. FIFO Queue는 이와 반대로 중복을 없애는 대신(exactly-once) 처리량(초당 300건)을 희생했다.

Standard Queue: 고가용성, 무제한 처리량, 중복 가능
→ 이메일/알림처럼 중복이 허용되거나, 멱등성으로 방어 가능한 경우
FIFO Queue: 순서 보장, 중복 없음, 처리량 제한
→ 결제/재고처럼 순서와 정확히 한 번 처리가 중요한 경우
선택 원칙: "멱등성 구현이 쉬우면 Standard, 비즈니스 로직으로 중복을 다루기 어려우면 FIFO"

Lambda + SQS 조합의 Visibility Timeout 계산법 (2025 권장)

Lambda와 SQS를 연동할 때 흔한 실수: Lambda 함수 타임아웃이 30초인데 SQS Visibility Timeout을 기본값 30초로 유지하면, Lambda 실행 시간이 길어질 때 같은 메시지를 두 Lambda 인스턴스가 동시에 처리하게 된다.

AWS 공식 권장 공식:
SQS Visibility Timeout = Lambda 함수 타임아웃 × 6
예시:
Lambda 타임아웃: 5분(300초)
권장 Visibility Timeout: 300 × 6 = 1800초(30분)
이유: Lambda가 배치 처리 중 스로틀링되더라도
이전 배치가 완료될 충분한 시간을 확보하기 위함
1. Producer(API 서버)가 SQS에 메시지 전송
→ SQS가 메시지를 여러 서버에 중복 저장 (고가용성 보장)
2. SQS가 메시지를 내부 저장소에 보관 (최대 14일)
3. Worker가 ReceiveMessage API로 메시지를 꺼냄
→ 메시지가 Visibility Timeout 동안 다른 Worker에게 보이지 않음
→ (중복 처리 방지 목적, 동시에 여러 Worker가 같은 메시지를 처리하는 것을 막음)
4. Worker가 처리 완료 후 DeleteMessage API로 메시지 삭제
→ 삭제해야만 완전히 제거됨
5. Visibility Timeout 내에 삭제 안 되면 → 메시지가 다시 Queue에 노출됨
(Worker 프로세스가 죽었거나 처리 시간 초과 → 자동 재시도)
6. 처리 실패가 maxReceiveCount(기본 10번) 초과하면 → DLQ로 이동

📖 더 보기: Amazon SQS Visibility Timeout - AWS 공식 문서 — 위 3~6단계 흐름과 ChangeMessageVisibility API 활용법 상세 설명


동기(Sync) vs 비동기(Async)

  • 동기: 요청 → 처리 완료까지 대기 → 응답. 사용자가 기다려야 함.
  • 비동기: 요청 → “접수했다” 즉시 응답 → 백그라운드에서 처리. 사용자는 안 기다림.

Message Queue 전체 흐름

[사용자 요청] → [API 서버] → [Queue에 메시지 넣기] → 즉시 200 응답 반환
[Worker가 폴링(주기적 확인)]
[메시지 꺼내서 처리]
성공: DeleteMessage → 메시지 삭제
실패: Visibility Timeout 후 재노출 → 재시도
N번 실패: DLQ로 이동 → 운영팀 알림

Standard Queue vs FIFO Queue 비교

SQS에는 두 가지 큐 유형이 있다. 실무에서 어떤 것을 선택할지 이해해야 한다.

항목Standard QueueFIFO Queue
순서 보장보장 안 됨 (best-effort)엄격하게 보장
중복at-least-once (중복 가능)exactly-once (중복 없음)
처리량무제한초당 최대 300건 (배치 시 3,000건)
가격저렴약 2배 비쌈
사용 사례이메일 발송, 이미지 처리결제 처리, 주문 순서 보장
Queue URL.amazonaws.com/queue-name.amazonaws.com/queue-name.fifo

실무 선택 기준: 순서가 중요하지 않고 처리량이 많으면 Standard, 결제/금융처럼 순서와 중복이 치명적이면 FIFO 선택.


SQS + NestJS Worker 구현 예시

sqs.module.ts
// npm install @ssut/nestjs-sqs @aws-sdk/client-sqs
import { SqsModule } from "@ssut/nestjs-sqs";
@Module({
imports: [
SqsModule.register({
consumers: [
{
name: "email-queue",
queueUrl:
"https://sqs.ap-northeast-2.amazonaws.com/123456/email-queue",
region: "ap-northeast-2",
},
],
producers: [
{
name: "email-queue",
queueUrl:
"https://sqs.ap-northeast-2.amazonaws.com/123456/email-queue",
region: "ap-northeast-2",
},
],
}),
],
})
export class AppModule {}
// email.consumer.ts — Worker 역할
import { SqsMessageHandler } from "@ssut/nestjs-sqs";
import { Message } from "@aws-sdk/client-sqs";
@Injectable()
export class EmailConsumer {
@SqsMessageHandler("email-queue", false)
async handleEmailJob(message: Message) {
const payload = JSON.parse(message.Body!);
// payload: { userId: '123', type: 'welcome', to: 'user@example.com' }
console.log(`이메일 발송 시작: ${payload.to}`);
await this.emailService.send(payload);
console.log(`이메일 발송 완료: ${payload.to}`);
// 성공 시 라이브러리가 자동으로 DeleteMessage 호출
}
}
// order.service.ts — Producer 역할
import { SqsService } from "@ssut/nestjs-sqs";
@Injectable()
export class OrderService {
constructor(private sqsService: SqsService) {}
async createOrder(dto: CreateOrderDto) {
const order = await this.orderRepository.save(dto);
// Queue에 메시지 전송
await this.sqsService.send("email-queue", {
id: `order-${order.id}-email`,
body: JSON.stringify({
userId: order.userId,
type: "order_created",
to: order.email,
}),
});
return order; // 이메일 발송을 기다리지 않고 즉시 응답
}
}

대표적인 Queue 서비스

  • SQS (AWS Simple Queue Service): AWS 관리형. 가장 쉽게 시작. 서버리스.
  • Bull (Redis 기반): Redis를 Queue로 활용. NestJS @nestjs/bull 패키지로 쉽게 연동.
  • RabbitMQ: 오픈소스. 유연한 라우팅. 복잡한 메시지 패턴에 적합.
  • Kafka: 대용량 스트리밍. 이벤트 기반 아키텍처, 순서 보장, 재처리 가능.

Dead Letter Queue (DLQ)

처리에 반복 실패한 메시지를 보내는 별도 Queue. “이 작업은 몇 번 시도해도 안 되니 나중에 사람이 확인해라”라는 뜻.

설정 방법 (SQS 콘솔):
1. SQS → Queues → email-queue → Edit
2. "Dead-letter queue" 섹션 활성화
3. DLQ ARN 선택 (미리 email-dlq 큐 생성 필요)
4. Maximum receives: 3 (3번 실패 시 DLQ로 이동)
5. Save

DLQ 메시지가 쌓이면 CloudWatch Alarm으로 알림 설정 권장:

Metric: ApproximateNumberOfMessagesVisible (email-dlq 기준)
조건: > 0 이면 알람

DLQ에 쌓인 메시지를 원본 큐로 다시 보내는 기능(Redrive):

Terminal window
# AWS 콘솔: SQS → email-dlq → Start DLQ Redrive
# 원인을 해결한 뒤 실행해야 함. 그렇지 않으면 다시 DLQ로 이동

Visibility Timeout

Worker가 메시지를 꺼내면, 다른 Worker가 같은 메시지를 가져가지 않도록 일정 시간 동안 숨김.

  • 기본값: 30초. 최대 12시간.
  • 처리 시간이 30초를 넘을 것 같으면 ChangeMessageVisibility API로 연장 필요
  • 너무 짧으면 → 처리 중에 다른 Worker가 같은 메시지를 가져가 중복 처리 발생

⚠️ At-least-once: 중복 처리 가능성

SQS를 포함한 대부분의 Queue는 “최소 1회 전달”을 보장한다.

네트워크 문제 등으로 같은 메시지가 두 번 전달될 수 있다.

따라서 Worker 로직은 같은 메시지를 두 번 처리해도 결과가 같도록(멱등성) 설계해야 한다.

→ 멱등성 설계 방법은 Retry/Backoff/Idempotency 문서 참고

패턴 1: Producer-Consumer 분리 배포 (독립 스케일링)

SQS의 핵심 장점 중 하나는 Producer와 Consumer를 별도 서비스로 배포해 독립적으로 스케일링할 수 있다는 점이다.

프로덕션 아키텍처 예시:
API 서버 (ECS Service, 최소 2대) # 요청을 받고 Queue에 넣는 역할
↓ [SQS email-queue]
Email Worker (ECS Service, 1~10대 오토스케일) # 실제 처리 담당
스케일링 트리거:
- email-queue의 ApproximateNumberOfMessagesVisible > 100 → Worker ECS 태스크 +2 증가
- email-queue의 ApproximateNumberOfMessagesVisible < 10 → Worker ECS 태스크 감소
장점:
- 이메일 발송이 느려도 API 서버에 영향 없음
- 트래픽 급증 시 Worker만 스케일아웃 → 비용 효율

패턴 2: NestJS BullMQ로 로컬 환경 Queue 구현

AWS SQS는 로컬 개발 시 불편하다. 로컬에서는 Redis 기반 BullMQ를 쓰고, 프로덕션에서 SQS로 전환하는 패턴이 일반적이다.

bull.module.ts
// npm install @nestjs/bullmq bullmq
import { BullModule } from "@nestjs/bullmq";
@Module({
imports: [
BullModule.forRoot({
connection: {
host: process.env.REDIS_HOST || "localhost",
port: 6379,
},
}),
BullModule.registerQueue({ name: "email" }),
],
})
export class AppModule {}
// email.producer.ts
import { InjectQueue } from "@nestjs/bullmq";
import { Queue } from "bullmq";
@Injectable()
export class EmailProducer {
constructor(@InjectQueue("email") private emailQueue: Queue) {}
async sendWelcomeEmail(userId: string, email: string) {
await this.emailQueue.add("welcome", { userId, email });
// 출력: Job { id: '1', name: 'welcome', data: { userId, email } } 추가됨
}
}
// email.consumer.ts
import { Processor, WorkerHost } from "@nestjs/bullmq";
import { Job } from "bullmq";
@Processor("email")
export class EmailConsumer extends WorkerHost {
async process(job: Job<{ userId: string; email: string }>) {
console.log(`이메일 처리 중: ${job.data.email}`);
await this.emailService.send(job.data);
console.log(`이메일 발송 완료: jobId=${job.id}`);
}
}

패턴 2-1: SQS Consumer 에러 이벤트 핸들러 (2025 권장)

@ssut/nestjs-sqs는 처리 중 예외가 발생했을 때 @SqsConsumerEventHandler로 에러를 캐치할 수 있다. 이를 활용하면 에러마다 CloudWatch 로그를 남기고 알림을 보낼 수 있다.

import { SqsConsumerEventHandler, SqsMessageHandler } from "@ssut/nestjs-sqs";
@Injectable()
export class EmailConsumer {
constructor(private readonly logger: Logger) {}
@SqsMessageHandler("email-queue", false)
async handleMessage(message: Message) {
const payload = JSON.parse(message.Body!);
await this.emailService.send(payload);
}
// 처리 실패 시 자동 호출 (DLQ로 이동 전 마지막 훅)
@SqsConsumerEventHandler("email-queue", "processing_error")
onProcessingError(error: Error, message: Message) {
this.logger.error(
`[email-queue] 메시지 처리 실패`,
JSON.stringify({
error: error.message,
messageId: message.MessageId,
body: message.Body,
}),
);
// CloudWatch에 에러 지표 전송하거나 Slack 알림 트리거 가능
}
}

📖 더 보기: How queues work, implementing AWS SQS with NestJS - Paktolus Engineering — Producer-Consumer 분리 설계와 SQS 에러 핸들링 실전 패턴 (중급)


패턴 3: Long Polling으로 폴링 비용 절감

SQS Worker가 메시지를 계속 폴링하면 빈 응답에도 API 호출 비용이 발생한다. Long Polling을 사용하면 메시지가 생길 때까지 최대 20초 대기 → 폴링 횟수 감소 → 비용 절감.

// @ssut/nestjs-sqs에서 Long Polling 설정
SqsModule.register({
consumers: [
{
name: "email-queue",
queueUrl: process.env.SQS_EMAIL_QUEUE_URL,
region: "ap-northeast-2",
waitTimeSeconds: 20, // Long Polling: 메시지 없으면 최대 20초 대기
// 기본값 0 (Short Polling) → 매번 즉시 응답, 빈 응답에도 과금
},
],
})
// AWS CLI로 Queue 기본값 설정 (콘솔에서도 가능)
aws sqs set-queue-attributes \
--queue-url https://sqs.ap-northeast-2.amazonaws.com/123456/email-queue \
--attributes ReceiveMessageWaitTimeSeconds=20
  • 이메일/알림 발송 (비동기)
  • 이미지/비디오 처리 (리사이즈, 변환)
  • 데이터 동기화 (외부 시스템과 연동)
  • 로그 수집/처리
  • 배치 작업 분배
  • 서비스에서 비동기로 처리되는 작업이 있다면 Queue 구조 이해 필요
  • “작업이 처리가 안 된다” 이슈 시 Queue 상태 확인 (메시지 적체 여부)
  • DLQ에 메시지가 쌓이면 처리 실패 원인 조사
  • Worker 프로세스의 상태/로그 확인
개념 A개념 B차이점
동기 처리비동기 처리동기는 완료까지 대기, 비동기는 즉시 응답 후 백그라운드 처리
SQSKafkaSQS는 단순 메시지 큐, Kafka는 이벤트 스트리밍(순서 보장, 재처리 가능)
QueuePub/SubQueue는 1:1(하나의 Consumer가 처리), Pub/Sub은 1:N(여러 Subscriber)
DLQRetryRetry는 자동 재시도, DLQ는 재시도 다 실패 후 격리
Standard QueueFIFO QueueStandard는 순서 미보장/무제한 처리량, FIFO는 순서 보장/처리량 제한
Short PollingLong PollingShort는 즉시 응답(빈 응답도), Long은 최대 20초 대기 후 응답 (비용 절감)

🔧 Queue에 메시지가 쌓이는데 Worker가 처리를 안 한다

섹션 제목: “🔧 Queue에 메시지가 쌓이는데 Worker가 처리를 안 한다”

증상: SQS 콘솔에서 ApproximateNumberOfMessagesVisible 숫자가 계속 올라감 원인 1: Worker 프로세스(ECS 태스크 등)가 종료되어 있거나 에러 상태 원인 2: Worker의 Visibility Timeout이 처리 시간보다 짧아 메시지가 계속 재노출됨 원인 3: Worker가 실행 중이지만 메시지 처리 중 예외가 발생해서 DeleteMessage를 못 하는 상황 해결:

  1. ECS 콘솔에서 Worker 서비스 태스크 상태 확인 (RUNNING vs STOPPED)
  2. Worker CloudWatch 로그에서 에러 확인
  3. SQS → Queue → ApproximateNumberOfMessagesNotVisible 확인 (처리 중인 메시지 수)
  4. Visibility Timeout을 실제 처리 시간의 2배 이상으로 늘려 설정

증상: email-dlq에 메시지가 수백 건 쌓여 알람 발동 원인: 특정 형식의 메시지를 Worker가 파싱하지 못하거나, 외부 서비스(이메일 API 등) 연결 불가 해결:

  1. DLQ에서 메시지 하나를 Send and receive messages 기능으로 내용 확인
  2. 메시지 Body를 로컬에서 파싱해서 어떤 데이터가 문제인지 확인
  3. 외부 API 오류라면 원인 해결 후 DLQ 메시지를 원본 큐로 Redrive(재전송)
  4. 파싱 버그라면 코드 수정 후 배포, DLQ 메시지 Redrive

🔧 같은 작업이 두 번 처리된다 (중복 처리)

섹션 제목: “🔧 같은 작업이 두 번 처리된다 (중복 처리)”

증상: 이메일이 한 사용자에게 2번 발송됨 원인: Visibility Timeout 내에 처리를 완료했지만 DeleteMessage가 실패했거나, 네트워크 지연으로 at-least-once 전달 해결:

  1. DB에 처리 완료 기록 테이블 추가 (processed_messages: message_id, processed_at)
  2. Worker 시작 시 해당 message_id가 이미 처리됐는지 확인
  3. 이미 처리됐으면 스킵, 아니면 처리 후 기록 저장
  4. 또는 SQS FIFO Queue로 전환 (exactly-once 처리 보장, 단 처리량 제한 있음)

🔧 Worker가 메시지를 처리하다 중간에 죽어서 메시지가 유실됐다

섹션 제목: “🔧 Worker가 메시지를 처리하다 중간에 죽어서 메시지가 유실됐다”

증상: 이메일 발송 요청이 Queue에서 사라졌는데 실제로 발송이 안 됨 원인: Worker가 메시지를 ReceiveMessage로 꺼낸 후 처리 도중 프로세스가 죽었고, Visibility Timeout이 짧아서 Queue에 돌아왔지만 maxReceiveCount를 초과해 DLQ로 이동 해결:

  1. 먼저 DLQ 확인 — 처리되지 않은 메시지가 DLQ에 쌓여있을 가능성 높음
  2. Visibility Timeout을 처리 예상 시간의 3배 이상으로 설정 (기본 30초 → 처리 시간에 맞게 조정)
  3. Worker ECS 서비스의 최소 태스크 수를 2 이상으로 설정 (단일 장애점 방지)
  4. DLQ에 메시지 있으면 원인 파악 후 Redrive로 원본 큐에 재투입

🔧 NestJS SqsModule에서 Cannot find module '@ssut/nestjs-sqs' 에러

섹션 제목: “🔧 NestJS SqsModule에서 Cannot find module '@ssut/nestjs-sqs' 에러”

증상: @ssut/nestjs-sqs 설치 후에도 모듈을 찾을 수 없다는 에러 발생 원인: @ssut/nestjs-sqs는 AWS SDK v2 기반 패키지이므로 AWS SDK v3만 설치되어 있으면 peer dependency 불일치 발생 해결:

  1. aws-sdk (v2) 설치 확인:
    Terminal window
    npm install aws-sdk @ssut/nestjs-sqs
  2. 또는 AWS SDK v3 호환 패키지로 교체:
    Terminal window
    # AWS SDK v3 기반 대안
    npm install @nestjs-packages/sqs @aws-sdk/client-sqs
  3. package.json에서 aws-sdk 버전이 ^2.x.x인지 확인

🔧 고부하 상황에서 메시지가 처리 전에 만료된다

섹션 제목: “🔧 고부하 상황에서 메시지가 처리 전에 만료된다”

증상: SQS에 메시지가 많이 쌓였을 때 일부 메시지가 처리되기 전에 사라짐 원인: SQS 메시지 보존 기간(MessageRetentionPeriod) 기본값은 4일이다. 처리가 밀리는 상황에서 4일 이상 지나면 메시지가 자동 삭제된다. 또는 DLQ의 maxReceiveCount가 너무 낮게 설정돼 정상 재시도 중에 DLQ로 이동하는 경우도 있다. 해결:

  1. 메시지 보존 기간 확인 및 연장 (최대 14일):
    Terminal window
    aws sqs set-queue-attributes \
    --queue-url https://sqs.ap-northeast-2.amazonaws.com/123456/my-queue \
    --attributes MessageRetentionPeriod=1209600 # 14일 (초 단위)
  2. DLQ의 maxReceiveCount를 최소 5 이상으로 설정 (AWS 권장값)
  3. Lambda + SQS 조합이라면 Visibility Timeout = Lambda 타임아웃 × 6 공식 적용

🔧 Visibility Timeout이 짧아서 메시지가 중복 처리된다

섹션 제목: “🔧 Visibility Timeout이 짧아서 메시지가 중복 처리된다”

증상: 동일한 메시지가 두 개의 Worker에서 동시에 처리되거나, 처리 완료 전에 큐에 다시 노출됨 원인: Visibility Timeout을 실제 처리 시간보다 짧게 설정했기 때문이다. Worker가 메시지를 가져간 후 Visibility Timeout 내에 DeleteMessage를 호출하지 못하면, 해당 메시지가 큐에 다시 보여서 다른 Worker가 가져간다 해결:

  1. 실제 처리 시간 측정 후 여유 있게 설정 (AWS 권장: 실제 처리 시간 × 6):

    Terminal window
    # 현재 Visibility Timeout 확인
    aws sqs get-queue-attributes \
    --queue-url https://sqs.ap-northeast-2.amazonaws.com/123456/my-queue \
    --attribute-names VisibilityTimeout \
    --region ap-northeast-2
    # Visibility Timeout 변경 (예: 300초 = 5분)
    aws sqs set-queue-attributes \
    --queue-url https://sqs.ap-northeast-2.amazonaws.com/123456/my-queue \
    --attributes VisibilityTimeout=300 \
    --region ap-northeast-2
  2. 처리 시간이 가변적인 경우 ChangeMessageVisibility로 동적 연장:

    @SqsMessageHandler('my-queue', false)
    async handleMessage(message: Message) {
    // 처리 시작 시 Visibility Timeout 연장 (처리 중 만료 방지)
    await this.sqsClient.send(new ChangeMessageVisibilityCommand({
    QueueUrl: process.env.QUEUE_URL,
    ReceiptHandle: message.ReceiptHandle!,
    VisibilityTimeout: 300, // 5분 추가
    }));
    await this.doHeavyWork(message);
    // 처리 완료 후 DeleteMessage는 @ssut/nestjs-sqs가 자동 처리
    }
  3. Visibility Timeout 설정 원칙:

    • 최소값: 예상 처리 시간 × 3 (안전 마진)
    • 최대값: 12시간 (SQS 제한)
    • Worker 타임아웃과 함께 설계 (Worker 타임아웃 < Visibility Timeout이어야 함)
  • 동기와 비동기 처리의 차이를 설명할 수 있다
  • “왜 이 작업에 Queue를 쓰는지” 설명할 수 있다
  • Producer → Queue → Consumer 흐름을 그릴 수 있다
  • DLQ가 뭔지, 왜 필요한지 설명할 수 있다
  • At-least-once 전달 방식이 뭔지, 그래서 Worker 로직에 멱등성이 필요한 이유를 설명할 수 있다
  • Standard Queue vs FIFO Queue의 차이와 선택 기준을 설명할 수 있다

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

  • 팀 서비스에서 Queue를 사용하는 부분이 있는지 확인
    Terminal window
    grep -r "SqsModule\|BullModule\|RabbitMQ\|@SqsMessageHandler" src/
    예상 출력: Queue 사용 중이면 해당 파일 경로와 줄 번호 표시
  • AWS SQS 콘솔에서 Queue 목록과 메시지 수 확인 경로: SQS → Queues → 각 Queue의 Messages Available 숫자 확인
  • DLQ가 설정되어 있는지, 메시지가 쌓여있는지 확인 경로: SQS → 해당 Queue → Dead-letter queue 탭 확인: ApproximateNumberOfMessagesVisible > 0 이면 처리 실패 발생 중
  • Worker 프로세스의 로그를 확인해서 처리 흐름 파악 경로: CloudWatch → Log groups → /ecs/<worker-서비스명>
  • 사용 중인 Queue가 Standard인지 FIFO인지 확인 경로: SQS → Queue URL.fifo로 끝나면 FIFO, 없으면 Standard
  1. Queue는 작업 대기열, Worker는 대기열에서 작업을 꺼내 처리하는 프로세스이다
  2. “지금 당장 안 해도 되는 작업”은 비동기 처리하면 서버 부하가 줄어든다
  3. Producer → Queue → Consumer 패턴이 비동기 처리의 기본이다
  4. DLQ로 실패한 메시지를 격리해서 시스템 안정성을 확보한다
  5. 대부분의 서비스에 비동기 처리 구간이 있으므로, 이 패턴을 알아야 전체 흐름이 보인다

11. 실전 장애 대응 시나리오 (On-Call Runbook)

섹션 제목: “11. 실전 장애 대응 시나리오 (On-Call Runbook)”

Queue/Worker 구조에서 on-call 시 가장 자주 마주치는 시나리오

시나리오 A: “Queue에 메시지가 쌓이고 처리가 안 된다”

섹션 제목: “시나리오 A: “Queue에 메시지가 쌓이고 처리가 안 된다””
발생 빈도: 매우 높음 (가장 흔한 Queue 장애 패턴)
즉각 확인 (5분 이내):
1. ApproximateNumberOfMessagesVisible (처리 대기 메시지 수)
SQS → 해당 Queue → Monitoring 탭
→ 계속 올라가면 Worker가 처리 못하는 것
2. ApproximateNumberOfMessagesNotVisible (처리 중인 메시지 수)
→ 이 숫자가 높으면 Worker가 처리 중이지만 완료/실패 처리를 못하는 것
→ Visibility Timeout이 너무 짧거나 Worker가 응답 없는 상태
3. Worker ECS 태스크 상태 확인
ECS → 서비스 → Tasks → 모든 태스크가 RUNNING인가
→ STOPPED 태스크 있으면 로그 확인:
CloudWatch → /ecs/[worker-service] → 가장 최근 스트림
4. 원인별 조치:
Worker 죽음: 태스크 재시작 또는 서비스 Update 실행
처리 속도 부족: Worker ECS 서비스 desired count 증가 (임시 스케일아웃)
특정 메시지 파싱 실패: DLQ 확인 후 문제 메시지 격리

시나리오 B: “DLQ에 수백 건이 쌓여있다”는 알람을 받았을 때

섹션 제목: “시나리오 B: “DLQ에 수백 건이 쌓여있다”는 알람을 받았을 때”
이 알람의 의미: "여러 번 시도했지만 처리 실패한 메시지가 있음"
Step 1. DLQ 메시지 샘플 확인 (원인 파악)
SQS → DLQ Queue → Send and receive messages → Receive message
→ Body 내용 확인: 어떤 종류의 메시지인가?
→ ApproximateReceiveCount: 몇 번 시도됐는가?
Step 2. 실패 원인 분류
A) 파싱/검증 오류 (메시지 자체 문제)
→ 코드 수정 없이는 해결 불가
→ 수정 후 배포 → DLQ Redrive
B) 외부 서비스 오류 (이메일 API down, DB 연결 실패 등)
→ 외부 서비스 복구 확인
→ 복구 후 DLQ Redrive
C) 비즈니스 로직 오류 (존재하지 않는 userId 등)
→ 데이터 정합성 확인
→ 수동 처리 또는 스킵 결정
Step 3. DLQ Redrive (원본 큐로 재전송)
정상적인 Redrive:
SQS → DLQ → Start DLQ Redrive
→ Source queue 선택
→ 속도 제한: "Redrive up to N messages per second" 설정 권장
주의: 원인 해결 전에 Redrive하면 다시 DLQ로 돌아옴
Step 4. 재발 방지
DLQ CloudWatch Alarm 확인/강화:
Metric: ApproximateNumberOfMessagesVisible (DLQ)
조건: > 0 이면 즉시 알람 (현재 없다면 추가)

시나리오 C: “같은 이메일이 2번 발송됐다”는 민원

섹션 제목: “시나리오 C: “같은 이메일이 2번 발송됐다”는 민원”
원인: At-least-once 전달로 인한 중복 처리
즉각 조치:
1. 영향받은 사용자 파악 → 사과 안내
2. 동일한 메시지 ID의 중복 처리 기록이 있는지 DB 확인
근본 원인 분석:
→ Visibility Timeout이 이메일 발송 시간보다 짧았는지 확인
SQS → Queue → Visibility Timeout 값 확인
실제 처리 시간: CloudWatch → Worker 로그에서 처리 소요 시간 확인
→ 처리 시간 > Visibility Timeout이면 타임아웃이 원인
→ Worker가 DeleteMessage 전에 종료됐는지 확인
Worker 로그에서 "Graceful Shutdown" 로그 패턴 확인
재발 방지:
1. Visibility Timeout = 처리 예상 시간 × 3 으로 설정
2. 처리 완료 기록 테이블 추가 (message_id + processed_at)
3. FIFO Queue로 전환 검토 (exactly-once 보장, 단 처리량 제한 있음)

DLQ Retention 14일 원칙

2025년 프로덕션 베스트 프랙티스로 DLQ의 메시지 보존 기간은 항상 최대값인 14일로 설정하는 것이 권장된다. 원본 큐와 동일한 보존 기간으로 설정하면 DLQ에 도달하기 전에 메시지가 만료되는 경우가 생긴다. DLQ는 “나중에 분석하기 위해 보관”하는 목적이므로 가장 긴 보존 기간이 맞다.

SQS Lambda Trigger 시 Short Polling 자동 Long Polling 전환

Lambda를 SQS Trigger로 연결하는 경우, AWS는 내부적으로 Long Polling(최대 20초)을 자동으로 사용한다. NestJS Worker처럼 직접 폴링하는 경우에는 명시적으로 waitTimeSeconds: 20을 설정해야 한다.

BullMQ v5 (2024~2025 주요 업데이트)

@nestjs/bullmq의 기반인 BullMQ v5가 2024년에 출시됐다. 주요 변경 사항:

  • Worker 인스턴스의 concurrency 기본값이 1에서 설정 가능하게 변경
  • Job TTL 설정으로 오래된 완료 작업 자동 정리 기능 강화
  • removeOnComplete, removeOnFail 옵션으로 Redis 메모리 관리 개선
// BullMQ v5 권장 설정 예시
await this.emailQueue.add(
"welcome",
{ userId, email },
{
removeOnComplete: { count: 1000 }, // 최근 1000건만 보관
removeOnFail: { count: 5000 }, // 실패 기록 5000건 보관
attempts: 3, // 최대 3번 재시도
backoff: { type: "exponential", delay: 1000 }, // 지수 백오프
},
);

📖 더 보기: The Complete SQS Troubleshooting Guide — SQS에서 가장 자주 발생하는 장애 패턴과 해결법 총정리 (중급)