Post

Amazon SNS - 완전 관리형 메시징 서비스

SNS를 활용한 발행-구독 패턴과 멀티 채널 알림 시스템 구축

Amazon SNS란?

Amazon SNS(Simple Notification Service)는 완전 관리형 발행-구독(Pub-Sub) 메시징 서비스임.

하나의 메시지를 여러 구독자에게 동시에 전달할 수 있으며, 이메일, SMS, Lambda, SQS, HTTP 엔드포인트 등 다양한 프로토콜을 지원함.


왜 SNS를 사용해야 하는가?

전통적인 알림 시스템의 문제점

직접 구현 방식:

1
2
3
4
5
애플리케이션 서버
  ├─ SMTP 서버 연결 → 이메일 전송
  ├─ SMS 게이트웨이 호출 → SMS 전송
  ├─ Slack API 호출 → Slack 메시지
  └─ Webhook 호출 → 외부 시스템

문제점:

  1. 복잡한 통합: 각 채널마다 다른 API 구현 필요
  2. 장애 처리: 특정 채널 장애 시 전체 프로세스 영향
  3. 확장 어려움: 새 채널 추가 시 코드 수정 필요
  4. 재시도 로직: 각 채널별로 재시도 구현해야 함
  5. 관리 부담: 인증 정보, 연결 풀, 타임아웃 등 직접 관리

SNS의 해결 방법

발행-구독 패턴:

1
2
3
4
5
6
7
8
애플리케이션
  ↓
SNS Topic (한 번만 발행)
  ├─ Email 구독 → 자동 이메일 전송
  ├─ SMS 구독 → 자동 SMS 전송
  ├─ Lambda 구독 → Slack 전송
  ├─ SQS 구독 → 비동기 처리
  └─ HTTP 구독 → Webhook 호출

장점:

  1. 단일 발행: 한 번 메시지 발행으로 모든 구독자에게 전달
  2. 느슨한 결합: 발행자와 구독자가 독립적
  3. 자동 재시도: 전달 실패 시 자동 재시도
  4. 확장 용이: 구독자 추가/제거 간편
  5. 프로토콜 다양성: 이메일, SMS, HTTP, SQS, Lambda 등

SNS 핵심 개념

1. Topic (토픽)

메시지를 발행하는 논리적 채널. 구독자들이 메시지를 받기 위해 구독하는 대상.

토픽 생성:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { SNSClient, CreateTopicCommand } from '@aws-sdk/client-sns';

const snsClient = new SNSClient({});

const response = await snsClient.send(new CreateTopicCommand({
  Name: 'order-notifications',
  Attributes: {
    DisplayName: '주문 알림',
    DeliveryPolicy: JSON.stringify({
      http: {
        defaultHealthyRetryPolicy: {
          minDelayTarget: 20,
          maxDelayTarget: 20,
          numRetries: 3,
          backoffFunction: 'linear'
        }
      }
    })
  }
}));

const topicArn = response.TopicArn;
// arn:aws:sns:ap-northeast-2:123456789:order-notifications

2. Subscription (구독)

토픽에서 메시지를 수신할 엔드포인트 등록.

지원 프로토콜:

  • Email: 이메일 주소로 전송
  • Email-JSON: JSON 형식으로 이메일 전송
  • SMS: 전화번호로 SMS 전송
  • HTTP/HTTPS: 웹훅으로 POST 요청
  • SQS: SQS 큐로 메시지 전달
  • Lambda: Lambda 함수 트리거
  • Application: 모바일 푸시 알림
  • Firehose: Kinesis Data Firehose로 스트리밍

구독 생성 예시:

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
import { SubscribeCommand } from '@aws-sdk/client-sns';

// 이메일 구독
await snsClient.send(new SubscribeCommand({
  TopicArn: topicArn,
  Protocol: 'email',
  Endpoint: 'user@example.com'
}));
// 사용자에게 확인 이메일 전송됨 (구독 확인 필요)

