coredot.today
AWS SQS·SNS 완전 정복: 시스템 사이에 '메시지'를 보내는 기술
블로그로 돌아가기
SQSSNSAWS메시징비동기마이크로서비스이벤트

AWS SQS·SNS 완전 정복: 시스템 사이에 '메시지'를 보내는 기술

배달앱에서 주문 버튼을 누르면 뒤에서 수십 개의 시스템이 동시에 움직인다. 이 시스템들을 연결하는 '메시지 큐'와 '이벤트 알림'의 세계 — SQS와 SNS를 카페 주문 비유부터 실전 아키텍처까지 완전히 풀어본다.

코어닷투데이2026-02-0877

들어가며: 배달앱에서 주문하면 뒤에서 무슨 일이?

배달 앱에서 "주문하기" 버튼을 누르는 건 0.3초의 일이다. 하지만 그 뒤에서는 수십 개의 시스템이 동시에 움직여야 한다.

STEP 1 주문 서비스가 주문 정보를 저장한다
STEP 2 결제 서비스가 카드 승인을 처리한다
STEP 3 재고 서비스가 메뉴 재고를 차감한다
STEP 4 알림 서비스가 가게에 새 주문 알림을 보낸다
STEP 5 배송 서비스가 근처 라이더를 매칭한다

자, 여기서 질문이 생긴다. 주문 서비스가 나머지 4개 서비스를 직접 하나하나 호출하면 어떻게 될까?

동기(Synchronous) 방식의 문제

"동기"란 전화 통화와 같다. 내가 상대방을 부르면, 상대방이 응답할 때까지 기다려야 한다.

주문 서비스
결제 서비스 (2초)
재고 서비스 (0.5초)
알림 서비스 (1초)
배송 서비스 (1.5초)

5초. 사용자는 주문 버튼을 누른 뒤 5초 동안 화면이 멈춘 채로 기다려야 한다. 그런데 결제 서비스가 장애를 일으키면? 뒤에 있는 재고·알림·배송 서비스는 호출조차 되지 않는다. 하나가 죽으면 전체가 죽는 구조다.

5초 총 응답 시간 순차 처리의 합
100% 장애 전파율 하나 죽으면 전체 중단
강결합 서비스 의존성 모든 서비스가 서로를 알아야 함

비동기(Asynchronous) 방식이라는 대안

"비동기"는 문자 메시지와 같다. 내가 메시지를 보내놓고 내 할 일을 하면 된다. 상대방은 자기 시간에 맞춰 메시지를 읽고 처리한다.

주문 서비스가 "주문 완료됐어!"라는 메시지를 큐에 던져놓고 즉시 사용자에게 "주문이 접수되었습니다"를 보여준다. 결제, 재고, 알림, 배송 서비스는 각자 알아서 큐에서 메시지를 꺼내 처리한다. 사용자 응답 시간은 0.3초로 줄어든다. 결제 서비스가 잠시 멈춰도 다른 서비스에는 아무 영향이 없다.

이 "메시지를 중간에서 전달해 주는 시스템"이 바로 오늘의 주인공, 메시지 큐(Message Queue)이벤트 알림(Pub/Sub) 이다.


1. 메시지 큐의 개념: 카페에서 주문하기

카페에서 번호표를 뽑고 순서대로 음료를 받는 한국 카페 주문 시스템으로 메시지 큐를 설명하는 일러스트

카페 비유로 이해하기

메시지 큐를 가장 쉽게 이해하는 방법은 카페 주문 시스템을 떠올리는 것이다.

카페에 갔다고 생각해 보자.

주문 손님(Producer)이 카운터에 "아이스 아메리카노 한 잔"이라고 주문한다
대기 주문표가 주문 대기판(Queue)에 꽂힌다. 손님은 자리에 가서 앉는다
처리 바리스타(Consumer)가 주문표를 하나씩 뽑아 커피를 만든다

핵심은 이것이다:

  • 손님은 커피가 나올 때까지 카운터 앞에 서 있지 않는다 (비동기)
  • 주문이 많아지면 대기판에 주문표가 쌓인다 (버퍼링)
  • 바리스타가 한 명이든 세 명이든, 주문표는 하나씩 처리된다 (스케일링)
  • 바리스타가 잠시 쉬어도 주문표는 사라지지 않는다 (내구성)

이 구조를 소프트웨어로 만든 것이 메시지 큐다.

Producer → Queue → Consumer

