주문 API가 결제·알림·재고 시스템을 순서대로 동기 호출합니다. 어느 날 알림 서비스가 느려지자 주문 API 전체가 그 응답을 기다리다 타임아웃, 결국 주문조차 안 됩니다. 트래픽이 몰린 세일 날엔 한 번에 들어온 요청을 다 받아내지 못해 줄줄이 실패합니다. 서비스를 동기로 직접 묶으면 한 곳의 느림·장애가 전체로 번집니다. 메시지 큐는 그 사이에 완충 지대를 둡니다.
- 1메시지 큐가 생산자·소비자를 비동기로 분리하는 원리를 안다
- 2큐(작업 분배)와 발행구독(팬아웃)의 차이를 구분할 수 있다
- 3최소 1회 전달과 멱등성이 왜 짝인지 설명할 수 있다
- 4DLQ가 독성 메시지를 격리하는 방식을 이해한다
- 5큐 길이를 스케일링·알람의 신호로 쓰는 법을 안다
사이에 완충 지대를 둔다
동기 호출의 결합 vs 큐의 분리
A가 B를 동기 호출하면, B가 느리면 A도 느리고 B가 죽으면 A도 막힙니다 — 강하게 결합됩니다. 사이에 메시지 큐를 두면, A는 큐에 메시지를 넣고 바로 응답하고, B는 자기 속도로 큐에서 꺼내 처리합니다.
효과: ① B가 잠깐 죽어도 메시지는 큐에 보관(유실 방지), ② 트래픽 급증을 큐가 완충(B는 밀린 만큼 천천히 처리), ③ B의 장애가 A로 전파되지 않음(장애 격리). 이것이 비동기 디커플링입니다(동기 vs 비동기).
확대
위 그림처럼 동기 호출은 응답을 기다려야 하므로 하나가 느려지면 전체가 막힙니다. 큐로 분리하면 생산자와 소비자가 독립적으로 동작해 장애가 전파되지 않습니다.
확대
위 그림처럼 큐(SQS)는 메시지 하나를 워커 하나가 가져가고, Pub/Sub(SNS)는 모든 구독자에게 각각 전달합니다. 작업 분산이냐 이벤트 팬아웃이냐에 따라 선택이 달라집니다.
확대
위 그림처럼 큐는 '최소 1회' 전달을 보장하므로 소비자가 멱등하게 설계돼야 합니다. 처리 실패가 반복되면 DLQ로 격리해 장애 원인을 분석하고 수동으로 재처리합니다.
메시지가 생산자에서 소비자로 가는 법
메시지가 생산자에서 소비자로 가는 법 — 발행·수신·ack 6단계
주문 서비스는 queue.send(message) 한 줄을 던지고 즉시 응답합니다. 그 메시지가 소비자에게 유실 없이·중복은 견디며 도달하기까지는 발행·저장·수신·처리·ack·재전송이 얽혀 돌아갑니다. 이 흐름을 단계로 보지 못하면 "왜 같은 주문이 두 번 처리됐지", "왜 메시지가 조용히 사라졌지"의 원인을 짚을 수 없습니다. 큐의 안전성은 결국 어느 시점에 ack(확정)하느냐에서 갈립니다.
[Producer] send(message)
│
① 브로커(큐/토픽)에 발행 → 브로커가 저장(내구성) 후 생산자에 ACK 반환
│ → 이 시점부터 메시지는 유실되지 않고 소비 전까지 보관됨
│
② Consumer가 pull(폴링) 또는 push로 수신
│ → 메시지는 "처리 중"으로 잠김 (visibility timeout 동안 안 보임)
│
③ Consumer가 처리 (비즈니스 로직 수행)
│
④ 성공 → ack (SQS는 삭제 / Kafka는 offset 커밋) → 메시지 확정 제거
│ 실패·무응답 → 타임아웃 만료 → 메시지 다시 보임 → 재전송
│
⑤ 재전송 = at-least-once → 같은 메시지 중복 가능 → 멱등 처리로 흡수
│
⑥ N회 반복 실패 → DLQ로 격리 (정상 흐름은 계속)
▼
[처리 완료] 또는 [DLQ에서 조사·재처리 대기]
각 단계에서 무슨 일이 일어나고, 어긋나면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 어긋나면 |
|---|---|---|
| ① 발행·저장 | 브로커가 저장한 뒤 ack. 저장 전 브로커가 죽으면 생산자는 실패를 알고 재시도 | ack를 못 받아 재시도 → 브로커엔 이미 저장돼 중복 유입 가능 |
| ② 수신·잠금 | 소비자가 꺼내면 visibility timeout 동안 다른 소비자에게 안 보임 | 처리 시간이 타임아웃보다 길면 처리 중인데 재노출 → 다른 워커가 중복 처리 |
| ③ 처리 | 소비자가 실제 작업 수행 (한 메시지는 보통 컨슈머 그룹 내 한 워커가) | 처리 도중 크래시 → ack 못 함 → ④에서 재전송됨(유실은 아님) |
| ④ ack·커밋 | 성공해야 삭제(SQS)·오프셋 커밋(Kafka). ack 전까지는 미확정 | 처리 전 조기 커밋 → 크래시 시 유실 / 처리 후 커밋이면 중복은 나도 유실은 없음 |
| ⑤ 중복 | at-least-once라 중복은 불가피 → 메시지 ID 기록·upsert로 멱등 처리 | 멱등 미보장 → 이중 결제·재고 이중 차감 |
| ⑥ DLQ·백프레셔 | 반복 실패분은 DLQ로 격리, 소비자가 못 따라가면 큐 적체가 쌓임(백프레셔 신호) | DLQ 방치·적체 무시 → 지연된 장애, 보존 기간 초과 시 조용한 유실 |
여기서 유실과 중복을 가르는 스위치는 ④의 커밋 시점 하나입니다 — 오프셋을 "처리 후"에 커밋하면 최악의 경우 중복(그래서 ⑤ 멱등이 필요)이고, "처리 전"에 커밋하면 최악의 경우 유실입니다. 분산 큐에서 중복은 없애는 것이 아니라 견디는 것이므로, 대부분의 시스템은 "처리 후 커밋 + 멱등 소비자"를 택합니다.
즉 "메시지가 안전히 처리됐다"는 ①저장 → ②잠금 → ④ack가 순서대로 참이라는 뜻입니다. 중복이 보이면 ②(타임아웃 대비 처리 시간)와 ④(ack 시점)를, 유실이 보이면 ④(처리 전 커밋 여부)와 보존 기간을 의심합니다 — 증상마다 봐야 할 단계가 정해져 있습니다.
큐(작업 분배) vs 발행구독(팬아웃)
- 큐(SQS, point-to-point): 메시지를 여러 워커가 나눠 처리. 한 메시지는 보통 한 워커가. 작업을 분산 처리할 때.
- 발행구독(SNS, pub/sub): 한 번 발행한 이벤트를 구독한 여러 대상에 동시 전달(팬아웃). 예: 주문 1건 → 결제·알림·재고에 동시 통지.
실무 패턴: SNS → 여러 SQS 로 묶어 "한 이벤트를 여러 시스템에 팬아웃하되, 각 시스템은 자기 큐로 안정적으로 소비"하게 만듭니다. 이벤트 버스(EventBridge)는 여기에 라우팅 규칙을 더한 형태입니다.
최소 1회 전달과 멱등성 — 중복을 견디는 설계
대부분의 분산 큐는 최소 1회(at-least-once) 전달입니다 — 재시도 때문에 같은 메시지가 두 번 올 수 있습니다. 처리 로직이 멱등하지 않으면 이중 결제·이중 차감이 납니다.
해결: 메시지 ID로 "이미 처리했는지" 확인하거나, "존재하면 갱신(upsert)"처럼 두 번 처리해도 결과가 같게 설계합니다(용어사전의 Idempotency-Key와 같은 원리). 반복 실패하는 독성 메시지는 DLQ(Dead Letter Queue)로 빼내 정상 흐름을 막지 않게 합니다.
큐에 쌓인(아직 처리 안 된) 메시지 수를 봅니다. 계속 늘면 소비자가 못 따라가는 것입니다.
aws sqs get-queue-attributes --queue-url "$Q_URL" \
--attribute-names ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible \
--query "Attributes"
{ "ApproximateNumberOfMessages": "1340", "ApproximateNumberOfMessagesNotVisible": "12" }
aws sqs get-queue-attributesDLQ에 메시지가 있으면 반복 실패가 있다는 신호입니다. 수를 확인하고 원인 메시지를 들여다봅니다.
aws sqs get-queue-attributes --queue-url "$DLQ_URL" \
--attribute-names ApproximateNumberOfMessages --query "Attributes.ApproximateNumberOfMessages"
"7" # 7건이 반복 실패 → 조사 필요
aws sqs get-queue-attributes (DLQ)- ApproximateNumberOfMessages가 계속 증가 추세 — 소비자 처리량 < 유입량. 워커 수를 큐 길이 기반으로 스케일아웃(오토스케일링 + 로드밸런서)하거나 처리 최적화
- NotVisible(처리 중) 수가 비정상적으로 큼 — 처리가 오래 걸려 가시성 타임아웃 동안 잠겨 있음. 처리 시간 vs visibility timeout 점검
- DLQ에 메시지가 쌓임 — 독성 메시지/버그. DLQ 적체는 알람 대상. 메시지 본문으로 원인 파악 후 수정·재처리
- 같은 작업이 두 번 수행된 흔적(이중 결제/알림) — 멱등성 미보장. 메시지 ID 기반 중복 제거 도입
상황: 메시지가 중복 처리돼 부작용이 두 번 발생.
원인: 큐의 최소 1회 전달 특성. 소비자가 메시지를 처리하고 삭제(ack)하기 전에 죽거나 가시성 타임아웃이 지나면, 메시지가 다시 보여 다른 워커가 또 처리합니다. 멱등하지 않은 로직이면 부작용이 중복됩니다.
진단: 처리 시간이 visibility timeout보다 긴지 확인 → 처리 완료 후에만 메시지를 삭제하는지(중간 실패 시 미삭제) → 멱등 처리 여부 확인.
해결: ① 처리 로직을 멱등하게(메시지 ID 중복 체크/upsert), ② visibility timeout을 실제 처리 시간보다 넉넉히, ③ 처리 완료 후 삭제. 정확히 1회가 꼭 필요하면 FIFO 큐(중복 제거) 검토 — 단 처리량 제약이 있음. 중복은 '없앤다'보다 '견딘다'가 분산 시스템의 기본 자세입니다.
심화 — 큐는 시간을 벌 뿐, 문제를 없애지 않는다
심화: 적체의 산수 — 드레인 시간·보존 기간·크기 한계
"큐가 급증을 흡수한다"의 다음 단계 질문은 "얼마나 오래, 얼마나 큰 것까지 흡수하는가"입니다. 세 가지 숫자를 알아야 합니다.
- 드레인 시간의 산수: 적체는 저절로 사라지지 않습니다. 밀린 양을 비우는 시간 = 적체량 ÷ (처리량 − 유입량). 유입 1,000건/초에 처리 600건/초면 큐는 초당 400건씩 계속 자라고, 이 식의 답은 무한대입니다. 큐가 '흡수'하는 것은 처리량이 결국 유입량을 넘어설 때의 이야기라, 소비자 스케일링과 짝을 이루지 못한 큐는 장애를 없애는 게 아니라 지연된 장애로 바꿔 둘 뿐입니다.
- 보존 기간이 지나면 조용히 사라집니다: 큐의 메시지 보존 기간(SQS 기본 4일, 최대 14일)이 지나면 메시지는 아무 알림 없이 삭제됩니다. "소비자는 다음 주에 고치면 되지"가 데이터 유실이 되는 지점입니다. 긴 장애가 예상되면 보존 기간을 먼저 늘리고, 가장 오래된 메시지의 나이(대기 시간)를 알람으로 겁니다(관측과 거버넌스).
- 메시지에는 크기 한계가 있습니다(SQS 256KB): 파일·대용량 페이로드를 메시지에 직접 담으면 곧 벽에 부딪힙니다. 정석은 클레임 체크(claim-check) 패턴 — 본문은 오브젝트 스토리지에 두고 메시지에는 그 키(포인터)만 담아, 큐는 가볍게·페이로드는 크기 제한 없이 다룹니다.
상황: 메시지 양은 미미한데 SQS 비용이 폭증. 메시지 건수 기준으로 견적을 냈던 팀은 원인을 찾지 못합니다.
원인: SQS는 메시지 수가 아니라 API 요청 건수로 과금하는데, 워커들이 쇼트 폴링으로 빈 큐를 쉬지 않고 두드리고 있었습니다. 대기 없이(WaitTimeSeconds=0) 루프를 도는 워커 20개가 각각 초당 수십 번 ReceiveMessage를 호출하면 월 수억 건의 '빈 수신' 요청이 됩니다 — 받은 메시지가 0건이어도 요청은 전부 과금됩니다.
진단: CloudWatch의 NumberOfEmptyReceives 지표를 확인합니다 — 수신 요청 대비 빈 응답 비율이 99%를 넘으면 확정입니다. 비용 탐색기에서 SQS 요청 수 추이와 워커 증설 시점을 대조합니다.
해결: ① 롱 폴링(WaitTimeSeconds=20)으로 전환 — 메시지가 없으면 최대 20초 기다렸다가 응답하므로 빈 요청이 수십 분의 일로 줄고, 새 메시지에 대한 반응은 오히려 빨라집니다 ② 수신 배치(MaxNumberOfMessages=10)로 요청당 처리량을 올림 ③ 유휴 시간대 워커 수 축소. 롱 폴링은 사실상 단점 없는 기본기인데 라이브러리·SDK 기본값이 쇼트 폴링인 경우가 있어, 명시적으로 설정했는지가 점검 포인트입니다.
"트래픽 급증을 어떻게 견디나요?"의 핵심 답 중 하나가 큐입니다 — 동기 처리로 다 받아내려다 무너지는 대신, 큐로 받아 두고 소비자가 자기 속도로 처리하면 스파이크를 흡수합니다(오토스케일링 + 로드밸런서). "서비스 간 장애 전파를 어떻게 막나요?"의 답도 큐를 통한 비동기 분리입니다.
설계 면접 단골: "주문 처리를 어떻게 구성하나요?" → "주문은 동기로 받아 확정하고, 결제 후속·알림·재고는 이벤트로 발행(SNS)해 각 시스템이 자기 큐(SQS)로 비동기 처리, 멱등성으로 중복 견디고 DLQ로 실패 격리"가 탄탄한 답입니다. 이벤트 기반 아키텍처(동기 vs 비동기)와 서버리스(서버리스(Lambda))가 자연스럽게 결합됩니다.
이로써 Cloud 트랙은 계정·네트워크·컴퓨트·스토리지·DB·서버리스·컨테이너·IaC·비용·관측·보안·메시징까지 — 클라우드 위에서 시스템을 설계·운영하는 큰 그림을 갖췄습니다. 더 깊은 이벤트 아키텍처는 동기 vs 비동기, 오케스트레이션은 Kubernetes로 이어집니다.