안녕하세요. 자바파커입니다.
초당 요청이 100건 들어오는데 서버는 20건밖에 처리하지 못한다고 해보겠습니다. 직접 호출이라면 20건은 처리되고 나머지는 타임아웃이나 오류가 됩니다.
그 사이에 메시지 큐를 넣으면 어떻게 될까요?
서버가 빨라지는 것은 아닙니다. 처리하지 못한 80건이 실패 대신 대기가 됩니다.
이것이 메시지 큐의 핵심입니다. 메시지 큐는 일을 빨리 하는 장치가 아니라, 생산 속도와 소비 속도의 차이를 안전하게 저장하는 장치입니다.
요청 블록이 큐에 쌓였다가 Worker가 자기 속도로 처리하고, Consumer를 늘리면 적체가 줄어드는 과정은 58초 영상으로도 정리했습니다. 영상이 공개되면 이 자리에 연결합니다.
Producer, Queue, Consumer
메시지 큐 시스템은 세 부분으로 나뉩니다.
Producer → Queue → Consumer
주문 API 대기열 결제·메일 Worker| 구성요소 | 역할 |
|---|---|
| Producer | 처리할 일을 메시지로 만들어 큐에 넣습니다 |
| Queue/Broker | Consumer가 가져갈 때까지 메시지를 저장합니다 |
| Consumer | 메시지를 가져와 실제 작업을 처리합니다 |
직접 호출에서는 Producer가 Consumer의 응답을 기다립니다. Consumer가 느리거나 죽으면 Producer까지 함께 느려집니다.
큐를 넣으면 Producer는 메시지를 맡기고 먼저 끝낼 수 있습니다. Consumer는 자기 속도로 처리합니다. 둘의 생명주기와 처리량이 분리됩니다. Amazon SQS 공식 문서가 메시지 큐의 첫 장점으로 분리(decoupling)와 독립적인 확장을 드는 이유입니다.
큐는 처리량 차이를 backlog로 바꾼다
초당 유입량을 λ, 초당 처리량을 μ라고 하겠습니다.
λ = 100 msg/s
μ = 20 msg/s
backlog 증가량 = λ - μ = 80 msg/s큐가 없으면 이 차이는 오류가 됩니다. 큐가 있으면 backlog가 됩니다.
이 차이는 중요합니다. 오류는 요청을 잃지만, backlog는 나중에 처리할 수 있습니다. 다만 큐가 문제를 없앤 것은 아닙니다. 시간을 벌었을 뿐입니다.
유입량이 계속 처리량보다 크면 큐는 무한히 쌓입니다.
10분 뒤 backlog = 80 × 600 = 48,000건그래서 "큐가 있으니 안전하다"가 아니라 **"얼마나 오래 버틸 수 있는가"**를 계산해야 합니다.
Consumer를 늘리면 적체가 줄어든다
큐의 장점은 Consumer를 독립적으로 확장할 수 있다는 점입니다.
Consumer 1개 × 20 msg/s = 20 msg/s
Consumer 3개 × 20 msg/s = 60 msg/s
Consumer 5개 × 20 msg/s = 100 msg/s다만 Consumer 수를 무작정 늘릴 수는 없습니다. 뒤에 있는 데이터베이스나 외부 API가 새 병목이 될 수 있습니다.
특히 Consumer마다 DB 커넥션을 여러 개 잡는다면, Worker 확장이 커넥션 풀 고갈로 이어질 수 있습니다. 큐 앞의 처리량만 볼 게 아니라 파이프라인 전체의 가장 느린 지점을 봐야 합니다.
ACK 전에는 끝난 게 아니다
메시지가 Consumer에 전달됐다고 해서 작업이 끝난 것은 아닙니다. Consumer가 처리 도중 죽을 수 있기 때문입니다.
그래서 메시지 브로커는 보통 ACK(acknowledgement) 를 사용합니다.
1. Consumer가 메시지를 받는다
2. 메시지를 처리한다
3. DB 저장·외부 호출까지 성공한다
4. ACK를 보낸다
5. Broker가 메시지를 삭제한다RabbitMQ 공식 문서도 Consumer ACK를 "메시지가 성공적으로 처리됐으니 삭제해도 된다"는 신호로 설명합니다.
Amazon SQS에서는 메시지를 받으면 일정 시간 다른 Consumer에게 보이지 않게 하는 Visibility Timeout을 씁니다. 그 시간 안에 삭제하지 못하면 메시지가 다시 보이고 다른 Consumer가 재처리할 수 있습니다.
따라서 ACK는 받자마자 보내는 게 아니라, 작업이 성공적으로 끝난 뒤 보내야 합니다.
at-least-once라면 중복을 전제로 만든다
ACK를 보낸 직후 네트워크가 끊기면 어떻게 될까요?
Consumer는 작업을 끝냈지만 Broker는 ACK를 못 받았다고 판단할 수 있습니다. 그러면 같은 메시지를 다시 전달합니다.
이것이 at-least-once delivery입니다.
적어도 한 번은 전달하지만, 두 번 전달될 수 있습니다.
중복을 막는 책임은 Consumer에도 있습니다. 같은 메시지를 두 번 받아도 결과가 한 번만 반영되도록 멱등성(idempotency) 을 만들어야 합니다.
CREATE UNIQUE INDEX uq_payment_event
ON payment_event(event_id);if (eventRepository.existsById(eventId)) {
return; // 이미 처리한 메시지
}결제·쿠폰·재고처럼 두 번 실행되면 안 되는 작업일수록 메시지 ID, 비즈니스 키, 고유 제약 조건이 중요합니다.
"Exactly once"라는 제품 기능이 있더라도 브로커 밖의 DB·메일·외부 API까지 포함한 종단간 exactly once는 별개의 문제입니다. 실무에서는 중복이 올 수 있다고 가정하고 멱등하게 만드는 편이 안전합니다.
계속 실패하는 메시지는 DLQ로 보낸다
잘못된 데이터나 코드 버그 때문에 처리할 수 없는 메시지가 있을 수 있습니다. 이런 메시지를 흔히 poison message라고 부릅니다.
무한 재시도하면 한 메시지가 Consumer 시간을 계속 잡아먹습니다.
실패 → 즉시 재시도 → 실패 → 즉시 재시도 → ...재시도에는 두 장치가 필요합니다.
- 지수 백오프 — 1초, 2초, 4초처럼 간격을 늘립니다
- DLQ(Dead-Letter Queue) — 정해진 횟수만큼 실패하면 별도 큐로 격리합니다
DLQ는 쓰레기통이 아닙니다. 실패 원인을 조사하고, 수정 뒤 재처리하기 위한 격리 구역입니다.
반드시 원본 메시지, 실패 횟수, 마지막 오류, 최초 발생 시각을 함께 남겨야 합니다.
순서는 공짜가 아니다
메시지 큐라고 해서 모든 메시지가 입력 순서대로 처리되는 것은 아닙니다.
Consumer가 여러 개면 먼저 가져간 메시지가 더 늦게 끝날 수 있습니다.
입력: A → B → C
완료: B → C → A순서가 중요하다면 주문 ID 같은 키로 파티션을 나누거나 FIFO 큐를 써야 합니다. 하지만 같은 키의 메시지를 직렬로 처리하면 병렬성이 줄어듭니다.
순서 보장과 처리량은 서로 교환하는 값입니다. 전체 순서가 정말 필요한지, 같은 주문·같은 사용자 안에서만 필요할지 먼저 정해야 합니다.
무엇을 모니터링해야 하나
큐 길이 하나만 보면 늦습니다.
| 지표 | 알려주는 것 |
|---|---|
| Backlog 크기 | 처리하지 못한 메시지가 얼마나 쌓였나 |
| Oldest message age | 가장 오래 기다린 메시지가 몇 초 됐나 |
| 처리량 | 초당 몇 건을 넣고 꺼내나 |
| 처리 시간 | Consumer 한 건이 얼마나 걸리나 |
| 재시도 횟수 | 일시 장애인지 poison message인지 |
| DLQ 유입량 | 처리할 수 없는 메시지가 생기고 있나 |
가장 중요한 지표는 가장 오래된 메시지의 나이입니다. 큐 길이가 같아도 1초 동안 몰린 1만 건과 3시간째 남은 1만 건은 심각도가 다릅니다.
메시지 큐를 쓰지 않아도 되는 경우
모든 호출을 비동기로 만들 필요는 없습니다.
- 사용자가 결과를 즉시 받아야 하는 로그인·잔액 조회
- 호출 하나면 끝나는 작은 시스템
- 강한 동기 트랜잭션이 필요한 작업
- 운영팀이 큐·재시도·DLQ를 관찰할 준비가 없는 경우
큐를 넣으면 결합도는 낮아지지만, 흐름 추적·재시도·중복·순서·운영 복잡도가 생깁니다.
느린 직접 호출을 숨기기 위해 큐를 넣지 마세요. 비동기 처리가 제품 요구사항에 맞고, 지연을 허용할 수 있을 때 사용해야 합니다.
자주 묻는 질문 (FAQ)
Kafka도 메시지 큐인가요?
넓게 보면 메시징 시스템이지만, 전통적인 작업 큐와는 모델이 다릅니다. Kafka는 메시지를 Consumer가 읽어도 바로 삭제하지 않고 로그에 보관하며, Consumer가 offset으로 읽은 위치를 관리합니다. 이벤트 스트리밍·재처리·여러 Consumer Group에 강합니다.
SQS와 RabbitMQ 중 무엇을 써야 하나요?
운영 부담을 줄이고 AWS 안에서 단순한 작업 큐가 필요하면 SQS가 편합니다. 세밀한 라우팅, AMQP, 자체 운영 제어가 필요하면 RabbitMQ가 후보입니다. 제품 이름보다 순서·전달 보장·지연·처리량·운영 책임을 먼저 정하세요.
메시지 큐를 넣으면 응답이 빨라지나요?
사용자에게 "접수 완료"를 빨리 돌려줄 수는 있습니다. 하지만 실제 작업 완료 시간은 줄지 않습니다. 오히려 큐 대기 시간만큼 늘 수 있습니다. 빠른 응답과 빠른 완료는 다른 지표입니다.
큐가 꽉 차면 어떻게 되나요?
메모리·디스크·보관 기간·서비스 할당량에 도달하면 publish가 실패하거나 오래된 메시지가 사라질 수 있습니다. 최대 backlog와 버틸 수 있는 시간을 미리 계산하고 알람을 설정해야 합니다.
정리하며
메시지 큐의 역할은 한 문장입니다.
처리량 차이를 실패가 아니라 안전한 대기로 바꾼다.
- Producer와 Consumer의 속도·생명주기를 분리합니다
- 큐는 작업을 빠르게 하지 않고 backlog로 시간을 법니다
- ACK 이전에는 끝난 것이 아닙니다
- at-least-once에서는 중복을 전제로 멱등하게 처리합니다
- 반복 실패는 백오프와 DLQ로 격리합니다
- 큐 길이보다 oldest message age를 함께 봅니다
큐가 쌓이는 것은 장애가 아니라 신호입니다. 문제는 쌓이는 속도보다 비우는 속도가 계속 느린데도 아무도 모르는 상태입니다.