메시지 큐 기본 구조
Producer 메시지를 보내는 쪽
Message Queue 메시지가 순서대로 쌓이는 곳
Consumer 메시지를 꺼내서 처리하는 쪽

이 패턴에서 Producer와 Consumer는 서로를 모른다. 그저 Queue만 알면 된다. 이것이 느슨한 결합(Loose Coupling) 이다. 마치 손님이 바리스타의 이름을 모르고, 바리스타도 손님의 이름을 모르지만 — 주문 대기판이라는 중간 매개체를 통해 완벽하게 소통하는 것과 같다.

💡
왜 '느슨한 결합'이 중요한가? 서비스 A가 서비스 B를 직접 호출하면, B의 API가 바뀔 때마다 A도 수정해야 한다. 하지만 메시지 큐를 사이에 두면, 메시지 형식만 맞으면 A와 B는 독립적으로 배포·확장·변경할 수 있다. 마이크로서비스 아키텍처의 핵심 원칙이다.

2. Amazon SQS: AWS의 메시지 큐

SQS란?

SQS(Simple Queue Service) 는 AWS가 제공하는 완전관리형 메시지 큐 서비스다. 2004년에 출시된 AWS의 초창기 서비스 중 하나로, S3(2006)보다도 먼저 나왔다. 서버를 관리할 필요 없이 메시지를 보내고 받을 수 있다.

2004 출시 연도 AWS 최초의 서비스 중 하나
무제한 처리량 초당 메시지 수 제한 없음
256KB 최대 메시지 크기 큰 데이터는 S3 참조
14일 최대 보존 기간 기본값 4일

Standard Queue vs FIFO Queue

SQS에는 두 가지 타입의 큐가 있다. 이 둘의 차이를 이해하는 것이 SQS 사용의 첫걸음이다.

Standard Queue는 말 그대로 "일반" 큐다. 카페에서 주문이 많을 때 바리스타 여러 명이 동시에 주문표를 뽑아 처리하는 것과 같다. 처리 속도가 엄청나게 빠르지만, 가끔 순서가 뒤바뀔 수 있다. 아이스 아메리카노를 먼저 주문했는데 뒤에 주문한 에스프레소가 먼저 나올 수 있는 것이다.

FIFO Queue는 "선입선출(First-In, First-Out)"을 보장한다. 은행 번호표처럼 정확히 순서대로 처리된다. 대신 속도 제한이 있다.

특성Standard QueueFIFO Queue
순서 보장최선 노력 (best-effort)엄격한 순서 보장
중복 배달가능 (at-least-once)정확히 한 번 (exactly-once)
처리량무제한 (초당 수만~수백만)초당 3,000건 (배치 시 최대 30,000)
가격요청 백만 건당 $0.40요청 백만 건당 $0.50
사용 사례로그 수집, 이미지 처리, 이메일 발송결제 처리, 주문 순서, 티켓 예매
💡
어떤 큐를 선택해야 할까? 대부분의 경우 Standard Queue로 충분하다. FIFO Queue는 "순서가 정말로 중요한" 경우에만 사용한다. 예를 들어 은행 계좌에 입금 → 출금 → 입금 순서로 트랜잭션이 발생했는데, 이 순서가 바뀌면 잔액이 달라질 수 있다. 이런 경우에 FIFO Queue를 쓴다.

Visibility Timeout: "이 메시지는 내가 처리 중이야"

SQS의 동작 방식에서 가장 중요한 개념이 Visibility Timeout이다.

카페 비유로 돌아가 보자. 바리스타가 주문표를 뽑으면, 그 주문표는 대기판에서 잠시 사라진다. 다른 바리스타가 같은 주문을 또 뽑지 않도록 하기 위해서다. 커피를 다 만들면 주문표를 버린다(삭제). 하지만 바리스타가 10분 안에 커피를 못 만들면? 주문표가 다시 대기판에 나타나서 다른 바리스타가 처리할 수 있게 된다.

수신 Consumer가 큐에서 메시지를 가져간다 (ReceiveMessage)
처리 중 메시지가 다른 Consumer에게 보이지 않는다 (Visibility Timeout 시작, 기본 30초)
완료 처리 완료 후 Consumer가 메시지를 삭제한다 (DeleteMessage)

만약 Consumer가 Visibility Timeout 안에 메시지를 삭제하지 못하면? 메시지가 다시 큐에 나타나서 다른 Consumer가 가져갈 수 있다. 이것이 SQS의 자동 재시도(retry) 메커니즘이다.

