Queue / Worker Basics
분류: Layer 6 - 운영 심화: 관측성 & 복원력
1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”Queue는 처리할 작업을 대기열에 넣는 것이고, Worker는 그 대기열에서 작업을 꺼내 처리하는 프로세스이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”모든 요청을 즉시 처리하면 서버가 과부하에 걸린다. 이메일 발송, 이미지 처리, 알림 전송 같은 “지금 당장 안 해도 되는 작업”은 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가 등장한 이유”Queue/Worker 이전의 단순한 방식은 API 요청 안에서 모든 일을 끝내는 것이다. 회원가입 API가 DB 저장, 이메일 발송, 이미지 리사이즈, 외부 CRM 동기화를 모두 동기로 처리하면 가장 느린 외부 호출이 사용자 응답 시간을 결정한다. await emailApi.send()가 2초 걸리거나 타임아웃 5초를 채우면, DB 저장은 이미 성공했는데 사용자는 실패 응답을 받는 불일치도 생긴다. 같은 문제를 서버 안의 setTimeout, in-process job, 브라우저 Web Worker처럼 “프로세스 내부 비동기”로 밀어도 프로세스가 죽으면 작업 상태가 함께 사라지고, API 서버와 작업 처리량을 독립적으로 늘릴 수 없다.
Queue/Worker는 이 한계를 내구성 있는 버퍼로 푼다. Producer(API 서버)는 “작업을 접수했다”는 사실만 Queue에 남기고 빠르게 응답하며, Worker는 Queue에서 작업을 꺼내 별도 속도로 처리한다. AWS SQS Standard Queue는 메시지를 여러 Availability Zone에 중복 저장해 단일 서버/AZ 장애로 메시지가 사라지지 않도록 설계되어 있고, 메시지 보존 기간은 기본 4일·최대 14일까지 설정할 수 있다. 대신 Standard Queue는 순서와 정확히 한 번 처리를 포기하고 at-least-once 전달을 선택하므로 Worker는 멱등성을 가져야 한다. 즉 이 토픽이 사라지면 “응답 흐름”과 “느린 작업 흐름”을 분리하는 판단 기준이 사라지고, 부하가 몰릴 때 API 서버 증설·재시도·장애 복구를 한 덩어리로 다루게 된다.
출처: Amazon SQS queue types - AWS 공식 문서, SQS queue parameters - AWS 공식 문서
3. 핵심 개념
섹션 제목: “3. 핵심 개념”Queue/Worker 동작 방식 (전체 흐름)
섹션 제목: “Queue/Worker 동작 방식 (전체 흐름)”비유: 식당 주문 접수와 같다. 손님(API 요청)이 주문하면 종업원(API 서버)이 “접수했습니다” 라고 즉시 응답하고, 주방 메모판(Queue)에 주문서를 붙인다. 요리사(Worker)는 메모판에서 주문서를 하나씩 꺼내 요리(작업 처리)한다. 손님은 주방이 바빠도 기다리지 않아도 된다.
원리: SQS Queue의 내부 동작 — 왜 이렇게 설계되었는가
SQS는 메시지를 수신하면 여러 서버에 중복 저장한다 (고가용성). 이 구조 때문에 “같은 메시지가 두 번 전달될 수 있다”(at-least-once)는 특성이 생긴다. 이는 버그가 아니라 의도된 트레이드오프다 — 고가용성을 위해 중복 가능성을 감수하는 선택.
왜 이렇게 설계되었는가 — 가용성 우선 트레이드오프
SQS Standard Queue는 메시지를 여러 AZ에 중복 저장하고, 네트워크 지연이나 Worker 장애가 있어도 메시지를 다시 노출해 처리 기회를 보장한다. 이 선택의 대가는 중복 전달과 best-effort ordering이다. FIFO Queue는 MessageGroupId 안의 순서를 보장하고 deduplication ID로 중복을 줄이는 대신, 기본 처리량은 API 작업별 초당 300회(배치 10개 사용 시 초당 3,000개 메시지)로 제한된다. 고처리량 FIFO 모드를 켜면 더 늘릴 수 있지만, 순서 보장은 여전히 message group 설계에 묶인다.
Standard Queue: 고가용성, 무제한 처리량, 중복 가능 → 이메일/알림처럼 중복이 허용되거나, 멱등성으로 방어 가능한 경우
FIFO Queue: message group 안의 순서 보장, deduplication ID 기준 중복 억제, 처리량 제한 → 결제/재고처럼 순서와 정확히 한 번 처리가 중요한 경우
선택 원칙: "멱등성 구현이 쉬우면 Standard, 비즈니스 로직으로 중복을 다루기 어려우면 FIFO"Lambda + SQS 조합의 Visibility Timeout 계산법
Lambda와 SQS를 연동할 때 흔한 실수: Lambda 함수 타임아웃이 30초인데 SQS Visibility Timeout을 기본값 30초로 유지하면, Lambda 실행 시간이 길어질 때 같은 메시지를 두 Lambda 인스턴스가 동시에 처리하게 된다.
AWS 공식 권장 공식:SQS Visibility Timeout >= Lambda 함수 타임아웃 × 6SQS 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가 배치 처리 중 스로틀링되더라도 이전 배치가 완료될 충분한 시간을 확보하기 위함출처: Creating and configuring an Amazon SQS event source mapping - AWS 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 Queue | FIFO Queue |
|---|---|---|
| 순서 보장 | 보장 안 됨 (best-effort) | 엄격하게 보장 |
| 중복 | at-least-once (중복 가능) | deduplication ID 기준 중복 억제 |
| 처리량 | 거의 무제한 | API 작업별 300 TPS, 배치 시 3,000 msg/s |
| 가격 | 저렴 | 약 2배 비쌈 |
| 사용 사례 | 이메일 발송, 이미지 처리 | 결제 처리, 주문 순서 보장 |
| Queue URL | .amazonaws.com/queue-name | .amazonaws.com/queue-name.fifo |
실무 선택 기준: 순서가 중요하지 않고 처리량이 많으면 Standard, 결제/금융처럼 같은 사용자·주문 단위의 순서가 치명적이면 FIFO를 선택한다. 단, FIFO에서도 “전체 큐의 전역 순서”가 아니라 MessageGroupId별 순서가 핵심이므로, 모든 메시지를 하나의 group에 넣으면 Worker를 늘려도 병렬성이 거의 나오지 않는다.
출처: Amazon SQS FIFO queue quotas - AWS 공식 문서, Amazon SQS queue types - AWS 공식 문서
SQS + NestJS Worker 구현 예시
// npm install @ssut/nestjs-sqs @aws-sdk/client-sqsimport { 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: '[email protected]' }
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: 대용량 스트리밍. 이벤트 기반 아키텍처, 순서 보장, 재처리 가능.
SQS vs Kafka vs RabbitMQ 심층 선택 기준
세 서비스는 모두 “비동기 처리”를 제공하지만, 설계 철학과 강점이 다르다.
| 항목 | SQS | Kafka | RabbitMQ |
|---|---|---|---|
| 처리량 | 사실상 무제한 (관리형) | 파티션당 수백만 건/s | 수만 건/s (메모리 기반) |
| 메시지 보존 | 최대 14일 (재처리 불가) | 설정한 기간 무제한 (재처리 가능) | 메모리/디스크 (장기 보관 비적합) |
| 이벤트 재생 | 불가 | 가능 (offset reset) | 불가 |
| 라우팅 복잡도 | 단순 (큐 1:1) | 중간 (토픽-파티션) | 복잡 라우팅 지원 (Exchange) |
| 운영 부담 | 없음 (서버리스) | 높음 (브로커 클러스터 관리) | 중간 (자체 호스팅 필요) |
| 주요 사용처 | AWS 통합 단순 큐잉 | 데이터 파이프라인, 이벤트 스트리밍 | 복잡한 메시지 라우팅, 멀티 프로토콜 |
결정 원칙:
- AWS 인프라 기반 + 운영팀 소규모 → SQS (서버리스, 인프라 관리 불필요)
- 데이터 파이프라인 / 이벤트 재처리 필요 / 장기 보관된 이벤트를 다시 읽어야 함 → Kafka
- 복잡한 라우팅 규칙 (토픽별 서로 다른 Consumer) / 멀티 프로토콜 → RabbitMQ
예를 들어 “회원가입 후 환영 이메일”은 사용자 응답에 이메일 발송 성공이 꼭 필요하지 않으므로 SQS에 넣고 API는 200을 먼저 반환해도 된다. 반대로 “카드 승인 결과”처럼 사용자가 즉시 성공/실패를 알아야 하고, 승인 실패 시 주문 생성 자체를 막아야 하는 흐름은 Queue로 숨기면 안 된다. 이 경우 Queue는 장애를 줄이는 도구가 아니라 실패를 늦게 발견하게 만드는 버퍼가 된다.
📖 출처: Apache Kafka vs RabbitMQ vs AWS SNS/SQS 비교 - Ably, Kafka vs RabbitMQ vs SQS - scalewithchintan
Queue가 적합하지 않은 경우 (Anti-pattern)
Queue는 만능이 아니다. 도입 전에 “Queue가 진짜 필요한가?”를 먼저 따져야 한다.
| 상황 | 적합한 대안 | 이유 |
|---|---|---|
| 매일 자정 정산 등 고정 스케줄 | Cron Job | 이벤트 트리거 없이 시간만 중요한 작업 → 큐 불필요 |
| 요청 즉시 응답 필수 (결제 확인) | 직접 API 호출 | 비동기 지연이 UX를 해침 → 동기 처리가 올바름 |
| 단일 서버 내 CPU 집약적 작업 | in-process async | Promise / Worker Thread로 해결 가능, 외부 큐 불필요 |
| 팀 운영 역량이 낮고 작업량 소규모 | Cron Job / BullMQ | SQS 설정·모니터링 오버헤드가 이점을 초과 |
Queue 도입 체크리스트:
- 실패 복구가 필요한가? (Worker 죽어도 작업이 재시도돼야 하는가)
- 작업 규모가 API 요청과 독립적으로 스케일링해야 하는가?
- 운영팀이 Queue 모니터링·DLQ 관리를 할 수 있는가?
→ 셋 다 “예”여야 Queue가 정당화된다. 그렇지 않으면 Cron Job이나 in-process async가 더 단순하고 유지보수하기 쉽다.
판단을 숫자로 바꾸면 더 명확하다. 작업이 사용자 응답의 필수 조건이고 p95 처리 시간이 API 타임아웃 안에 안정적으로 들어온다면 동기 호출이 단순하다. 작업이 외부 API 지연 때문에 p95 응답 시간을 수백 ms 이상 흔들거나, 실패해도 “접수됨” 상태로 나중에 복구할 수 있다면 Queue가 맞다. 단, 예상 적체 시간이 SQS 기본 보존 기간 4일을 넘을 수 있는 배치라면 retention을 최대 14일로 늘리거나 Kafka/DB job table처럼 재처리 가능한 저장소를 검토해야 한다.
📖 출처: Message Queues vs Job Schedulers - jobrunr.io, SQS queue parameters - AWS 공식 문서
Dead Letter Queue (DLQ)
처리에 반복 실패한 메시지를 보내는 별도 Queue. “이 작업은 몇 번 시도해도 안 되니 나중에 사람이 확인해라”라는 뜻.
설정 방법 (SQS 콘솔):1. SQS → Queues → email-queue → Edit2. "Dead-letter queue" 섹션 활성화3. DLQ ARN 선택 (미리 email-dlq 큐 생성 필요)4. Maximum receives: 3 (3번 실패 시 DLQ로 이동)5. SaveDLQ 메시지가 쌓이면 CloudWatch Alarm으로 알림 설정 권장:
Metric: ApproximateNumberOfMessagesVisible (email-dlq 기준)조건: > 0 이면 알람DLQ에 쌓인 메시지를 원본 큐로 다시 보내는 기능(Redrive):
# AWS 콘솔: SQS → email-dlq → Start DLQ Redrive# 원인을 해결한 뒤 실행해야 함. 그렇지 않으면 다시 DLQ로 이동Visibility Timeout
Worker가 메시지를 꺼내면, 다른 Worker가 같은 메시지를 가져가지 않도록 일정 시간 동안 숨김.
- 기본값: 30초. 최대 12시간.
- 처리 시간이 30초를 넘을 것 같으면
ChangeMessageVisibilityAPI로 연장 필요 - 너무 짧으면 → 처리 중에 다른 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로 전환하는 패턴이 일반적이다.
// npm install @nestjs/bullmq bullmqimport { 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.tsimport { 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.tsimport { 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=204. 실무에서 어디에 쓰이나
섹션 제목: “4. 실무에서 어디에 쓰이나”- 이메일/알림 발송 (비동기)
- 이미지/비디오 처리 (리사이즈, 변환)
- 데이터 동기화 (외부 시스템과 연동)
- 로그 수집/처리
- 배치 작업 분배
5. 현재 내 업무와 연결점
섹션 제목: “5. 현재 내 업무와 연결점”- 서비스에서 비동기로 처리되는 작업이 있다면 Queue 구조 이해 필요
- “작업이 처리가 안 된다” 이슈 시 Queue 상태 확인 (메시지 적체 여부)
- DLQ에 메시지가 쌓이면 처리 실패 원인 조사
- Worker 프로세스의 상태/로그 확인
6. 자주 헷갈리는 개념 비교
섹션 제목: “6. 자주 헷갈리는 개념 비교”| 개념 A | 개념 B | 차이점 |
|---|---|---|
| 동기 처리 | 비동기 처리 | 동기는 완료까지 대기, 비동기는 즉시 응답 후 백그라운드 처리 |
| SQS | Kafka | SQS는 단순 메시지 큐, Kafka는 이벤트 스트리밍(순서 보장, 재처리 가능) |
| Queue | Pub/Sub | Queue는 1:1(하나의 Consumer가 처리), Pub/Sub은 1:N(여러 Subscriber) |
| DLQ | Retry | Retry는 자동 재시도, DLQ는 재시도 다 실패 후 격리 |
| Standard Queue | FIFO Queue | Standard는 순서 미보장/무제한 처리량, FIFO는 순서 보장/처리량 제한 |
| Short Polling | Long Polling | Short는 즉시 응답(빈 응답도), Long은 최대 20초 대기 후 응답 (비용 절감) |
6.5. 트러블슈팅
섹션 제목: “6.5. 트러블슈팅”🔧 Queue에 메시지가 쌓이는데 Worker가 처리를 안 한다
섹션 제목: “🔧 Queue에 메시지가 쌓이는데 Worker가 처리를 안 한다”증상: SQS 콘솔에서 ApproximateNumberOfMessagesVisible 숫자가 계속 올라감
원인 1: Worker 프로세스(ECS 태스크 등)가 종료되어 있거나 에러 상태
원인 2: Worker의 Visibility Timeout이 처리 시간보다 짧아 메시지가 계속 재노출됨
원인 3: Worker가 실행 중이지만 메시지 처리 중 예외가 발생해서 DeleteMessage를 못 하는 상황
해결:
- ECS 콘솔에서 Worker 서비스 태스크 상태 확인 (
RUNNINGvsSTOPPED) - Worker CloudWatch 로그에서 에러 확인
- SQS → Queue →
ApproximateNumberOfMessagesNotVisible확인 (처리 중인 메시지 수) - Visibility Timeout을 실제 처리 시간의 2배 이상으로 늘려 설정
🔧 DLQ에 메시지가 계속 쌓인다
섹션 제목: “🔧 DLQ에 메시지가 계속 쌓인다”증상: email-dlq에 메시지가 수백 건 쌓여 알람 발동
원인: 특정 형식의 메시지를 Worker가 파싱하지 못하거나, 외부 서비스(이메일 API 등) 연결 불가
해결:
- DLQ에서 메시지 하나를
Send and receive messages기능으로 내용 확인 - 메시지 Body를 로컬에서 파싱해서 어떤 데이터가 문제인지 확인
- 외부 API 오류라면 원인 해결 후 DLQ 메시지를 원본 큐로
Redrive(재전송) - 파싱 버그라면 코드 수정 후 배포, DLQ 메시지 Redrive
🔧 같은 작업이 두 번 처리된다 (중복 처리)
섹션 제목: “🔧 같은 작업이 두 번 처리된다 (중복 처리)”증상: 이메일이 한 사용자에게 2번 발송됨 원인: Visibility Timeout 내에 처리를 완료했지만 DeleteMessage가 실패했거나, 네트워크 지연으로 at-least-once 전달 해결:
- DB에 처리 완료 기록 테이블 추가 (
processed_messages: message_id, processed_at) - Worker 시작 시 해당 message_id가 이미 처리됐는지 확인
- 이미 처리됐으면 스킵, 아니면 처리 후 기록 저장
- 또는 SQS FIFO Queue로 전환 (exactly-once 처리 보장, 단 처리량 제한 있음)
🔧 Worker가 메시지를 처리하다 중간에 죽어서 메시지가 유실됐다
섹션 제목: “🔧 Worker가 메시지를 처리하다 중간에 죽어서 메시지가 유실됐다”증상: 이메일 발송 요청이 Queue에서 사라졌는데 실제로 발송이 안 됨
원인: Worker가 메시지를 ReceiveMessage로 꺼낸 후 처리 도중 프로세스가 죽었고, Visibility Timeout이 짧아서 Queue에 돌아왔지만 maxReceiveCount를 초과해 DLQ로 이동
해결:
- 먼저 DLQ 확인 — 처리되지 않은 메시지가 DLQ에 쌓여있을 가능성 높음
- Visibility Timeout을 처리 예상 시간의 3배 이상으로 설정 (기본 30초 → 처리 시간에 맞게 조정)
- Worker ECS 서비스의 최소 태스크 수를 2 이상으로 설정 (단일 장애점 방지)
- 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 불일치 발생
해결:
aws-sdk(v2) 설치 확인:Terminal window npm install aws-sdk @ssut/nestjs-sqs- 또는 AWS SDK v3 호환 패키지로 교체:
Terminal window # AWS SDK v3 기반 대안npm install @nestjs-packages/sqs @aws-sdk/client-sqs package.json에서aws-sdk버전이^2.x.x인지 확인
🔧 고부하 상황에서 메시지가 처리 전에 만료된다
섹션 제목: “🔧 고부하 상황에서 메시지가 처리 전에 만료된다”증상: SQS에 메시지가 많이 쌓였을 때 일부 메시지가 처리되기 전에 사라짐 원인: SQS 메시지 보존 기간(MessageRetentionPeriod) 기본값은 4일이다. 처리가 밀리는 상황에서 4일 이상 지나면 메시지가 자동 삭제된다. 또는 DLQ의 maxReceiveCount가 너무 낮게 설정돼 정상 재시도 중에 DLQ로 이동하는 경우도 있다. 해결:
- 메시지 보존 기간 확인 및 연장 (최대 14일):
Terminal window aws sqs set-queue-attributes \--queue-url https://sqs.ap-northeast-2.amazonaws.com/123456/my-queue \--attributes MessageRetentionPeriod=1209600 # 14일 (초 단위) - DLQ의 maxReceiveCount를 최소 5 이상으로 설정 (AWS 권장값)
- Lambda + SQS 조합이라면 Visibility Timeout = Lambda 타임아웃 × 6 공식 적용
🔧 Visibility Timeout이 짧아서 메시지가 중복 처리된다
섹션 제목: “🔧 Visibility Timeout이 짧아서 메시지가 중복 처리된다”증상: 동일한 메시지가 두 개의 Worker에서 동시에 처리되거나, 처리 완료 전에 큐에 다시 노출됨
원인: Visibility Timeout을 실제 처리 시간보다 짧게 설정했기 때문이다. Worker가 메시지를 가져간 후 Visibility Timeout 내에 DeleteMessage를 호출하지 못하면, 해당 메시지가 큐에 다시 보여서 다른 Worker가 가져간다
해결:
-
실제 처리 시간 측정 후 여유 있게 설정 (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 -
처리 시간이 가변적인 경우
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가 자동 처리} -
Visibility Timeout 설정 원칙:
- 최소값: 예상 처리 시간 × 3 (안전 마진)
- 최대값: 12시간 (SQS 제한)
- Worker 타임아웃과 함께 설계 (Worker 타임아웃 < Visibility Timeout이어야 함)
7. 체크리스트
섹션 제목: “7. 체크리스트”- 동기와 비동기 처리의 차이를 설명할 수 있다
- “왜 이 작업에 Queue를 쓰는지” 설명할 수 있다
- Producer → Queue → Consumer 흐름을 그릴 수 있다
- DLQ가 뭔지, 왜 필요한지 설명할 수 있다
- At-least-once 전달 방식이 뭔지, 그래서 Worker 로직에 멱등성이 필요한 이유를 설명할 수 있다
- Standard Queue vs FIFO Queue의 차이와 선택 기준을 설명할 수 있다
7.5. 전이 가능한 사고 모델: “비동기 분리” 원리의 확장
섹션 제목: “7.5. 전이 가능한 사고 모델: “비동기 분리” 원리의 확장”Queue/Worker 패턴의 핵심 원리인 “생산자와 소비자를 분리해 독립적으로 실행” 은 하나의 도메인에 국한되지 않는다.
동일 원리가 작동하는 레이어별 비교
| 레이어 | 생산자(Producer) | 버퍼(Queue) | 소비자(Consumer) |
|---|---|---|---|
| 브라우저 | 메인 스레드 | postMessage() 채널 | Web Worker |
| OS 프로세스 | 부모 프로세스 | IPC 파이프/소켓 | 자식 프로세스 |
| 백엔드 서비스 | API 서버 | SQS / RabbitMQ | Worker 프로세스 |
| 클라우드 이벤트 | Lambda / 마이크로서비스 | Kafka 토픽 | 스트림 소비자 |
패턴 확장: Producer-Consumer → CQRS → Event Sourcing
같은 “비동기 분리” 원리가 아키텍처 패턴으로 확장된다.
[Queue/Worker] Producer가 메시지를 큐에 넣고, Consumer가 독립 처리 ↓ 확장[CQRS] Command(쓰기)와 Query(읽기)를 분리 → Command 큐에 쓰기 요청을 넣고 Handler가 처리 ↓ 확장[Event Sourcing] 모든 상태 변경을 이벤트(메시지)로 저장 → Kafka 같은 이벤트 스트림이 영구 저장소 역할핵심 통찰: Queue Worker를 잘 이해하면 CQRS와 Event Sourcing을 “같은 원리의 다른 스케일”로 읽을 수 있다. 모두 “지금 처리하는 흐름”과 “나중에 처리하는 흐름”을 분리해 시스템 탄력성을 높이는 패턴이다.
📖 출처: Event-Driven Architecture: Message Queues, Event Sourcing, and CQRS - Calmops, CQRS Pattern - Microsoft Azure Architecture Center
8. 추가 학습 키워드
섹션 제목: “8. 추가 학습 키워드”Event-Driven Architecture, FIFO Queue, Message Deduplication, Fan-out Pattern, Backpressure, At-least-once vs Exactly-once, Long Polling, BullMQ, CQRS, Event Sourcing
8.5. 추천 리소스
섹션 제목: “8.5. 추천 리소스”- 📖 Amazon SQS 개발자 가이드 - AWS 공식 문서 — SQS 전반적인 동작 원리, Visibility Timeout, DLQ 설정 공식 레퍼런스 (입문)
- 📖 NestJS Queues 공식 문서 (Bull) — NestJS에서 Bull 큐로 Worker 구현하는 공식 가이드 (입문)
- 📖 NestJS + SQS 통합 가이드 - Medium —
@ssut/nestjs-sqs로 Producer/Consumer 완전 구현 예제 (중급) - 📖 SQS Dead Letter Queue 이해 - AWS re:Post — DLQ 설정, Redrive 방법, 모니터링 전략 (중급)
- 📖 Building Scalable Notification System with SQS + NestJS - Medium — 실제 SaaS 플랫폼에서 알림 시스템 구축한 사례와 설계 결정 (중급)
9. 내가 직접 확인해볼 것
섹션 제목: “9. 내가 직접 확인해볼 것”- 팀 서비스에서 Queue를 사용하는 부분이 있는지 확인
예상 출력: Queue 사용 중이면 해당 파일 경로와 줄 번호 표시
Terminal window grep -r "SqsModule\|BullModule\|RabbitMQ\|@SqsMessageHandler" src/ - 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
10. 5줄 요약
섹션 제목: “10. 5줄 요약”- Queue는 작업 대기열, Worker는 대기열에서 작업을 꺼내 처리하는 프로세스이다
- “지금 당장 안 해도 되는 작업”은 비동기 처리하면 서버 부하가 줄어든다
- Producer → Queue → Consumer 패턴이 비동기 처리의 기본이다
- DLQ로 실패한 메시지를 격리해서 시스템 안정성을 확보한다
- 대부분의 서비스에 비동기 처리 구간이 있으므로, 이 패턴을 알아야 전체 흐름이 보인다
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 보장, 단 처리량 제한 있음)운영 기준: 조용히 실패하는 Queue 설정
섹션 제목: “운영 기준: 조용히 실패하는 Queue 설정”DLQ Retention은 원본 Queue보다 길게
Standard Queue에서 DLQ로 이동한 메시지는 원본 enqueue timestamp를 유지한다. 원본 큐 retention이 4일이고 메시지가 3일 동안 재시도된 뒤 DLQ로 이동했는데, DLQ retention도 4일이면 운영자는 DLQ에서 4일을 더 볼 수 있는 것이 아니라 약 1일 뒤 메시지를 잃을 수 있다. 이 실패는 에러 로그 없이 “DLQ에 있었어야 할 메시지가 안 보임”으로 나타나므로, DLQ retention은 원본 Queue보다 길게, 운영 분석이 필요하면 최대값인 14일로 둔다.
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일이므로 원본 큐와 같거나 짧은지 확인하고, 짧다면 아래처럼 늘린다.
aws sqs set-queue-attributes \ --queue-url https://sqs.ap-northeast-2.amazonaws.com/123456/email-dlq \ --attributes MessageRetentionPeriod=1209600 \ --region ap-northeast-2출처: Setting-up dead-letter queue retention in Amazon SQS - AWS 공식 문서
직접 폴링 Worker의 Long Polling 설정
Lambda를 SQS Trigger로 연결하는 경우 Event Source Mapping이 Queue를 폴링하고 Lambda를 배치 단위로 호출한다. NestJS Worker처럼 직접 폴링하는 경우에는 waitTimeSeconds: 20을 명시해 빈 응답 비용을 줄인다. Long Polling의 최대 대기 시간은 20초이며, 0초는 Short Polling이다.
BullMQ 작업 기록 보존
Redis 기반 Queue는 처리 완료·실패 Job 기록이 계속 쌓이면 Redis 메모리를 압박한다. SQS의 retention을 운영 기준으로 잡듯, BullMQ도 완료/실패 기록을 얼마나 남길지 Job 옵션으로 정한다.
// BullMQ 작업 기록 보존 설정 예시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에서 가장 자주 발생하는 장애 패턴과 해결법 총정리 (중급)