74장. SQS — 큐로 결합도 낮추기
이 장에서 말하고자 하는 것
가장 단순한 비동기 도구가
Amazon SQS (Simple Queue Service)
다.
큐(Queue)는 우편함 같은 것이다.
- 누가 메시지를 넣는다
- 다른 누가 와서 꺼낸다
- 보내는 쪽과 받는 쪽이 동시에 살아 있지 않아도 된다
1. SQS의 기본 모델
[Producer] → [Queue: orders-jobs] → [Consumer]
├─ msg 1
├─ msg 2
└─ msg 3
- Producer는 큐에 메시지를 넣는다 (SendMessage)
- Consumer는 큐에서 메시지를 꺼낸다 (ReceiveMessage)
- 처리가 끝나면 메시지를 삭제 (DeleteMessage)
2. Standard vs FIFO
| 항목 | Standard | FIFO |
|---|---|---|
| 순서 보장 | 약함 (대체로) | 강함 (그룹 안에서) |
| 중복 가능성 | 있음 (At-least-once) | 없음 (Exactly-once) |
| 처리량 | 거의 무제한 | 초당 ~300건 (배치로 ~3000) |
| 비용 | 쌈 | 약간 비쌈 |
대부분 Standard로 시작 — 순서/중복이 중요한 곳만 FIFO
3. Visibility Timeout — 처리 중인 메시지를 잠시 감춤
Consumer가 메시지를 꺼내면 그 메시지는 다른 Consumer가 못 보게 잠시 숨겨진다.
Consumer A가 메시지 꺼냄 → 30초 동안 다른 Consumer에 안 보임
↓
처리 성공 → DeleteMessage → 큐에서 사라짐
처리 실패 / 시간 초과 → 다시 나타남 → 다른 Consumer가 받을 수 있음
처리 시간보다 Visibility Timeout이 짧으면 같은 메시지가 두 번 처리된다.
처리 시간이 길면 Visibility Timeout도 길게 (또는 ChangeMessageVisibility로 연장)
4. At-least-once — “한 번 이상” 보장
Standard SQS의 핵심 특성.
한 메시지가 두 번 이상 전달될 수 있다
따라서 Consumer는 멱등하게(idempotent) 처리해야 한다.
"주문 12345 처리됨" 표시를 먼저 본다
↓ 이미 처리됐으면 skip
↓ 아니면 처리
SQS를 쓰는 모든 Consumer는 멱등성 설계가 기본이다
5. Dead Letter Queue (DLQ)
메시지가 N번 처리에 실패하면 별도 큐로 옮긴다.
[Queue] → 3번 실패 → [DLQ]
DLQ는
- 끝없이 재시도되는 메시지를 격리
- 사람이 살펴볼 수 있게 보관
- 알람의 트리거
DLQ 없는 큐는 운영하지 않는다
6. Long Polling
ReceiveMessage 할 때 메시지가 없으면 빈 응답이 즉시 돌아온다 (Short Polling).
이러면 API 호출이 끝없이 발생해 비용 · 부하 모두 손해.
WaitTimeSeconds = 20
이러면 메시지가 도착할 때까지 (최대 20초) 기다린다.
Long Polling은 사실상 기본값 — 항상 켠다
7. 흔한 패턴 — 워커 큐
[Producer]
↓ SendMessage
[Queue]
↓
[Consumer Auto Scaling]
├─ Worker 1
├─ Worker 2
└─ Worker N (큐 깊이에 따라 자동 증감)
ECS Service Auto Scaling이 큐 길이를 보고 Worker 수를 조절한다.
큐에 메시지가 1000건 → Worker 늘어남
큐가 비면 → Worker 줄어듦
8. 우리 서비스에서
[orders 서비스]
↓ SendMessage "주문 생성됨"
[SQS: order-events]
↓
[notification worker] → 알림 전송
[analytics worker] → 통계 적재
각 Worker는 자기 처리만 책임진다.
order-events ───→ notification-jobs (DLQ: notification-dlq)
───→ analytics-jobs (DLQ: analytics-dlq)
큐를 워커별로 분리하면 한쪽이 막혀도 다른 쪽이 영향 없다.
9. 직접 확인해보기 — CLI
# 큐 만들기
aws sqs create-queue --queue-name order-events
# 메시지 보내기
aws sqs send-message \
--queue-url <queue-url> \
--message-body '{"type":"OrderCreated","orderId":"o-1"}'
# 메시지 받기 (Long Polling)
aws sqs receive-message \
--queue-url <queue-url> \
--wait-time-seconds 20
# 메시지 삭제
aws sqs delete-message \
--queue-url <queue-url> \
--receipt-handle <handle>
10. 코드로는 이렇게 생겼다 — Terraform
resource "aws_sqs_queue" "order_events_dlq" {
name = "order-events-dlq"
message_retention_seconds = 1209600 # 14일
}
resource "aws_sqs_queue" "order_events" {
name = "order-events"
visibility_timeout_seconds = 60
message_retention_seconds = 345600 # 4일
receive_wait_time_seconds = 20 # Long polling
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.order_events_dlq.arn
maxReceiveCount = 3
})
}
DLQ는 항상 함께 만든다.
11. 이렇게 쓰면 망한다 — 안티패턴
안티패턴 1. DLQ 없이 큐를 운영
실패 메시지가 영원히 재시도된다. 비용 폭증.
안티패턴 2. 멱등성 없이 Consumer를 만든다
같은 주문이 두 번 결제되는 버그가 난다.
안티패턴 3. Visibility Timeout이 처리 시간보다 짧다
같은 메시지를 여러 Worker가 동시에 처리한다.
안티패턴 4. Short Polling으로 끝없이 ReceiveMessage 호출
SQS API 비용이 메시지 처리 비용보다 커진다.
Long Polling은 항상 켠다
12. 한 줄로 정리
SQS는 가장 단순한 비동기 도구이며,
Visibility Timeout · 멱등성 · DLQ · Long Polling 네 가지가 운영의 토대다
13. 이 장의 핵심 정리
- SQS는 1:1 큐 — Producer가 넣고 Consumer가 꺼낸다.
- Standard는 at-least-once, FIFO는 순서/중복 제거가 강하다.
- Visibility Timeout은 처리 중 메시지를 다른 Consumer로부터 감춘다.
- Consumer는 항상 멱등하게 설계한다.
- DLQ는 사실상 필수, Long Polling도 항상 켠다.
- ECS Service Auto Scaling을 큐 깊이로 묶어 워커를 자동 조절한다.