VISIBILITY TIMEOUT 시나리오
정상 시나리오
Consumer가 메시지 수신 → 15초 만에 처리 완료 → DeleteMessage 호출 → 메시지 영구 삭제
타임아웃 시나리오
Consumer가 메시지 수신 → 처리 중 장애 발생 → 30초(기본값) 경과 → 메시지가 큐에 다시 나타남 → 다른 Consumer가 재처리
Visibility Timeout은 "실제 처리 시간의 6배"로 설정하는 것이 권장된다. 처리에 5초 걸리면 → 30초로 설정.

Dead Letter Queue: 실패한 메시지의 무덤

어떤 메시지는 아무리 재시도해도 처리할 수 없다. JSON 형식이 깨졌다거나, 참조하는 데이터가 존재하지 않는다거나. 이런 메시지가 큐에 계속 남아서 재시도되면, Consumer의 자원만 낭비하게 된다.

이때 사용하는 것이 DLQ(Dead Letter Queue) 다.

!
문제
잘못된 형식의 메시지가 큐에 들어왔다. Consumer가 처리할 때마다 에러가 발생하고, 메시지가 다시 큐로 돌아온다. 이것이 무한 반복된다.
해결
maxReceiveCount를 설정한다. "이 메시지가 3번 처리에 실패하면 DLQ로 보내라." DLQ는 별도의 SQS 큐로, 실패한 메시지만 모아둔다.
결과
메인 큐는 정상 메시지만 처리하고, 개발자는 DLQ를 모니터링하면서 실패 원인을 분석할 수 있다. CloudWatch 알림을 걸어두면 DLQ에 메시지가 쌓일 때 즉시 알 수 있다.

DLQ 설정은 간단하다:

hljs language-json
{
  "RedrivePolicy": {
    "deadLetterTargetArn": "arn:aws:sqs:ap-northeast-2:123456789:order-dlq",
    "maxReceiveCount": 3
  }
}

maxReceiveCount: 3은 "3번 수신(=3번 처리 실패)하면 DLQ로 보내라"는 뜻이다.


3. Amazon SNS: 이벤트를 여러 곳에 동시에 알리기

신문 배달부가 스쿠터를 타고 여러 아파트 단지에 동시에 같은 신문을 배달하는 SNS 팬아웃 패턴 일러스트

SQS와 SNS의 차이

SQS가 "한 명에게 메시지를 전달하는 우편함" 이라면, SNS는 "여러 명에게 동시에 알리는 방송 시스템" 이다.

카페 비유를 바꿔보자. SQS는 "바리스타에게 주문표를 전달"하는 것이다. 한 장의 주문표는 한 명의 바리스타가 처리한다.

SNS는 "카페 전체에 마이크를 잡고 공지"하는 것이다. "오늘 신메뉴 출시!" 하고 방송하면, 바리스타도 듣고, 캐셔도 듣고, 매니저도 듣는다. 같은 메시지를 여러 수신자가 동시에 받는다.

SQS (Point-to-Point) vs SNS (Pub/Sub)
SQS 1:1 전달
SNS 1:N 전달

Pub/Sub 패턴이란?

SNS는 Pub/Sub(Publish/Subscribe) 패턴을 구현한 서비스다.

  • Publisher(발행자): 메시지를 보내는 쪽. "이런 이벤트가 발생했어!"
  • Topic(주제): 메시지가 모이는 채널. 뉴스 카테고리 같은 것
  • Subscriber(구독자): 특정 Topic을 구독하는 쪽. 새 메시지가 오면 자동으로 받는다
Publisher
SNS Topic
이메일
SMS
SQS
Lambda
HTTP

유튜브 구독을 생각하면 쉽다. 유튜버(Publisher)가 영상을 업로드(Publish)하면, 구독자(Subscriber)들에게 자동으로 알림이 간다. 구독자가 100명이든 100만 명이든, 유튜버는 영상을 한 번만 올리면 된다.

SNS가 지원하는 구독 유형

SNS Topic에 구독할 수 있는 엔드포인트의 종류는 다양하다:

구독 유형설명사용 사례
SQSSQS 큐로 메시지 전달비동기 처리 파이프라인
LambdaLambda 함수 직접 호출이벤트 기반 서버리스 처리
HTTP/HTTPS웹훅 URL로 POST 요청외부 서비스 연동
이메일이메일 주소로 알림 발송장애 알림, 보고서
SMS휴대폰 문자 메시지 발송긴급 알림, OTP 발송
모바일 푸시iOS/Android 푸시 알림앱 알림