// Lambda 구독
await snsClient.send(new SubscribeCommand({
  TopicArn: topicArn,
  Protocol: 'lambda',
  Endpoint: 'arn:aws:lambda:ap-northeast-2:123456789:function:notification-handler'
}));

// SQS 구독
await snsClient.send(new SubscribeCommand({
  TopicArn: topicArn,
  Protocol: 'sqs',
  Endpoint: 'arn:aws:sqs:ap-northeast-2:123456789:my-queue',
  Attributes: {
    RawMessageDelivery: 'true'  // SNS 메타데이터 없이 원본 메시지만 전달
  }
}));

3. Message Publishing (메시지 발행)

토픽에 메시지를 발행하면 모든 구독자에게 전달됨.

기본 발행:

1
2
3
4
5
6
7
import { PublishCommand } from '@aws-sdk/client-sns';

await snsClient.send(new PublishCommand({
  TopicArn: topicArn,
  Subject: '주문 완료 알림',
  Message: '주문번호 12345가 성공적으로 처리되었습니다.'
}));

메시지 속성 포함:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
await snsClient.send(new PublishCommand({
  TopicArn: topicArn,
  Message: JSON.stringify({
    orderId: 'order-123',
    customerId: 'user-456',
    totalAmount: 50000,
    status: 'completed'
  }),
  MessageAttributes: {
    orderType: {
      DataType: 'String',
      StringValue: 'express'
    },
    priority: {
      DataType: 'Number',
      StringValue: '1'
    },
    timestamp: {
      DataType: 'String',
      StringValue: new Date().toISOString()
    }
  }
}));

실전 프로젝트 활용 사례

사례 1: 멀티 채널 알림 시스템

아키텍처:

1
2
3
4
5
6
7
주문 완료 이벤트
  ↓
SNS Topic
  ├─ Email Queue → Lambda → SES 이메일 전송
  ├─ SMS Queue → Lambda → SNS SMS 전송
  ├─ Slack Queue → Lambda → Slack Webhook
  └─ Webhook Queue → Lambda → 외부 시스템 호출

메시지 필터링 정책:

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
// 이메일 구독 (email 또는 all 채널만)
await snsClient.send(new SubscribeCommand({
  TopicArn: topicArn,
  Protocol: 'sqs',
  Endpoint: emailQueueArn,
  Attributes: {
    FilterPolicy: JSON.stringify({
      channel: ['email', 'all']
    }),
    RawMessageDelivery: 'true'
  }
}));

// SMS 구독 (sms 또는 all 채널만)
await snsClient.send(new SubscribeCommand({
  TopicArn: topicArn,
  Protocol: 'sqs',
  Endpoint: smsQueueArn,
  Attributes: {
    FilterPolicy: JSON.stringify({
      channel: ['sms', 'all']
    }),
    RawMessageDelivery: 'true'
  }
}));

// Slack 구독 (slack 또는 all 채널만)
await snsClient.send(new SubscribeCommand({
  TopicArn: topicArn,
  Protocol: 'sqs',
  Endpoint: slackQueueArn,
  Attributes: {
    FilterPolicy: JSON.stringify({
      channel: ['slack', 'all']
    }),
    RawMessageDelivery: 'true'
  }
}));

채널별 메시지 발행:

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
// 이메일만 전송
await snsClient.send(new PublishCommand({
  TopicArn: topicArn,
  Message: JSON.stringify({
    title: '주문 완료',
    message: '주문이 성공적으로 처리되었습니다'
  }),
  MessageAttributes: {
    channel: {
      DataType: 'String',
      StringValue: 'email'
    }
  }
}));

// 모든 채널로 전송
await snsClient.send(new PublishCommand({
  TopicArn: topicArn,
  Message: JSON.stringify({
    title: '긴급 알림',
    message: '시스템 점검 예정입니다'
  }),
  MessageAttributes: {
    channel: {
      DataType: 'String',
      StringValue: 'all'
    },
    severity: {
      DataType: 'String',
      StringValue: 'high'
    }
  }
}));

