Post

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초 대기)

문제점:

  1. 느린 응답: 모든 작업이 끝날 때까지 사용자 대기
  2. 서비스 의존성: 결제 서비스 장애 시 전체 요청 실패
  3. 확장성 제한: 동시 요청 수만큼 서버 필요
  4. 리소스 낭비: CPU 대기 시간 동안 유휴 상태
  5. 재시도 어려움: 실패 시 전체 재실행 필요

SQS의 해결 방법

비동기 메시지 큐 패턴:

1
2
3
4
5
6
7
8
9
10
11
사용자 요청 → API Server
           ├─ 주문 생성 (200ms)
           ├─ SQS에 메시지 전송 (50ms)
           └─ 즉시 응답 반환 ✅ (총 250ms)

백그라운드:
SQS Queue → Lambda Worker
          ├─ 재고 확인
          ├─ 결제 처리
          └─ 이메일 전송
          (사용자는 대기 안 함)

장점:

  1. 빠른 응답: 즉시 사용자에게 응답 반환
  2. 서비스 분리: 각 서비스가 독립적으로 동작
  3. 자동 확장: 메시지 수에 따라 워커 자동 증가
  4. 내결함성: 실패 시 자동 재시도
  5. 부하 평준화: 급격한 트래픽도 안정적으로 처리

SQS 핵심 개념

1. 큐 (Queue)

메시지를 임시로 저장하는 버퍼.

큐 유형 비교:

특성Standard QueueFIFO 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

특성SQSSNS
패턴점대점 (Queue)발행-구독 (Pub-Sub)
메시지 저장저장 (최대 14일)저장 안 함 (즉시 전달)
소비자한 소비자만 처리여러 구독자에게 전달
재시도자동 재시도구독자가 처리
사용 사례작업 큐, 부하 평준화알림, 이벤트 브로드캐스트

함께 사용하는 패턴:

1
2
SNS (발행) → SQS (저장) → Lambda (처리)
# SNS Fanout 패턴

SQS vs Amazon MQ

특성SQSAmazon MQ
프로토콜AWS API (HTTP)JMS, AMQP, MQTT, STOMP
관리완전 관리형관리형 (일부 설정 필요)
확장성무제한 자동 확장수동 확장
비용저렴 (사용량 기반)인스턴스 기반
사용 사례신규 애플리케이션레거시 마이그레이션

SQS vs Kafka (Amazon MSK)

특성SQSKafka
처리량높음매우 높음
순서 보장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를 사용해야 할 때:

  • 비동기 작업 처리 (이메일, 알림, 파일 처리)
  • 마이크로서비스 간 통신
  • 부하 평준화 (트래픽 급증 대응)
  • 재시도가 필요한 작업
  • 작업 큐 구현

핵심 패턴:

  1. Producer-Consumer: API → SQS → Lambda
  2. Fanout: SNS → 여러 SQS → 여러 Lambda
  3. DLQ 활용: 실패 메시지 자동 격리
  4. Batch Processing: 10-100개씩 묶어 처리
  5. Long Polling: 비용 절감 및 지연 감소

실전 프로젝트에서는 SQS를 Lambda(처리), DynamoDB(저장), SNS(알림)와 조합하여 완전한 비동기 시스템을 구축할 수 있음.

적절한 배치 크기, Visibility Timeout, DLQ 설정으로 안정적이고 비용 효율적인 메시지 처리 시스템을 만들 수 있음.

도움이 되셨길 바랍니다! 😀

This post is licensed under CC BY 4.0 by the author.