메시지 필터링

SNS의 강력한 기능 중 하나가 메시지 필터링(Message Filtering) 이다.

모든 구독자가 모든 메시지를 받을 필요는 없다. 예를 들어 이커머스에서 "주문 완료" 이벤트가 발생하면:

  • 결제 서비스: 모든 주문을 받아야 한다
  • VIP 관리 서비스: VIP 고객의 주문만 받으면 된다
  • 해외배송 서비스: 해외 배송 주문만 받으면 된다

SNS 필터 정책을 사용하면, 구독자별로 원하는 메시지만 받을 수 있다:

hljs language-json
{
  "customer_type": ["vip"],
  "order_total": [{"numeric": [">=", 100000]}]
}

이 필터는 "VIP 고객이고 주문 금액이 10만 원 이상인 경우에만 메시지를 받겠다"는 뜻이다. 필터링은 SNS 내부에서 처리되므로, 구독자가 불필요한 메시지를 받아서 버리는 비용이 사라진다.


4. SQS + SNS 결합: Fan-out 패턴

Fan-out이란?

SQS와 SNS를 각각 이해했으니, 이제 이 둘을 결합하는 강력한 패턴을 알아보자. 이것이 실전에서 가장 많이 쓰이는 Fan-out 패턴이다.

"Fan-out"은 부채가 펼쳐지는 모양에서 따온 이름이다. 하나의 메시지가 여러 갈래로 퍼져 나간다.

SNS + SQS FAN-OUT 패턴
주문 서비스 Publisher
SNS Topic order-events
결제 큐 SQS
재고 큐 SQS
알림 큐 SQS
분석 큐 SQS

왜 SNS에서 바로 처리하지 않고 SQS를 끼울까?

좋은 질문이다. SNS에서 Lambda를 직접 호출해도 되는데, 왜 굳이 중간에 SQS를 두는 걸까?

!
SNS → Lambda 직접 호출의 문제
SNS가 Lambda를 직접 호출하면, Lambda가 실패했을 때 SNS가 최대 3번 재시도한 후 메시지를 버린다. 메시지가 영구적으로 사라진다.
SNS → SQS → Lambda의 장점
SQS를 중간에 두면 메시지가 큐에 안전하게 보관된다. Consumer가 실패해도 메시지는 사라지지 않는다. Visibility Timeout 후에 다시 처리 가능하고, 최종 실패 시 DLQ로 이동해서 나중에 분석할 수 있다.
추가 이점
버퍼링 효과도 있다. 트래픽이 갑자기 10배 증가해도 SQS가 메시지를 쌓아두고, Consumer가 자기 속도에 맞게 처리할 수 있다. Lambda 동시 실행 한도(concurrency limit)를 초과하지 않는다.

실전 코드: Node.js로 Fan-out 구현하기

hljs language-javascript
// 1. SNS에 메시지 발행 (주문 서비스)
import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";

const sns = new SNSClient({ region: "ap-northeast-2" });

await sns.send(new PublishCommand({
  TopicArn: "arn:aws:sns:ap-northeast-2:123456789:order-events",
  Message: JSON.stringify({
    orderId: "ORD-20260401-001",
    userId: "user-123",
    items: [{ name: "아이스 아메리카노", qty: 2, price: 4500 }],
    totalAmount: 9000,
    timestamp: "2026-04-01T10:30:00Z"
  }),
  MessageAttributes: {
    orderType: { DataType: "String", StringValue: "food" },
    region: { DataType: "String", StringValue: "seoul" }
  }
}));
hljs language-javascript
// 2. SQS에서 메시지 소비 (결제 서비스 — Lambda)
export const handler = async (event) => {
  for (const record of event.Records) {
    const message = JSON.parse(record.body);
    const order = JSON.parse(message.Message); // SNS 메시지 unwrap

    console.log(`결제 처리 시작: ${order.orderId}`);
    await processPayment(order);
    console.log(`결제 처리 완료: ${order.orderId}`);
  }
};

주문 서비스는 SNS에 메시지를 한 번만 발행한다. 결제, 재고, 알림, 분석 — 각 서비스의 SQS 큐에 메시지가 자동으로 복사된다.


5. EventBridge vs SQS vs SNS: 언제 무엇을 쓸까?

AWS에는 메시징/이벤트 서비스가 여러 개다. 처음에는 헷갈릴 수 있다. 언제 무엇을 써야 하는지 정리해 보자.

Amazon EventBridge란?