이메일 발송 Lambda:

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
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';

const sesClient = new SESClient({});

export const handler = async (event) => {
  for (const record of event.Records) {
    const message = JSON.parse(record.body);

    // HTML 이메일 템플릿 생성
    const htmlBody = `
      <div style="font-family: Arial, sans-serif;">
        <h2 style="color: #333;">${message.title}</h2>
        <p>${message.message}</p>
        <hr>
        <p style="color: #999; font-size: 12px;">
          발송 시간: ${new Date().toLocaleString('ko-KR')}
        </p>
      </div>
    `;

    await sesClient.send(new SendEmailCommand({
      Source: 'noreply@example.com',
      Destination: {
        ToAddresses: [message.email || 'default@example.com']
      },
      Message: {
        Subject: { Data: message.title },
        Body: {
          Html: { Data: htmlBody },
          Text: { Data: message.message }
        }
      }
    }));

    console.log(`Email sent for: ${message.title}`);
  }
};

SMS 발송 Lambda:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export const handler = async (event) => {
  for (const record of event.Records) {
    const message = JSON.parse(record.body);

    // SMS는 160자 제한
    const smsMessage = `[${message.title}] ${message.message}`.substring(0, 160);

    await snsClient.send(new PublishCommand({
      PhoneNumber: message.phoneNumber || '+821012345678',
      Message: smsMessage,
      MessageAttributes: {
        'AWS.SNS.SMS.SMSType': {
          DataType: 'String',
          StringValue: message.severity === 'high' ? 'Transactional' : 'Promotional'
        }
      }
    }));

    console.log(`SMS sent to: ${message.phoneNumber}`);
  }
};

사례 2: Lambda 직접 트리거

CloudWatch Alarm → SNS → Lambda 패턴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
resources:
  Resources:
    HighCPUAlarm:
      Type: AWS::CloudWatch::Alarm
      Properties:
        AlarmName: HighCPUUsage
        MetricName: CPUUtilization
        Namespace: AWS/EC2
        Statistic: Average
        Period: 300
        EvaluationPeriods: 2
        Threshold: 80
        ComparisonOperator: GreaterThanThreshold
        AlarmActions:
          - !Ref AlertTopic

functions:
  alertHandler:
    handler: handler.processAlarm
    events:
      - sns:
          arn: !Ref AlertTopic
          topicName: system-alerts

Lambda에서 알람 처리:

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
export const handler = async (event) => {
  for (const record of event.Records) {
    const snsMessage = JSON.parse(record.Sns.Message);

    // CloudWatch Alarm 메시지 파싱
    const alarmName = snsMessage.AlarmName;
    const newState = snsMessage.NewStateValue;
    const reason = snsMessage.NewStateReason;

    console.log(`Alarm: ${alarmName} is now ${newState}`);
    console.log(`Reason: ${reason}`);

    if (newState === 'ALARM') {
      // Slack으로 알림 전송
      await sendSlackNotification({
        title: `🚨 ${alarmName}`,
        message: reason,
        severity: 'critical'
      });

      // 자동 스케일링 트리거 등
      await triggerAutoScaling();
    }
  }
};

사례 3: Fanout 패턴 (SNS → SQS)

한 이벤트를 여러 큐로 분산:

1
2
3
4
5
6
7
S3 Upload Event
  ↓
SNS Topic
  ├─ Thumbnail Queue → Lambda (썸네일 생성)
  ├─ Metadata Queue → Lambda (메타데이터 추출)
  ├─ Virus Scan Queue → Lambda (바이러스 검사)
  └─ Analytics Queue → Lambda (통계 수집)

