Amazon SQS - 완전 관리형 메시지 큐 서비스
SQS로 비동기 처리와 마이크로서비스 간 통신을 구현하는 방법
Amazon SQS란?
Amazon SQS(Simple Queue Service)는 완전 관리형 메시지 큐 서비스로, 마이크로서비스, 분산 시스템 및 서버리스 애플리케이션을 분리하고 확장할 수 있게 해줌.
메시지를 안정적으로 저장하고 전달하며, 메시지 손실 없이 시스템 간 통신을 보장함.
왜 SQS를 사용해야 하는가?
동기 처리 방식의 문제점
전통적인 동기 API 호출:
1
2
3
4
5
6
사용자 요청 → API Server
├─ 주문 생성 (200ms)
├─ 재고 확인 (300ms)
├─ 결제 처리 (1000ms) ⏱️ 느림
├─ 이메일 전송 (500ms)
└─ 응답 반환 (총 2초 대기)
문제점:
- 느린 응답: 모든 작업이 끝날 때까지 사용자 대기
- 서비스 의존성: 결제 서비스 장애 시 전체 요청 실패
- 확장성 제한: 동시 요청 수만큼 서버 필요
- 리소스 낭비: CPU 대기 시간 동안 유휴 상태
- 재시도 어려움: 실패 시 전체 재실행 필요
SQS의 해결 방법
비동기 메시지 큐 패턴:
1
2
3
4
5
6
7
8
9
10
11
사용자 요청 → API Server
├─ 주문 생성 (200ms)
├─ SQS에 메시지 전송 (50ms)
└─ 즉시 응답 반환 ✅ (총 250ms)
백그라운드:
SQS Queue → Lambda Worker
├─ 재고 확인
├─ 결제 처리
└─ 이메일 전송
(사용자는 대기 안 함)
장점:
- 빠른 응답: 즉시 사용자에게 응답 반환
- 서비스 분리: 각 서비스가 독립적으로 동작
- 자동 확장: 메시지 수에 따라 워커 자동 증가
- 내결함성: 실패 시 자동 재시도
- 부하 평준화: 급격한 트래픽도 안정적으로 처리
SQS 핵심 개념
1. 큐 (Queue)
메시지를 임시로 저장하는 버퍼.
큐 유형 비교:
| 특성 | Standard Queue | FIFO Queue |
|---|---|---|
| 순서 보장 | 순서 보장 안 됨 (거의 순서대로) | 완벽한 순서 보장 |
| 중복 가능성 | 최소 1회 전달 (중복 가능) | 정확히 1회 전달 |
| 처리량 | 거의 무제한 | 3,000 msg/초 (배치 시 30,000) |
| 사용 사례 | 로그 처리, 대량 작업 | 금융 거래, 주문 처리 |
Standard Queue 예시:
1
2
3
4
5
6
// 메시지 순서가 중요하지 않은 경우
이미지 리사이징 작업
→ 어떤 순서로 처리되든 상관없음
로그 수집
→ 순서 상관없이 모두 저장하면 됨
FIFO Queue 예시:
1
2
3
4
5
6
// 순서가 중요한 경우
주문 상태 업데이트: 생성 → 결제 → 배송 → 완료
→ 반드시 순서대로 처리되어야 함
재고 관리: 입고 → 출고 → 재고 확인
→ 순서가 바뀌면 재고 수량 오류 발생
2. 메시지 (Message)
큐에 저장되는 데이터 단위.
메시지 구조:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
MessageId: "12345-abcde-67890-fghij", // AWS 자동 생성
ReceiptHandle: "...", // 삭제 시 필요한 핸들
MD5OfBody: "...", // 무결성 검증
Body: JSON.stringify({ // 실제 데이터 (최대 256KB)
orderId: "order-123",
customerId: "user-456",
items: [...],
totalAmount: 50000
}),
Attributes: { // 시스템 속성
SentTimestamp: "1704772800000",
ApproximateReceiveCount: "1",
ApproximateFirstReceiveTimestamp: "..."
},
MessageAttributes: { // 사용자 정의 속성
Priority: {
DataType: "Number",
StringValue: "1"
},
OrderType: {
DataType: "String",
StringValue: "express"
}
}
}
3. Visibility Timeout (가시성 타임아웃)
메시지를 수신한 후, 다른 소비자가 볼 수 없는 시간.
동작 원리:
1
2
3
4
5
6
7
8
9
10
11
12
1. 워커 A가 메시지 수신
→ Visibility Timeout 시작 (예: 5분)
→ 다른 워커는 이 메시지를 못 봄
2-a. 워커 A가 5분 내 처리 완료
→ DeleteMessage 호출
→ 메시지 영구 삭제 ✅
2-b. 워커 A가 5분 내 처리 못 함
→ Visibility Timeout 만료
→ 메시지 다시 큐에 나타남
→ 워커 B가 재시도 가능 🔄
최적 Visibility Timeout 설정:
1
2
3
4
5
6
7
8
9
// 평균 처리 시간 기준으로 설정
평균 처리 시간: 30초
→ Visibility Timeout: 60초 (2배 여유)
평균 처리 시간: 2분
→ Visibility Timeout: 5분
너무 짧으면: 중복 처리 발생
너무 길면: 실패 시 재시도까지 오래 걸림
4. Dead Letter Queue (DLQ)
여러 번 실패한 메시지를 보관하는 큐.
설정 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
resources:
Resources:
MainQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 300 # 5분
MessageRetentionPeriod: 345600 # 4일
RedrivePolicy:
deadLetterTargetArn: !GetAtt DLQueue.Arn
maxReceiveCount: 3 # 3번 실패 후 DLQ로
DLQueue:
Type: AWS::SQS::Queue
Properties:
MessageRetentionPeriod: 1209600 # 14일 (오래 보관)
DLQ 활용:
1
2
3
4
5
6
7
8
// 1. 실패 원인 분석
// DLQ에서 메시지를 읽어 로그 확인
// 2. 수동 재처리
// 문제 수정 후 DLQ → Main Queue로 이동
// 3. 알림 설정
// DLQ에 메시지 도착 시 CloudWatch Alarm → SNS 알림
실전 프로젝트 활용 사례
사례 1: 비동기 주문 처리 시스템
아키텍처:
1
2
3
4
5
6
7
8
9
API Gateway → Lambda (Order Creator)
→ SQS Queue (Main)
→ Lambda (Order Worker) [배치 10개]
├─ 재고 확인
├─ 결제 처리
├─ DynamoDB 저장
└─ SNS 알림
실패 시 → DLQ (Dead Letter Queue)
메시지 전송 (Producer):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
const sqsClient = new SQSClient({});
export const handler = async (event) => {
const order = JSON.parse(event.body);
// 주문 유효성 검사
if (!order.customerId || !order.items || order.items.length === 0) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Invalid order data' })
};
}
// 총 금액 계산
const totalAmount = order.items.reduce((sum, item) =>
sum + (item.quantity * item.price), 0
);
// SQS에 메시지 전송
const orderId = `order-${Date.now()}`;
await sqsClient.send(new SendMessageCommand({
QueueUrl: process.env.QUEUE_URL,
MessageBody: JSON.stringify({
orderId,
customerId: order.customerId,
items: order.items,
totalAmount,
createdAt: new Date().toISOString()
}),
MessageAttributes: {
priority: {
DataType: 'Number',
StringValue: totalAmount > 1000000 ? '1' : '0' // 고액 주문 우선 처리
},
orderType: {
DataType: 'String',
StringValue: 'standard'
}
}
}));
// 즉시 응답 반환 (202 Accepted)
return {
statusCode: 202,
body: JSON.stringify({
message: 'Order accepted for processing',
orderId,
estimatedProcessingTime: '1-2 minutes'
})
};
};
메시지 수신 및 처리 (Consumer):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { SQSClient, DeleteMessageCommand } from '@aws-sdk/client-sqs';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
const sqsClient = new SQSClient({});
const dynamoClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const snsClient = new SNSClient({});
export const handler = async (event) => {
const results = { successful: 0, failed: 0 };
for (const record of event.Records) {
const order = JSON.parse(record.body);
try {
// 1. 재고 확인 (10% 실패율 시뮬레이션)
await checkInventory(order.items);
// 2. 결제 처리 (5% 실패율 시뮬레이션)
const paymentInfo = await processPayment(order);
// 3. DynamoDB에 저장
await dynamoClient.send(new PutCommand({
TableName: process.env.ORDERS_TABLE,
Item: {
orderId: order.orderId,
customerId: order.customerId,
items: order.items,
totalAmount: order.totalAmount,
status: 'completed',
paymentInfo,
createdAt: order.createdAt,
processedAt: new Date().toISOString()
}
}));
// 4. 고객에게 SNS 알림
await snsClient.send(new PublishCommand({
TopicArn: process.env.SNS_TOPIC_ARN,
Subject: '주문이 완료되었습니다',
Message: `주문번호 ${order.orderId}의 결제가 완료되었습니다.`
}));
// 5. 성공 시 메시지 삭제
await sqsClient.send(new DeleteMessageCommand({
QueueUrl: process.env.QUEUE_URL,
ReceiptHandle: record.receiptHandle
}));
results.successful++;
} catch (error) {
console.error(`Failed to process order ${order.orderId}:`, error);
// 실패한 주문도 저장 (상태: failed)
await dynamoClient.send(new PutCommand({
TableName: process.env.ORDERS_TABLE,
Item: {
orderId: order.orderId,
status: 'failed',
errorMessage: error.message,
failedAt: new Date().toISOString()
}
}));
results.failed++;
// 메시지를 삭제하지 않으면 자동으로 재시도됨
// maxReceiveCount 초과 시 DLQ로 이동
}
}
console.log(`Processed: ${results.successful} successful, ${results.failed} failed`);
return results;
};
// 재고 확인 시뮬레이션
async function checkInventory(items) {
await new Promise(resolve => setTimeout(resolve, 500));
if (Math.random() < 0.1) {
throw new Error('재고 부족');
}
}
// 결제 처리 시뮬레이션
async function processPayment(order) {
await new Promise(resolve => setTimeout(resolve, 1000));
if (Math.random() < 0.05) {
throw new Error('결제 실패');
}
return {
transactionId: `txn-${Date.now()}`,
status: 'approved',
processedAt: new Date().toISOString()
};
}
Lambda 트리거 설정:
1
2
3
4
5
6
7
8
9
functions:
orderWorker:
handler: worker.handler
events:
- sqs:
arn: !GetAtt OrderQueue.Arn
batchSize: 10 # 한 번에 10개 메시지 처리
maximumBatchingWindowInSeconds: 5 # 최대 5초 대기 후 처리
functionResponseType: ReportBatchItemFailures # 실패한 메시지만 재시도
사례 2: 알림 전송 시스템
SNS → SQS Fanout 패턴:
1
2
3
4
5
SNS Topic (알림 발행)
├─ Email Queue → Lambda (Email 발송)
├─ SMS Queue → Lambda (SMS 발송)
├─ Slack Queue → Lambda (Slack 메시지)
└─ Webhook Queue → Lambda (Webhook 호출)
각 큐에 필터 정책 적용:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Email Queue 구독 필터
{
"channel": ["email", "all"]
}
// SMS Queue 구독 필터
{
"channel": ["sms", "all"]
}
// Slack Queue 구독 필터
{
"channel": ["slack", "all"]
}
메시지 발행:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SNS에 메시지 발행
await snsClient.send(new PublishCommand({
TopicArn: SNS_TOPIC_ARN,
Message: JSON.stringify({
notificationId: 'notif-123',
title: '시스템 알림',
message: '중요한 업데이트가 있습니다'
}),
MessageAttributes: {
channel: {
DataType: 'String',
StringValue: 'all' // 모든 채널로 전송
},
severity: {
DataType: 'String',
StringValue: 'high'
}
}
}));
// SNS가 필터링 후 각 SQS 큐로 전달
// 각 Lambda가 병렬로 처리
SQS 고급 기능
1. Long Polling (긴 폴링)
Short Polling (기본):
1
2
3
4
5
6
Lambda → SQS: 메시지 있나요?
SQS → Lambda: 없어요 (빈 응답)
# 1초 후
Lambda → SQS: 메시지 있나요?
SQS → Lambda: 없어요
# 계속 반복 (비용 증가, 비효율적)
Long Polling (권장):
1
2
3
Lambda → SQS: 메시지 있나요? (20초 대기)
# 메시지 올 때까지 최대 20초 대기
SQS → Lambda: 메시지 도착! (즉시 응답)
설정:
1
2
3
4
5
6
resources:
Resources:
MyQueue:
Type: AWS::SQS::Queue
Properties:
ReceiveMessageWaitTimeSeconds: 20 # Long Polling 활성화
장점:
- API 호출 횟수 감소 (비용 절감)
- 빈 응답 감소
- 메시지 수신 지연 감소
2. Delay Queue (지연 큐)
메시지가 큐에 도착한 후 일정 시간 동안 보이지 않게 함.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 큐 레벨 지연 (모든 메시지)
resources:
Resources:
DelayedQueue:
Type: AWS::SQS::Queue
Properties:
DelaySeconds: 300 # 5분 지연
// 메시지 레벨 지연 (개별 메시지)
await sqsClient.send(new SendMessageCommand({
QueueUrl: QUEUE_URL,
MessageBody: JSON.stringify(data),
DelaySeconds: 60 # 이 메시지만 1분 지연
}));
사용 사례:
1
2
3
4
5
6
// 이메일 재전송 로직
await sqsClient.send(new SendMessageCommand({
QueueUrl: RETRY_QUEUE_URL,
MessageBody: JSON.stringify({ email, attempt: 1 }),
DelaySeconds: 300 // 5분 후 재시도
}));
3. Message Deduplication (중복 제거) - FIFO만
Content-Based Deduplication:
1
2
3
4
5
6
MyFIFOQueue:
Type: AWS::SQS::Queue
Properties:
FifoQueue: true
QueueName: orders.fifo
ContentBasedDeduplication: true # 메시지 내용으로 중복 제거
Deduplication ID 사용:
1
2
3
4
5
6
await sqsClient.send(new SendMessageCommand({
QueueUrl: 'https://sqs.ap-northeast-2.amazonaws.com/123456789/orders.fifo',
MessageBody: JSON.stringify(order),
MessageGroupId: order.customerId, # FIFO 필수
MessageDeduplicationId: order.orderId # 5분 내 동일 ID 중복 제거
}));
4. Message Groups (FIFO 큐의 순서 보장)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 고객별로 순서 보장
await sqsClient.send(new SendMessageCommand({
QueueUrl: FIFO_QUEUE_URL,
MessageBody: JSON.stringify({
customerId: 'user-123',
action: 'order_created'
}),
MessageGroupId: 'user-123', // 같은 그룹 ID끼리 순서 보장
MessageDeduplicationId: `order-${Date.now()}`
}));
await sqsClient.send(new SendMessageCommand({
QueueUrl: FIFO_QUEUE_URL,
MessageBody: JSON.stringify({
customerId: 'user-123',
action: 'payment_completed'
}),
MessageGroupId: 'user-123', // user-123의 메시지는 순서대로
MessageDeduplicationId: `payment-${Date.now()}`
}));
// user-456의 메시지는 user-123과 독립적으로 처리 가능 (병렬성)
await sqsClient.send(new SendMessageCommand({
QueueUrl: FIFO_QUEUE_URL,
MessageBody: JSON.stringify({
customerId: 'user-456',
action: 'order_created'
}),
MessageGroupId: 'user-456', // 다른 그룹 → 병렬 처리
MessageDeduplicationId: `order-${Date.now()}`
}));
SQS 배치 작업
배치 전송 (최대 10개)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { SendMessageBatchCommand } from '@aws-sdk/client-sqs';
async function sendBatchMessages(messages) {
const entries = messages.map((msg, index) => ({
Id: String(index), // 배치 내 고유 ID
MessageBody: JSON.stringify(msg),
MessageAttributes: {
timestamp: {
DataType: 'String',
StringValue: new Date().toISOString()
}
}
}));
// 10개씩 나누기
for (let i = 0; i < entries.length; i += 10) {
const batch = entries.slice(i, i + 10);
const response = await sqsClient.send(new SendMessageBatchCommand({
QueueUrl: QUEUE_URL,
Entries: batch
}));
console.log(`Sent ${response.Successful.length} messages`);
if (response.Failed.length > 0) {
console.error('Failed messages:', response.Failed);
}
}
}
// 100개 메시지를 10개씩 배치로 전송
await sendBatchMessages(Array.from({ length: 100 }, (_, i) => ({
orderId: `order-${i}`,
data: '...'
})));
비용 절감:
1
2
개별 전송: 100 요청 × $0.0000004 = $0.00004
배치 전송: 10 요청 × $0.0000004 = $0.000004 (90% 절감)
배치 삭제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { DeleteMessageBatchCommand } from '@aws-sdk/client-sqs';
export const handler = async (event) => {
const processed = [];
for (const record of event.Records) {
const message = JSON.parse(record.body);
try {
await processMessage(message);
processed.push({
Id: record.messageId,
ReceiptHandle: record.receiptHandle
});
} catch (error) {
console.error('Failed:', error);
}
}
// 성공한 메시지 일괄 삭제
if (processed.length > 0) {
await sqsClient.send(new DeleteMessageBatchCommand({
QueueUrl: QUEUE_URL,
Entries: processed
}));
}
};
SQS 모니터링 및 알람
CloudWatch 메트릭
주요 메트릭:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
resources:
Resources:
# 큐에 대기 중인 메시지 수 알람
HighQueueDepthAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: HighQueueDepth
MetricName: ApproximateNumberOfMessagesVisible
Namespace: AWS/SQS
Dimensions:
- Name: QueueName
Value: !GetAtt MyQueue.QueueName
Statistic: Average
Period: 300 # 5분
EvaluationPeriods: 2
Threshold: 1000 # 1000개 이상 대기 시
ComparisonOperator: GreaterThanThreshold
AlarmActions:
- !Ref SNSAlertTopic
# DLQ에 메시지 도착 시 알람
DLQAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: MessagesInDLQ
MetricName: ApproximateNumberOfMessagesVisible
Namespace: AWS/SQS
Dimensions:
- Name: QueueName
Value: !GetAtt DLQueue.QueueName
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1 # DLQ에 메시지 1개만 있어도 알람
ComparisonOperator: GreaterThanOrEqualToThreshold
AlarmActions:
- !Ref SNSAlertTopic
# 오래된 메시지 알람
OldestMessageAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: OldMessage
MetricName: ApproximateAgeOfOldestMessage
Namespace: AWS/SQS
Dimensions:
- Name: QueueName
Value: !GetAtt MyQueue.QueueName
Statistic: Maximum
Period: 300
EvaluationPeriods: 1
Threshold: 3600 # 1시간 이상 대기
ComparisonOperator: GreaterThanThreshold
큐 상태 모니터링
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { GetQueueAttributesCommand } from '@aws-sdk/client-sqs';
async function getQueueStats(queueUrl) {
const response = await sqsClient.send(new GetQueueAttributesCommand({
QueueUrl: queueUrl,
AttributeNames: [
'ApproximateNumberOfMessages', // 대기 중
'ApproximateNumberOfMessagesNotVisible', // 처리 중
'ApproximateNumberOfMessagesDelayed' // 지연 중
]
}));
return {
waiting: parseInt(response.Attributes.ApproximateNumberOfMessages),
inFlight: parseInt(response.Attributes.ApproximateNumberOfMessagesNotVisible),
delayed: parseInt(response.Attributes.ApproximateNumberOfMessagesDelayed)
};
}
// 대시보드에 표시
const stats = await getQueueStats(QUEUE_URL);
console.log(`대기: ${stats.waiting}, 처리중: ${stats.inFlight}, 지연: ${stats.delayed}`);
SQS Best Practices
1. 멱등성 보장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ❌ 나쁜 예 - 중복 처리 시 문제 발생
async function processOrder(order) {
await chargeCustomer(order.customerId, order.totalAmount);
await reduceInventory(order.items);
}
// 메시지 중복 수신 시 두 번 결제됨!
// ✅ 좋은 예 - 멱등성 보장
async function processOrder(order) {
// 이미 처리했는지 확인
const existing = await checkIfProcessed(order.orderId);
if (existing) {
console.log('Already processed, skipping');
return;
}
await chargeCustomer(order.customerId, order.totalAmount);
await reduceInventory(order.items);
// 처리 완료 기록
await markAsProcessed(order.orderId);
}
2. 적절한 Visibility Timeout 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 동적으로 Visibility Timeout 연장
import { ChangeMessageVisibilityCommand } from '@aws-sdk/client-sqs';
export const handler = async (event) => {
for (const record of event.Records) {
const startTime = Date.now();
try {
await processLongRunningTask(record.body);
} catch (error) {
const elapsed = Date.now() - startTime;
// 처리 시간이 오래 걸리면 Visibility Timeout 연장
if (elapsed > 240000) { // 4분 이상
await sqsClient.send(new ChangeMessageVisibilityCommand({
QueueUrl: QUEUE_URL,
ReceiptHandle: record.receiptHandle,
VisibilityTimeout: 600 // 10분으로 연장
}));
}
throw error; // 재시도
}
}
};
3. 배치 크기 최적화
1
2
3
4
5
6
7
8
9
10
# Lambda 트리거 배치 설정
functions:
worker:
handler: worker.handler
timeout: 300 # 5분
events:
- sqs:
arn: !GetAtt Queue.Arn
batchSize: 10 # 한 번에 10개
maximumBatchingWindowInSeconds: 5 # 최대 5초 대기
배치 크기 선택 기준:
1
2
3
4
5
6
7
8
처리 시간이 짧은 작업 (< 1초):
→ batchSize: 100 (많이 처리)
처리 시간이 긴 작업 (> 10초):
→ batchSize: 1-5 (적게 처리, 타임아웃 방지)
중간 정도:
→ batchSize: 10-20
4. DLQ 처리 자동화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// DLQ 메시지 분석 및 재처리
import { ReceiveMessageCommand, SendMessageCommand } from '@aws-sdk/client-sqs';
async function reprocessDLQ() {
const response = await sqsClient.send(new ReceiveMessageCommand({
QueueUrl: DLQ_URL,
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20
}));
for (const message of response.Messages || []) {
const body = JSON.parse(message.Body);
// 실패 원인 분석
console.log('Failed message:', body);
// 수정 후 원래 큐로 재전송
await sqsClient.send(new SendMessageCommand({
QueueUrl: MAIN_QUEUE_URL,
MessageBody: message.Body,
MessageAttributes: {
retryCount: {
DataType: 'Number',
StringValue: '1'
}
}
}));
// DLQ에서 삭제
await sqsClient.send(new DeleteMessageCommand({
QueueUrl: DLQ_URL,
ReceiptHandle: message.ReceiptHandle
}));
}
}
5. 메시지 크기 최적화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ❌ 나쁜 예 - 큰 데이터를 메시지에 직접 포함 (256KB 제한)
await sqsClient.send(new SendMessageCommand({
QueueUrl: QUEUE_URL,
MessageBody: JSON.stringify({
orderId: 'order-123',
largeData: base64EncodedImage // 너무 큼!
})
}));
// ✅ 좋은 예 - S3에 저장하고 참조만 전달
const s3Key = `temp/${orderId}.json`;
await s3Client.send(new PutObjectCommand({
Bucket: 'my-bucket',
Key: s3Key,
Body: JSON.stringify(largeData)
}));
await sqsClient.send(new SendMessageCommand({
QueueUrl: QUEUE_URL,
MessageBody: JSON.stringify({
orderId: 'order-123',
s3Bucket: 'my-bucket',
s3Key: s3Key // 참조만 전달
})
}));
// Consumer에서 S3에서 다운로드
const data = await s3Client.send(new GetObjectCommand({
Bucket: message.s3Bucket,
Key: message.s3Key
}));
SQS vs 다른 메시징 서비스
SQS vs SNS
| 특성 | SQS | SNS |
|---|---|---|
| 패턴 | 점대점 (Queue) | 발행-구독 (Pub-Sub) |
| 메시지 저장 | 저장 (최대 14일) | 저장 안 함 (즉시 전달) |
| 소비자 | 한 소비자만 처리 | 여러 구독자에게 전달 |
| 재시도 | 자동 재시도 | 구독자가 처리 |
| 사용 사례 | 작업 큐, 부하 평준화 | 알림, 이벤트 브로드캐스트 |
함께 사용하는 패턴:
1
2
SNS (발행) → SQS (저장) → Lambda (처리)
# SNS Fanout 패턴
SQS vs Amazon MQ
| 특성 | SQS | Amazon MQ |
|---|---|---|
| 프로토콜 | AWS API (HTTP) | JMS, AMQP, MQTT, STOMP |
| 관리 | 완전 관리형 | 관리형 (일부 설정 필요) |
| 확장성 | 무제한 자동 확장 | 수동 확장 |
| 비용 | 저렴 (사용량 기반) | 인스턴스 기반 |
| 사용 사례 | 신규 애플리케이션 | 레거시 마이그레이션 |
SQS vs Kafka (Amazon MSK)
| 특성 | SQS | Kafka |
|---|---|---|
| 처리량 | 높음 | 매우 높음 |
| 순서 보장 | FIFO 큐만 | 파티션 내 보장 |
| 메시지 보관 | 최대 14일 | 무제한 (설정에 따라) |
| 복잡도 | 낮음 | 높음 |
| 비용 | 저렴 | 높음 (클러스터 운영) |
| 사용 사례 | 일반 큐잉 | 이벤트 스트리밍, 로그 |
SQS 비용 최적화
비용 구조
1
2
3
4
5
6
7
요청 비용:
- 처음 100만 요청/월: 무료
- 이후: $0.40 per million requests
데이터 전송:
- AWS 내부: 무료
- 인터넷으로 나가는 경우: 유료
최적화 전략
1. Long Polling 사용:
1
2
Short Polling: 100만 빈 응답 = $0.40
Long Polling: 5만 응답 = $0.02 (95% 절감)
2. 배치 작업:
1
2
개별 전송: 100만 메시지 = $0.40
배치 전송: 10만 요청 = $0.04 (90% 절감)
3. 메시지 크기 최적화:
1
2
3
64KB 청크로 청구됨
65KB 메시지 = 2 요청 비용
63KB 메시지 = 1 요청 비용
마치며
Amazon SQS는 서버리스 아키텍처의 필수 구성 요소임.
SQS를 사용해야 할 때:
- 비동기 작업 처리 (이메일, 알림, 파일 처리)
- 마이크로서비스 간 통신
- 부하 평준화 (트래픽 급증 대응)
- 재시도가 필요한 작업
- 작업 큐 구현
핵심 패턴:
- Producer-Consumer: API → SQS → Lambda
- Fanout: SNS → 여러 SQS → 여러 Lambda
- DLQ 활용: 실패 메시지 자동 격리
- Batch Processing: 10-100개씩 묶어 처리
- Long Polling: 비용 절감 및 지연 감소
실전 프로젝트에서는 SQS를 Lambda(처리), DynamoDB(저장), SNS(알림)와 조합하여 완전한 비동기 시스템을 구축할 수 있음.
적절한 배치 크기, Visibility Timeout, DLQ 설정으로 안정적이고 비용 효율적인 메시지 처리 시스템을 만들 수 있음.