EventBridge는 2019년에 출시된 서비스로, SNS의 진화된 버전이라고 생각할 수 있다. 가장 큰 차이점은 이벤트 패턴 매칭이다.

SNS의 메시지 필터링은 메시지 속성(MessageAttributes)에 대한 단순 필터링만 지원한다. 반면 EventBridge는 메시지 본문(body) 안의 JSON 필드까지 패턴 매칭할 수 있다.

hljs language-json
{
  "source": ["com.myapp.orders"],
  "detail-type": ["OrderCreated"],
  "detail": {
    "totalAmount": [{"numeric": [">", 50000]}],
    "items": {
      "category": ["electronics"]
    }
  }
}

"주문 생성 이벤트 중, 금액이 5만 원 이상이고 카테고리가 전자제품인 것만 라우팅해라." EventBridge는 이런 복잡한 조건을 지원한다.

세 서비스 비교

특성SQSSNSEventBridge
패턴Point-to-Point (1:1)Pub/Sub (1:N)Event Bus (N:N)
메시지 보존최대 14일 보관보관 안 함 (즉시 전달)아카이브로 재생 가능
필터링없음메시지 속성 기반JSON 본문까지 패턴 매칭
처리량무제한무제한초당 수천~수만 이벤트
외부 연동AWS 내부만HTTP/이메일/SMSSalesforce, Zendesk 등 SaaS
가격가장 저렴중간이벤트당 $1/백만 건
스케줄링없음없음크론 표현식 지원

선택 가이드

SQS vs SNS vs EVENTBRIDGE 선택 기준
SQS를 선택할 때
Consumer가 자기 속도에 맞게 처리해야 할 때. 버퍼링이 필요할 때. 처리 실패 시 재시도가 중요할 때. "작업 큐"가 필요할 때.
SNS를 선택할 때
하나의 이벤트를 여러 서비스에 동시에 전달할 때. Fan-out이 필요할 때. 이메일/SMS 알림을 보낼 때.
EventBridge를 선택할 때
복잡한 이벤트 라우팅이 필요할 때. 외부 SaaS와 연동할 때. 스케줄 기반 이벤트를 처리할 때. 이벤트를 아카이빙하고 재생해야 할 때.

실무에서는 이 세 가지를 함께 쓰는 경우가 많다. EventBridge가 이벤트를 라우팅하고, SNS가 Fan-out으로 퍼뜨리고, SQS가 각 서비스의 버퍼 역할을 한다.


6. 실전 아키텍처: 이커머스 주문 처리 파이프라인

지금까지 배운 것들을 모아서, 실제 이커머스 서비스의 주문 처리 파이프라인을 설계해 보자.

전체 아키텍처

이커머스 주문 처리 파이프라인
사용자 주문하기 버튼 클릭
API Gateway 요청 수신 + 인증
주문 Lambda 주문 저장 + SNS 발행
SNS: order-events Fan-out
결제 큐 FIFO
재고 큐 Standard
알림 큐 Standard
분석 큐 Standard

각 큐의 역할

왜 결제만 FIFO Queue를 사용하고, 나머지는 Standard Queue를 사용할까?

  • 결제 큐(FIFO): 같은 주문에 대해 "결제 요청 → 결제 확인 → 환불" 순서가 뒤바뀌면 안 된다. 순서가 중요하므로 FIFO를 사용한다
  • 재고 큐(Standard): 재고 차감은 순서보다 속도가 중요하다. 품절을 빨리 감지해야 하므로 처리량이 높은 Standard를 사용한다
  • 알림 큐(Standard): 고객에게 "주문 접수됐습니다" 알림을 보내는 건 순서가 중요하지 않다. 빠르게 보내기만 하면 된다
  • 분석 큐(Standard): 주문 데이터를 데이터 웨어하우스에 적재하는 건 실시간이 아니어도 된다. 약간의 지연이나 중복은 허용된다

주문 흐름 상세

1. 주문 사용자가 주문 → 주문 Lambda가 DynamoDB에 주문 저장 (상태: PENDING) → SNS에 OrderCreated 이벤트 발행 → 사용자에게 "주문 접수" 응답 (200ms)
2. 결제 결제 큐 → 결제 Lambda가 PG사 API 호출 → 성공 시 SNS에 PaymentCompleted 이벤트 발행 → 주문 상태를 PAID로 업데이트
3. 재고 재고 큐 → 재고 Lambda가 재고 DB에서 수량 차감 → 재고 부족 시 SNS에 StockLow 이벤트 발행
4. 알림 알림 큐 → 알림 Lambda가 고객에게 푸시 알림 + 가게에 주문 알림 전송
5. 분석 분석 큐 → 분석 Lambda가 Redshift/S3에 주문 데이터 적재 → 실시간 대시보드 업데이트