Serverless Framework 설정:

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
resources:
  Resources:
    S3EventTopic:
      Type: AWS::SNS::Topic
      Properties:
        TopicName: s3-upload-events

    ThumbnailQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: thumbnail-queue

    ThumbnailQueueSubscription:
      Type: AWS::SNS::Subscription
      Properties:
        TopicArn: !Ref S3EventTopic
        Protocol: sqs
        Endpoint: !GetAtt ThumbnailQueue.Arn
        RawMessageDelivery: true

    # SQS 정책 (SNS가 메시지 전송 허용)
    ThumbnailQueuePolicy:
      Type: AWS::SQS::QueuePolicy
      Properties:
        Queues:
          - !Ref ThumbnailQueue
        PolicyDocument:
          Statement:
            - Effect: Allow
              Principal:
                Service: sns.amazonaws.com
              Action: sqs:SendMessage
              Resource: !GetAtt ThumbnailQueue.Arn
              Condition:
                ArnEquals:
                  aws:SourceArn: !Ref S3EventTopic

functions:
  thumbnailGenerator:
    handler: thumbnail.handler
    events:
      - sqs:
          arn: !GetAtt ThumbnailQueue.Arn
          batchSize: 10

장점:

  • 각 처리 단계가 독립적으로 실패/재시도
  • 새로운 처리 로직 추가 시 구독만 추가하면 됨
  • 각 큐가 독립적으로 스케일링

SMS 전송 (SNS Direct)

국가별 전화번호 형식:

1
2
3
4
5
6
7
8
9
10
11
// 한국
await snsClient.send(new PublishCommand({
  PhoneNumber: '+821012345678',  // +82 (국가 코드) + 10-1234-5678
  Message: '인증 번호: 123456'
}));

// 미국
await snsClient.send(new PublishCommand({
  PhoneNumber: '+12025551234',  // +1 (국가 코드) + 202-555-1234
  Message: 'Verification code: 123456'
}));

SMS 타입:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Transactional (거래형) - 중요한 메시지, 비용 높음, 전달률 높음
await snsClient.send(new PublishCommand({
  PhoneNumber: '+821012345678',
  Message: '계좌 이체가 완료되었습니다',
  MessageAttributes: {
    'AWS.SNS.SMS.SMSType': {
      DataType: 'String',
      StringValue: 'Transactional'
    }
  }
}));

// Promotional (홍보형) - 마케팅 메시지, 비용 낮음
await snsClient.send(new PublishCommand({
  PhoneNumber: '+821012345678',
  Message: '할인 이벤트를 확인하세요!',
  MessageAttributes: {
    'AWS.SNS.SMS.SMSType': {
      DataType: 'String',
      StringValue: 'Promotional'
    }
  }
}));

SMS 설정:

1
2
3
4
5
6
7
8
9
10
import { SetSMSAttributesCommand } from '@aws-sdk/client-sns';

await snsClient.send(new SetSMSAttributesCommand({
  attributes: {
    DefaultSMSType: 'Transactional',  // 기본값 설정
    MonthlySpendLimit: '100',         // 월 지출 한도 ($100)
    DeliveryStatusIAMRole: 'arn:aws:iam::123456789:role/SNSSMSRole',  // 배달 상태 로깅
    DeliveryStatusSuccessSamplingRate: '100'  // 성공 샘플링 비율 100%
  }
}));

메시지 필터링

복잡한 필터 정책:

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
// 구독 생성 시 필터 정책 설정
await snsClient.send(new SubscribeCommand({
  TopicArn: topicArn,
  Protocol: 'sqs',
  Endpoint: queueArn,
  Attributes: {
    FilterPolicy: JSON.stringify({
      // 숫자 비교
      priority: [{ numeric: ['>=', 1] }],

      // 문자열 일치
      orderType: ['express', 'overnight'],

      // 문자열 접두사
      region: [{ prefix: 'ap-' }],

      // 존재 여부
      customerId: [{ exists: true }],

      // AND 조건 (모든 조건 만족)
      // OR 조건 (배열 내 하나라도 만족)
      severity: ['high', 'critical']
    })
  }
}));