핵심은 각 단계가 독립적이라는 것이다. 알림 서비스가 장애를 일으켜도 결제는 정상적으로 처리된다. 분석 서비스가 느려도 사용자에게는 아무 영향이 없다. 이것이 메시징 기반 비동기 아키텍처의 힘이다.


7. 에러 처리: DLQ, 재시도 전략, 멱등성

우체국에서 반송된 편지와 배달 실패 택배가 쌓인 DLQ 처리 대기 선반을 보여주는 일러스트

비동기 시스템에서 가장 어려운 부분은 에러 처리다. 동기 시스템에서는 에러가 바로 호출한 쪽으로 돌아오지만, 비동기 시스템에서는 메시지가 큐에 있으므로 에러를 "누가, 언제, 어떻게" 처리할지 명확히 정해야 한다.

재시도 전략

전략설명사용 사례예시
즉시 재시도실패 후 바로 다시 시도네트워크 순간 끊김0초 → 재시도
고정 간격일정 시간 후 재시도외부 API 일시 장애5초 → 5초 → 5초
지수 백오프간격을 점점 늘려 재시도권장 기본 전략1초 → 2초 → 4초 → 8초
지수 백오프 + 지터간격에 랜덤 변동 추가대규모 시스템 권장1.3초 → 2.7초 → 5.1초

지수 백오프(Exponential Backoff) 가 권장되는 이유는 간단하다. 서버가 과부하로 실패했는데, 수천 개의 클라이언트가 동시에 즉시 재시도하면 서버 부하가 더 심해진다. 간격을 점점 늘리면 서버에 회복할 시간을 준다.

지터(Jitter) 는 여기에 약간의 랜덤성을 추가한다. 1,000개의 메시지가 정확히 같은 시간에 재시도하면 또 다시 과부하가 걸린다. 각각 조금씩 다른 시간에 재시도하게 하는 것이다.

멱등성(Idempotency): 같은 메시지를 두 번 처리해도 괜찮게 만들기

SQS Standard Queue는 "최소 한 번(at-least-once)" 전달을 보장한다. 이 말은 같은 메시지가 두 번 배달될 수 있다는 뜻이다.

배달 앱으로 치면, 같은 주문이 두 번 결제되는 상황이다. 이건 큰 문제다.

!
중복 처리의 위험
"아이스 아메리카노 2잔, 9,000원" 결제 메시지가 두 번 처리되면 18,000원이 결제된다. 재고도 두 배로 차감되고, 알림도 두 번 간다.
멱등성 키(Idempotency Key)
각 메시지에 고유한 ID를 부여한다. 처리 전에 "이 ID를 이미 처리했는가?"를 확인한다. DynamoDB의 조건부 쓰기Redis의 SET NX로 구현할 수 있다.
결과
같은 메시지가 100번 처리되어도 결과는 1번 처리한 것과 동일하다. 이것이 멱등성이다. f(f(x)) = f(x).

멱등성 구현 코드 예시:

hljs language-javascript
// DynamoDB를 사용한 멱등성 체크
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";

const dynamodb = new DynamoDBClient({ region: "ap-northeast-2" });

async function processWithIdempotency(orderId, messageId, processFn) {
  try {
    // 1. 멱등성 키 저장 시도 (조건부 쓰기)
    await dynamodb.send(new PutItemCommand({
      TableName: "idempotency-keys",
      Item: {
        pk: { S: `payment#${orderId}` },
        messageId: { S: messageId },
        processedAt: { S: new Date().toISOString() },
        ttl: { N: String(Math.floor(Date.now() / 1000) + 86400) } // 24시간 후 만료
      },
      ConditionExpression: "attribute_not_exists(pk)" // 이미 존재하면 실패
    }));

    // 2. 키가 새로 저장되었으면 → 처음 처리하는 메시지
    await processFn();

  } catch (error) {
    if (error.name === "ConditionalCheckFailedException") {
      console.log(`이미 처리된 메시지입니다: ${orderId}`);
      return; // 중복이므로 무시
    }
    throw error; // 진짜 에러는 다시 던진다
  }
}

DLQ 모니터링 파이프라인

DLQ에 메시지가 쌓이면 누군가 알아야 한다. 실전에서는 이런 파이프라인을 구축한다:

DLQ에 메시지 도착
CloudWatch Alarm
SNS Topic (알림)
Slack 알림
이메일
PagerDuty

CloudWatch에서 DLQ의 ApproximateNumberOfMessagesVisible 지표를 모니터링하다가, 0보다 커지면 알람을 울리는 구조다. 여기서도 SNS가 쓰인다 — 알림을 여러 채널로 동시에 보내는 Fan-out 패턴이다.


8. 비용과 스케일링

비용: 쓴 만큼만 낸다

SQS와 SNS의 가장 큰 장점 중 하나는 완전한 종량제 모델이다. 서버를 미리 띄워놓고 돈을 내는 게 아니라, 메시지를 보내고 받은 만큼만 비용이 발생한다.

$0 기본 요금 대기 중에는 비용 없음
$0.40 SQS 100만 건당 Standard Queue 기준
$0.50 SNS 100만 건당 HTTP/SQS/Lambda 전달
100만 무료 티어 SQS·SNS 각각 매월

실제 비용을 계산해 보자. 하루 10만 건의 주문을 처리하는 이커머스 서비스가 있다고 하자.

월간 비용 계산 (하루 100,000 주문)
SNS 비용
100,000건/일 x 30일 = 3,000,000건/월
무료 티어 1,000,000건 제외 = 2,000,000건
2 x $0.50 = $1.00/월
SQS 비용 (큐 4개)
큐당 3,000,000건/월 x 4개 큐 = 12,000,000건/월
무료 티어 1,000,000건 제외 = 11,000,000건
11 x $0.40 = $4.40/월
합계
SNS $1.00 + SQS $4.40 = 총 $5.40/월 (약 7,200원)
하루 10만 건 주문 처리 비용이 커피 한 잔 값보다 싸다.

물론 실제로는 각 메시지당 ReceiveMessage, DeleteMessage 등 여러 API 호출이 발생하므로 실제 비용은 이보다 높다. 하지만 Long Polling을 사용하면 빈 응답을 줄여 비용을 최적화할 수 있다.

Long Polling vs Short Polling

SQS에서 메시지를 가져오는 방식에는 두 가지가 있다.

Short Polling(기본값): Consumer가 "메시지 있어?" 하고 물어본다. 큐가 비어있으면 즉시 "없어"라고 응답한다. 이 빈 응답도 API 호출 1회로 과금된다.

Long Polling: Consumer가 "메시지 있어? 있을 때까지 최대 20초 기다릴게" 하고 물어본다. 20초 안에 메시지가 오면 바로 응답하고, 없으면 20초 후에 "없어"라고 응답한다.

방식Short PollingLong Polling
빈 응답많음 (비용 낭비)거의 없음
응답 지연즉시 (0ms)최대 WaitTimeSeconds
비용높음낮음
설정WaitTimeSeconds = 0WaitTimeSeconds = 20 (권장)
💡
항상 Long Polling을 사용하라. Short Polling을 쓸 이유가 거의 없다. Long Polling은 빈 응답을 줄여 비용을 절감하고, 메시지가 도착하면 즉시 전달하므로 지연도 거의 없다. SQS 큐 생성 시 ReceiveMessageWaitTimeSeconds를 20으로 설정하면 된다.

Auto Scaling: 트래픽에 맞게 자동 확장

메시지 큐의 가장 강력한 장점은 Consumer를 독립적으로 확장할 수 있다는 것이다.

평소에는 Consumer 2개가 메시지를 처리한다. 블랙프라이데이에 트래픽이 10배가 되면? SQS에 메시지가 쌓이기 시작한다. CloudWatch가 큐의 ApproximateNumberOfMessagesVisible을 모니터링하다가, 특정 임계값을 넘으면 Auto Scaling이 Consumer를 20개로 늘린다. 트래픽이 줄어들면 다시 2개로 줄인다.

시간대별 SQS 큐 깊이 & Consumer 수
00시~06시
Consumer 1
06시~09시
Consumer 2
09시~12시
Consumer 5
12시~13시
Consumer 10
13시~18시
Consumer 4
18시~21시
Consumer 9
21시~00시
Consumer 3

점심 시간(12시13시)과 저녁 시간(18시21시)에 주문이 몰린다. Consumer가 자동으로 확장되어 큐가 밀리지 않게 처리한다. 새벽에는 Consumer 1개로 충분하다. 이것이 메시지 큐 + Auto Scaling의 시너지다.