필터 정책 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 고액 주문만 처리
{
  "totalAmount": [{ "numeric": [">=", 1000000] }]
}

// 특정 지역의 긴급 알림만
{
  "region": ["ap-northeast-2", "us-east-1"],
  "severity": ["critical"]
}

// 프리미엄 고객의 모든 주문
{
  "customerTier": ["premium", "vip"]
}

SNS FIFO Topics

순서 보장이 필요한 경우:

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
// FIFO Topic 생성 (.fifo 접미사 필수)
const response = await snsClient.send(new CreateTopicCommand({
  Name: 'order-events.fifo',
  Attributes: {
    FifoTopic: 'true',
    ContentBasedDeduplication: 'true'  // 메시지 내용으로 중복 제거
  }
}));

// FIFO SQS 구독
await snsClient.send(new SubscribeCommand({
  TopicArn: fifoTopicArn,
  Protocol: 'sqs',
  Endpoint: 'arn:aws:sqs:ap-northeast-2:123456789:orders.fifo'
}));

// 메시지 발행 (순서 보장)
await snsClient.send(new PublishCommand({
  TopicArn: fifoTopicArn,
  Message: JSON.stringify({
    orderId: 'order-123',
    status: 'created'
  }),
  MessageGroupId: 'order-123',  // 그룹별 순서 보장
  MessageDeduplicationId: `create-${Date.now()}`  // 중복 제거 ID
}));

await snsClient.send(new PublishCommand({
  TopicArn: fifoTopicArn,
  Message: JSON.stringify({
    orderId: 'order-123',
    status: 'paid'
  }),
  MessageGroupId: 'order-123',  // 같은 그룹 → 순서 보장
  MessageDeduplicationId: `pay-${Date.now()}`
}));

Standard vs FIFO:

특성Standard TopicFIFO Topic
순서보장 안 됨완벽 보장
중복가능정확히 1회 전달
처리량무제한300 msg/s (배치 시 3,000)
구독자모든 프로토콜SQS FIFO만

SNS 모니터링

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
resources:
  Resources:
    FailedNotificationsAlarm:
      Type: AWS::CloudWatch::Alarm
      Properties:
        AlarmName: SNSFailedNotifications
        MetricName: NumberOfNotificationsFailed
        Namespace: AWS/SNS
        Dimensions:
          - Name: TopicName
            Value: !GetAtt MyTopic.TopicName
        Statistic: Sum
        Period: 300
        EvaluationPeriods: 1
        Threshold: 10
        ComparisonOperator: GreaterThanThreshold
        AlarmActions:
          - !Ref AlertTopic

    SlowDeliveryAlarm:
      Type: AWS::CloudWatch::Alarm
      Properties:
        AlarmName: SNSSlowDelivery
        MetricName: NumberOfNotificationsDelivered
        Namespace: AWS/SNS
        Dimensions:
          - Name: TopicName
            Value: !GetAtt MyTopic.TopicName
        Statistic: Average
        Period: 60
        EvaluationPeriods: 5
        Threshold: 100
        ComparisonOperator: LessThanThreshold

전달 상태 로깅 (HTTP/Lambda):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
await snsClient.send(new CreateTopicCommand({
  Name: 'my-topic',
  Attributes: {
    // Lambda 전달 상태
    LambdaSuccessFeedbackRoleArn: 'arn:aws:iam::123456789:role/SNSFeedbackRole',
    LambdaSuccessFeedbackSampleRate: '100',
    LambdaFailureFeedbackRoleArn: 'arn:aws:iam::123456789:role/SNSFeedbackRole',

    // HTTP 전달 상태
    HTTPSuccessFeedbackRoleArn: 'arn:aws:iam::123456789:role/SNSFeedbackRole',
    HTTPSuccessFeedbackSampleRate: '100',
    HTTPFailureFeedbackRoleArn: 'arn:aws:iam::123456789:role/SNSFeedbackRole'
  }
}));

// CloudWatch Logs에 전달 성공/실패 로그 기록됨