Producer(주문 서비스)는 트래픽이 몇 배가 되든 그냥 큐에 메시지를 넣기만 하면 된다. 처리 속도 조절은 Consumer 쪽에서 독립적으로 한다. 이 "관심사의 분리"가 대규모 시스템 설계의 핵심이다.


9. 실전 팁과 주의사항

메시지 크기 제한을 넘을 때

SQS의 메시지 크기 제한은 256KB다. 이미지나 대용량 데이터를 보내야 한다면?

Producer
S3에 데이터 저장
SQS에 S3 URL만 전송
Consumer가 S3에서 다운로드

이 패턴을 Claim-Check 패턴이라고 한다. 옷가게에서 코트를 맡기면 번호표(claim check)만 받는 것과 같다. 메시지에는 가벼운 "참조"만 넣고, 실제 데이터는 S3에 보관한다.

AWS는 이 패턴을 위해 Amazon SQS Extended Client Library를 제공한다. 메시지가 256KB를 넘으면 자동으로 S3에 저장하고, Consumer가 받을 때 자동으로 S3에서 다운로드한다.

FIFO Queue의 MessageGroupId

FIFO Queue에서 순서를 보장하려면 MessageGroupId를 올바르게 설정해야 한다.

같은 MessageGroupId를 가진 메시지끼리만 순서가 보장된다. 예를 들어:

hljs language-javascript
// 주문 ID를 MessageGroupId로 사용
await sqs.send(new SendMessageCommand({
  QueueUrl: "https://sqs.ap-northeast-2.amazonaws.com/123456789/orders.fifo",
  MessageBody: JSON.stringify(orderEvent),
  MessageGroupId: "order-ORD-20260401-001", // 같은 주문의 이벤트끼리 순서 보장
  MessageDeduplicationId: `${orderId}-${eventType}-${timestamp}` // 중복 방지 ID
}));

서로 다른 주문은 병렬로 처리될 수 있다. 주문 A의 이벤트와 주문 B의 이벤트 사이에는 순서가 없어도 된다. 하지만 주문 A 안에서는 "생성 → 결제 → 배송" 순서가 지켜져야 한다. MessageGroupId가 이것을 가능하게 한다.

배치 처리로 비용 절감

SQS는 한 번에 최대 10개의 메시지를 보내거나 받을 수 있다. 개별 API 호출 대비 최대 10배 비용을 절감할 수 있다.

방식개별 처리배치 처리
10개 메시지 전송API 호출 10회API 호출 1회
100만 건 전송 비용$0.40$0.04
네트워크 오버헤드높음낮음

Lambda를 SQS Consumer로 사용할 때, Lambda는 자동으로 배치 처리를 한다. event.Records에 최대 10개(설정에 따라 최대 10,000개)의 메시지가 한꺼번에 들어온다.


마치며: 메시지가 시스템을 살린다

지금까지 SQS, SNS, 그리고 이 둘을 결합하는 패턴을 살펴봤다. 핵심을 정리하면 이렇다.

SQS 메시지 큐 1:1, 버퍼링, 재시도
SNS 이벤트 알림 1:N, Fan-out, 실시간
Fan-out SNS + SQS 실전에서 가장 많이 쓰는 패턴

비동기 메시징은 단순히 "빠르게 만드는 기술"이 아니다. 시스템의 생존력을 높이는 기술이다.

  • 결합도가 낮아진다: 서비스 A가 서비스 B의 존재를 몰라도 된다
  • 장애가 전파되지 않는다: 하나가 죽어도 나머지는 정상 동작한다
  • 트래픽 급증에 대응한다: 큐가 버퍼 역할을 해서 Consumer가 자기 속도로 처리한다
  • 독립적 확장이 가능하다: 병목이 되는 서비스만 골라서 Consumer를 늘릴 수 있다

처음에는 동기 방식(직접 API 호출)이 단순하고 이해하기 쉽다. 하지만 서비스가 커지고, 트래픽이 늘고, 장애가 빈번해지면 — 그때서야 메시징의 가치를 깨닫게 된다. 그리고 그때는 이미 바꾸기 어렵다.

그래서 많은 시니어 엔지니어들이 말한다: "비동기로 할 수 있다면, 비동기로 해라."

다음 편에서는 이 메시징 시스템을 서버리스 환경에서 어떻게 운영하는지 — Lambda와 SQS의 이벤트 소스 매핑, 동시성 제어, 부분 배치 실패 처리 등을 다룰 예정이다.