Best Practices

1. DLQ (Dead Letter Queue) 설정

1
2
3
4
5
6
7
8
9
10
11
// SNS 구독에 DLQ 설정
await snsClient.send(new SubscribeCommand({
  TopicArn: topicArn,
  Protocol: 'lambda',
  Endpoint: lambdaArn,
  Attributes: {
    RedrivePolicy: JSON.stringify({
      deadLetterTargetArn: dlqArn
    })
  }
}));

2. 재시도 정책 커스터마이징

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
await snsClient.send(new SetTopicAttributesCommand({
  TopicArn: topicArn,
  AttributeName: 'DeliveryPolicy',
  AttributeValue: JSON.stringify({
    http: {
      defaultHealthyRetryPolicy: {
        minDelayTarget: 20,      // 최소 재시도 간격 (초)
        maxDelayTarget: 600,     // 최대 재시도 간격 (초)
        numRetries: 5,           // 재시도 횟수
        backoffFunction: 'exponential'  // 지수 백오프
      },
      disableSubscriptionOverrides: false
    }
  })
}));

3. 메시지 크기 최적화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ 큰 데이터를 직접 전송 (256KB 제한)
await snsClient.send(new PublishCommand({
  TopicArn: topicArn,
  Message: JSON.stringify({
    data: largeObject  // 너무 큼
  })
}));

// ✅ S3 참조만 전송
const s3Key = `notifications/${notificationId}.json`;
await s3Client.send(new PutObjectCommand({
  Bucket: 'notification-data',
  Key: s3Key,
  Body: JSON.stringify(largeObject)
}));

await snsClient.send(new PublishCommand({
  TopicArn: topicArn,
  Message: JSON.stringify({
    notificationId,
    s3Bucket: 'notification-data',
    s3Key: s3Key
  })
}));

4. 이메일 구독 자동 확인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 구독 확인 토큰 자동 처리
export const handler = async (event) => {
  for (const record of event.Records) {
    const message = JSON.parse(record.Sns.Message);

    if (record.Sns.Type === 'SubscriptionConfirmation') {
      // 구독 확인 URL 호출
      const confirmUrl = message.SubscribeURL;
      await axios.get(confirmUrl);
      console.log('Subscription confirmed');
    } else if (record.Sns.Type === 'Notification') {
      // 실제 알림 처리
      await processNotification(message);
    }
  }
};

SNS 비용 최적화

비용 구조:

1
2
3
4
5
6
7
8
9
10
11
12
발행:
- 처음 100만 요청/월: 무료
- 이후: $0.50 per million publishes

이메일/HTTP/Lambda/SQS 전달:
- 무료

SMS:
- 국가별 상이 (한국: ~$0.06/건)

모바일 푸시:
- 무료

최적화 전략:

  1. 필터 정책 활용: 불필요한 메시지 전달 방지
  2. 배치 발행: 여러 메시지를 하나로 묶어 발행
  3. SMS 대신 푸시/이메일: SMS는 비용 높음
  4. SQS Fanout: SNS → SQS → Lambda (재시도 제어 가능)

마치며

Amazon SNS는 확장 가능한 알림 시스템 구축의 핵심임.

SNS를 사용해야 할 때:

  • 여러 구독자에게 동시 알림
  • 다양한 채널로 메시지 전송
  • 이벤트 기반 아키텍처
  • Fanout 패턴 구현
  • 시스템 간 느슨한 결합

핵심 패턴:

  1. Fanout: SNS → 여러 SQS → 독립적 처리
  2. 필터링: MessageAttributes + FilterPolicy
  3. 멀티 채널: 이메일, SMS, Lambda, HTTP 동시 지원
  4. 이벤트 라우팅: CloudWatch Alarm → SNS → Lambda

실전 프로젝트에서는 SNS를 SQS(큐잉), Lambda(처리), SES(이메일)와 조합하여 확장 가능한 알림 시스템을 구축할 수 있음.

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

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