메시지 큐와 이벤트 기반 아키텍처
🎯 핵심 질문
시스템 결합도가 심각하고 트래픽이 급증할 때, 핵심 체인의 안정성을 어떻게 보장할 수 있을까? 메시지 큐는 현대 분산 시스템의 "버퍼"이자 "디커플러"이다. 이 글에서는 실제 사례(레스토랑 호출 시스템, 택배 분류, 플래시 세일 시스템)를 통해 메시지 큐의 설계 철학과 엔지니어링 실천법을 깊이 이해해본다.
1. 왜 "메시지 큐"가 필요한가?
1.1 실제 사례에서 시작하기: 타오바오 주문 시스템의 진화
2012년, 타오바오 주문 시스템은 심각한 장애를 겪었다. 광군제(11.11) 자정, 트래픽이 순간적으로 폭주하며 주문 서비스가 재고 서비스, 결제 서비스, 물류 서비스를 직접 호출했고... 전체 체인이 도미노처럼 연쇄적으로 무너졌다.
당시 아키텍처(강한 결합):
사용자 주문 → 주문 서비스 → 동기식 재고 서비스 호출 → 동기식 결제 서비스 호출 → 동기식 물류 서비스 호출
↓ ↓ ↓
응답 200ms 응답 500ms 응답 300ms⚠️ 강한 결합의 치명적인 문제
- 총 응답 시간 = 200 + 500 + 300 = 1000ms (사용자 1초 대기)
- 재고 서비스 장애 → 주문 서비스도 장애 (스레드 풀 고갈)
- 결제 서비스 지연 → 전체 체인 지연
- 수평 확장 불가 → 수직 확장만 가능 (비싸고 한계 있음)
개선된 아키텍처(메시지 큐 도입):
사용자 주문 → 주문 서비스 → "주문 생성" 메시지 전송 → 즉시 응답(50ms)
↓
메시지 큐(Kafka)
↓
┌─────────────┬─────────────┬─────────────┐
▼ ▼ ▼ ▼
재고 서비스 결제 서비스 물류 서비스 알림 서비스
(비동기 차감) (비동기 처리) (비동기 생성) (비동기 발송)✨ 개선 효과
- 사용자 응답 시간 = 50ms (체감 성능 20배 향상)
- 재고 서비스 장애 → 메시지 큐에 임시 저장, 복구 후 계속 처리
- 결제 서비스 지연 → 주문 생성에 영향 없음
- 수평 확장 가능 → 컨슈머 인스턴스만 추가하면 됨
1.2 메시지 큐의 일상 비유
레스토랑 호출 시스템
인기 레스토랑에 갔다고 상상해보자:
- 호출 시스템 없음: 손님이 창구 앞에서 기다려야 하고, 창구는 한정적이며, 뒷사람은 길게 줄 서고, 레스토랑은 큰 부담
- 호출 시스템 있음: 주문 후 번호표를 받고, 자리에 앉아 기다리다 번호가 호출되면 음식을 받으러 감
메시지 큐는 바로 소프트웨어 시스템의 "호출 시스템"이다:
- 프로듀서(주문하는 사람) → 메시지(주문)를 큐에 넣음
- 큐(호출기) → 메시지를 임시 저장
- 컨슈머(요리사) → 자신의 페이스대로 메시지를 처리
After the traffic peak passes, the system keeps processing the backlog at full speed until the queue is empty. This is peak shaving.
2. 메시지 큐란 무엇인가? (정의 + 핵심 3요소)
2.1 "메시지 큐"란?
🤔 용어 설명
메시지 큐(Message Queue, MQ) 는 메시지를 저장하는 컨테이너로, 프로듀서가 메시지를 넣고 컨슈머가 메시지를 꺼내 처리한다. "비동기 통신"을 구현하여, 송신자가 수신자의 처리를 기다릴 필요가 없게 한다.
동기 vs 비동기:
- 동기: 전화 통화처럼, 상대방이 받아야만 대화 가능
- 비동기: 메시지 전송처럼, 보내기만 하면 되고 상대방이 시간 있을 때 확인
이는 친구에게 전화 걸기(동기) vs 메시지 보내기(비동기)와 같다.
2.2 메시지 큐의 핵심 3요소
요소 1: 프로듀서 (Producer)
역할: 메시지를 생성하여 큐에 전송한다.
일상 비유: 프로듀서는 "발신인"과 같아서, 편지(메시지)를 우체국(큐)에 보낸다.
주요 설계 포인트
- 전송 방식: 동기 전송 (신뢰성 높지만 블로킹) vs 비동기 전송 (고성능이지만 콜백 처리 필요)
- 메시지 확인: 브로커 확인 대기 (At Least Once) vs 전송 후 무시 (At Most Once)
- 실패 처리: 재시도 전략, 로컬 로그 백업, 데드 레터 큐
요소 2: 컨슈머 (Consumer)
역할: 큐에서 메시지를 가져와 처리한다.
일상 비유: 컨슈머는 "수신인"과 같아서, 우편함(큐)에서 편지(메시지)를 꺼내 처리한다.
주요 설계 포인트
- 소비 모드: 푸시 모드 (Push, 브로커가 능동적으로 푸시) vs 풀 모드 (Pull, 컨슈머가 능동적으로 가져옴)
- 소비 확인: 자동 ACK (효율적이지만 메시지 손실 가능) vs 수동 ACK (신뢰성 높지만 타임아웃 처리 필요)
- 동시성 제어: 단일 스레드 순차 소비 vs 멀티 스레드 병렬 소비
- 실패 처리: 재시도 전략, 데드 레터 큐, 보상 메커니즘
요소 3: 브로커 (Broker)
역할: 메시지를 수신, 저장, 전달한다.
일상 비유: 브로커는 "우체국" 또는 "택배 중계소"와 같아서, 편지를 접수, 분류, 배송한다.
주요 설계 포인트
- 저장 모델: 메모리 저장 (저지연) vs 디스크 저장 (고신뢰성)
- 복제 전략: 마스터-슬레이브 복제, 다중 레플리카 동기화
- 고가용성 메커니즘: 클러스터 배포, 자동 장애 조치
- 확장성: 파티션 (Partition), 샤딩 (Sharding)
3. 핵심 문제 1: 시스템을 어떻게 디커플링하여 "한 곳이 흔들리면 전체가 무너지는" 상황을 피할 것인가?
3.1 강한 결합의 비극: 서비스 하나가 죽으면 전체가 끝장난다
시나리오 재현: 한 이커머스 플랫폼의 초기 아키텍처
주문 서비스가 다운스트림 서비스를 직접 호출:
┌─────────────┐
│ 주문 서비스 │
└──────┬──────┘
│
├───────────┬───────────┬───────────┐
▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│재고 서비스│ │결제 서비스│ │물류 서비스│ │SMS 서비스│
│ 200ms │ │ 500ms │ │ 300ms │ │ 100ms │
└──────────┘ └──────────┘ └──────────┘ └──────────┘📊 문제점 분석 표
| 문제점 | 구체적 현상 | 결과 |
|---|---|---|
| 연쇄 장애 | 재고 서비스 다운, 주문 서비스 동기 호출 타임아웃 | 주문 서비스 스레드 풀 고갈, 새 요청 처리 불가 |
| 응답 지연 | 모든 다운스트림 서비스 응답을 기다려야 함 | 사용자 1초 이상 대기, 극도로 나쁜 사용자 경험 |
| 확장 어려움 | 포인트 서비스 추가 시 주문 서비스 코드 수정 필요 | 릴리스 주기 증가, 리스크 증가 |
| 자원 낭비 | 주문 서비스가 SMS 서비스를 기다려야 함 | DB 커넥션 장시간 점유 |
3.2 디커플링 방안: 메시지 큐를 "중간 계층"으로 도입
디커플링 후의 아키텍처:
주문 서비스는 메시지 발행만 담당, 누가 소비하는지 신경 쓰지 않음:
┌─────────────┐
│ 주문 서비스 │ ──"주문 생성" 메시지 발행──┐
└─────────────┘ │
▼
┌───────────────────┐
│ 메시지 큐 │
│ (Kafka/RabbitMQ) │
│ - 안정적 저장 │
│ - 다중 레플리카 │
│ - 순서 보장 │
└─────────┬─────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 재고 서비스 │ │ 결제 서비스 │ │ 물류 서비스 │
│ 주문 이벤트 │ │ 주문 이벤트 │ │ 주문 이벤트 │
│ 구독 │ │ 구독 │ │ 구독 │
└──────────────┘ └──────────────┘ └──────────────┘✨ 디커플링의 장점
| 차원 | 디커플링 전 | 디커플링 후 |
|---|---|---|
| 장애 격리 | 재고 장애 = 주문 장애 | 재고 장애 시, 메시지 큐에 임시 저장, 복구 후 소비 |
| 응답 시간 | 1000ms (동기 대기) | 50ms (메시지 발행 후 즉시 응답) |
| 확장성 | 새 서비스 추가 시 주문 코드 수정 필요 | 새 서비스는 토픽만 구독하면 됨 |
| 시스템 복잡도 | 주문 서비스가 다운스트림에 강하게 의존 | 주문 서비스는 메시지 큐에만 의존 |
3.3 디커플링의 본질: "직접 호출"에서 "이벤트 기반"으로
사고방식의 전환:
전통적 사고 (명령형):
"주문 서비스가 재고 서비스에 명령한다: 재고를 차감해라!"
↓ 직접 호출
↓ 높은 결합도, 호출 대상이 반드시 온라인이어야 함
↓ 호출자가 호출 대상의 인터페이스를 알아야 함
이벤트 기반 사고 (선언형):
"주문 서비스가 선언한다: 주문이 생성되었다, 필요한 사람이 처리하라."
↓ 이벤트를 메시지 큐에 발행
↓ 디커플링, 컨슈머는 오프라인 가능
↓ 프로듀서는 컨슈머의 존재를 알 필요 없음4. 핵심 문제 2: 어떻게 피크를 깎고 골짜기를 채워(Peak Shaving), 트래픽 급증에 대응할 것인가?
4.1 플래시 세일 시나리오: 10만 QPS를 어떻게 안정적으로 처리할까?
시나리오 재현: 한 이커머스 플랫폼의 광군제 플래시 세일 이벤트, 예상 피크 10만 QPS, 그러나 DB는 1000 QPS밖에 감당 불가.
직접 충격의 결과:
사용자 요청 ──→ 애플리케이션 서버 ──→ 데이터베이스
10만/s 10만/s 1000/s (한계)
↓
커넥션 풀 고갈
응답 타임아웃
데이터베이스 크래시
↓
눈사태 효과 (DB에 의존하는 모든 서비스 장애)🌊 용어 설명
QPS (Queries Per Second): 초당 쿼리 수, 시스템 처리량을 측정하는 지표.
10만 QPS는 매초 10만 개의 요청이 들어오는 것을 의미하며, 10만 명이 동시에 매장으로 몰려드는 것과 같다.
4.2 피크 셰이빙 방안: 메시지 큐를 "저수조"로 활용
아키텍처 설계:
┌───────────────────────────────────────────────────────────────────────┐
│ 플래시 세일 시스템 아키텍처 │
├───────────────────────────────────────────────────────────────────────┤
│ │
│ 제1 계층: 게이트웨이 계층 (하드 리밋) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ - 토큰 버킷 속도 제한: 10만/s → 1만/s (90% 요청 폐기) │ │
│ │ - CDN 정적 리소스 캐싱 (상품 상세 페이지) │ │
│ │ - 캡차/대기 페이지 (1차 피크 셰이빙) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 제2 계층: 서비스 계층 (소프트 리밋) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ - Nginx 속도 제한: 1만/s → 5000/s │ │
│ │ - Redis 재고 사전 차감 (원자적 연산): │ │
│ │ * Lua 스크립트로 원자성 보장 │ │
│ │ * 재고 부족 시 즉시 "품절" 반환 │ │
│ │ - 주문 토큰 생성 (대기 증명서) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 제3 계층: 메시지 큐 계층 (핵심 피크 셰이빙) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Kafka/RocketMQ: │ │
│ │ - 배치 쓰기: 5000/s → 1000/s (DB 감당 능력) │ │
│ │ - 메시지 영속화: 디스크 기록으로 메시지 손실 방지 │ │
│ │ - 다중 파티션 병렬 소비: 처리량 향상 │ │
│ │ - 컨슈머 오프셋 관리: 장애 복구 지원 │ │
│ │ │ │
│ │ 핵심 지표 모니터링: │ │
│ │ - 생산 속도 (Produce Rate) │ │
│ │ - 소비 속도 (Consume Rate) │ │
│ │ - 메시지 적체 (Lag) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 제4 계층: 소비 계층 (비동기 처리) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 주문 처리 컨슈머 (다중 인스턴스): │ │
│ │ - Kafka에서 메시지 풀링 (1000/s, DB 성능에 맞춤) │ │
│ │ - DB 트랜잭션: 주문 생성 + 재고 차감 │ │
│ │ - 주문 상태를 "생성됨"으로 업데이트 │ │
│ │ - 주문 생성 성공 알림 발송 (이메일/SMS/푸시) │ │
│ │ - 메시지 소비 확인 (ACK) │ │
│ │ │ │
│ │ 컨슈머 확장 전략: │ │
│ │ - Lag > 10000 시, 컨슈머 인스턴스 자동 증가 │ │
│ │ - Lag < 1000 시, 컨슈머 인스턴스 감소 (비용 절감) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────┘After the traffic peak passes, the system keeps processing the backlog at full speed until the queue is empty. This is peak shaving.
4.3 피크 셰이빙의 수학적 원리
트래픽 평활화 효과:
원본 트래픽 (피크): 평활화된 트래픽:
10만/s │ ╱╲ 1000/s │████████████████
│ ╱ ╲ │
│ ╱ ╲ │
1000/s│╱ ╲ 0/s │
└─────────────── └────────────────
0s 1s 2s 0s 20s
원본: 10만/s 피크, 1초 지속
평활화: 1000/s 일정 속도, 100초 지속핵심 공식:
큐 길이 = 프로듀서 속도 × 지속 시간 - 컨슈머 속도 × 지속 시간
= 100,000 × 1 - 1,000 × 1
= 99,000 개의 메시지 (피크 시 큐 적체량)
모든 메시지 소비 완료 시간 = 큐 길이 / 컨슈머 속도
= 99,000 / 1,000
= 99 초5. 핵심 문제 3: 메시지 손실, 중복, 순서를 어떻게 보장할 것인가?
5.1 메시지 신뢰성: 3중 방어선
메시지는 세 가지 단계에서 손실될 수 있다: 프로듀서 전송 시, 브로커 저장 시, 컨슈머 처리 시.
🛡️ 3중 방어선
방어선 1: 프로듀서 확인 (Producer ACK)
- 메시지 전송 시, 브로커가 수신 확인할 때까지 대기
- 확인을 받지 못하면 재시도하거나 로컬 로그에 기록
방어선 2: 브로커 영속화
- 메시지를 디스크에 기록, 메모리에만 두지 않음
- 다중 레플리카 동기화로 데이터 손실 방지
방어선 3: 컨슈머 확인 (Consumer ACK)
- 메시지 처리 완료 후, 수동 확인 (ACK)
- 처리 실패 시 확인하지 않음, 브로커가 재전달
5.2 메시지 중복 소비를 어떻게 처리할 것인가?
메시지 중복은 다음 시나리오에서 발생할 수 있다:
- 프로듀서 재시도: 프로듀서가 메시지 전송 후 ACK를 받지 못해 동일 메시지 재전송
- 컨슈머 ACK 타임아웃: 컨슈머가 처리를 완료했지만 ACK가 타임아웃되어 브로커가 재전달
- 네트워크 지터: 컨슈머 ACK가 브로커에 도달하지 못해 브로커가 미소비로 간주
- 컨슈머 재시작: 컨슈머 재시작 후 동일한 배치의 메시지를 다시 소비
💡 멱등성
멱등성(Idempotence): 동일한 작업을 여러 번 실행해도 한 번 실행한 것과 결과가 같다.
일상 속 멱등성:
- 멱등: 엘리베이터 버튼 누르기 (10번 누르나 1번 누르나 엘리베이터는 온다)
- 비멱등: 계좌 이체 (10원 이체를 두 번 실행하면 20원이 이체된다)
기술적 해결 방안: 각 메시지에 고유 ID를 생성하고, 처리 전에 이미 처리되었는지 확인한다.
6. 실전: 어떻게 메시지 큐를 선택할 것인가?
6.1 4대 주요 메시지 큐 비교
| 특성 | RabbitMQ | Kafka | RocketMQ | Redis Stream |
|---|---|---|---|---|
| 포지셔닝 | 전통적 메시지 큐 | 분산 로그 스트림 | 이커머스급 메시지 큐 | 경량 큐 |
| 처리량 | ~1만/초 | ~100만/초 | ~10만/초 | ~5만/초 |
| 지연 | 마이크로초급 | 밀리초급 | 밀리초급 | 밀리초급 |
| 신뢰성 | 높음 (영속화) | 높음 (다중 레플리카) | 높음 (동기 플러시) | 중간 (AOF) |
| 메시지 재처리 | 미지원 | 지원 | 지원 | 지원 |
| 트랜잭션 메시지 | 지원 (약함) | 미지원 | 지원 (강함) | 미지원 |
| 지연 메시지 | 지원 | 미지원 | 지원 | 미지원 |
| 적합한 시나리오 | 전통적 엔터프라이즈 앱 | 로그, 빅데이터 | 이커머스, 금융 | 소규모 앱 |
💡 선택 가이드
의사 결정 트리:
메시지 큐 선택:
│
├─ 트랜잭션 메시지 (분산 트랜잭션) 필요?
│ ├─ 예 → RocketMQ (우선) 또는 RabbitMQ
│ └─ 아니오 → 계속
│
├─ 대량 로그/실시간 스트림 처리 필요?
│ ├─ 예 → Kafka (우선)
│ └─ 아니오 → 계속
│
├─ QPS > 1만/초?
│ ├─ 예 → RocketMQ 또는 Kafka
│ └─ 아니오 → 계속
│
├─ 복잡한 라우팅 (예: headers 매칭) 필요?
│ ├─ 예 → RabbitMQ
│ └─ 아니오 → 계속
│
├─ 기존 Redis 인프라 보유?
│ ├─ 예 → Redis Stream (빠른 시작)
│ └─ 아니오 → RabbitMQ (풍부한 기능, 적절한 학습 곡선)7. 정리: 메시지 큐 설계 철학
7.1 핵심 원칙 복습
| 원칙 | 의미 | 실천 포인트 |
|---|---|---|
| 디커플링 | 서비스 간 직접 의존 없음 | 메시지 큐로 통신, 컨슈머 장애가 프로듀서에 영향 없음 |
| 피크 셰이빙 | 트래픽 변동 평활화 | 메시지 큐를 저수조로, 컨슈머는 일정 속도로 처리 |
| 신뢰성 | 메시지 손실 없음 | 프로듀서 확인 + 브로커 영속화 + 컨슈머 확인 |
| 멱등성 | 중복 소비 무영향 | 비즈니스 레벨에서 멱등성 보장 (유니크 키, 상태 머신) |
| 순서 보장 | 메시지 순서 보장 | 단일 파티션 순서 또는 컨슈머 측 정렬 |
7.2 설계 체크리스트
메시지 큐를 도입하기 전에, 스스로에게 다음 질문을 던져보자:
- [ ] 정말 메시지 큐가 필요한가? (간단한 비동기는 스레드 풀로 가능)
- [ ] 메시지 손실이 허용 가능한가? (신뢰성 수준 결정)
- [ ] 메시지 중복이 비즈니스에 영향을 미치는가? (멱등성 투자 결정)
- [ ] 메시지 순서가 중요한가? (파티션 전략 결정)
- [ ] 컨슈머 처리 능력은 어느 정도인가? (큐 크기와 알람 임계값 결정)
- [ ] 소비 실패를 어떻게 처리할 것인가? (재시도 및 데드 레터 전략 결정)
8. 용어 빠른 참조표
| 용어 | 전체 이름 | 설명 |
|---|---|---|
| MQ | Message Queue | 메시지 큐. 비동기 통신을 위한 미들웨어, 프로듀서와 컨슈머의 디커플링 구현. |
| Producer | - | 프로듀서. 메시지를 전송하는 측. |
| Consumer | - | 컨슈머. 메시지를 수신하고 처리하는 측. |
| Broker | - | 브로커. 메시지를 저장하고 전달하는 서버 프로그램. |
| Topic | - | 토픽. 메시지의 논리적 분류 (예: "orders"). |
| Queue | - | 큐. 메시지를 저장하는 물리적 컨테이너. |
| Partition | - | 파티션. Kafka의 개념, 하나의 Topic을 여러 Partition으로 나누어 동시성 향상. |
| ACK | Acknowledgment | 확인. 컨슈머가 메시지 처리 완료 후 브로커에 확인. |
| Pub/Sub | Publish/Subscribe | 발행-구독. 하나의 메시지를 여러 컨슈머가 수신할 수 있는 메시지 패턴. |
| P2P | Point-to-Point | 점대점. 하나의 메시지를 하나의 컨슈머만 수신할 수 있는 메시지 패턴. |
| DLQ | Dead Letter Queue | 데드 레터 큐. 소비할 수 없는 메시지를 보관. |
| Idempotence | - | 멱등성. 여러 번 실행해도 결과가 같음. |
| Throughput | - | 처리량. 단위 시간당 처리되는 메시지 수. |
| Latency | - | 지연. 메시지가 전송된 시점부터 수신된 시점까지의 시간 차이. |
| Persistence | - | 영속화. 메시지를 디스크에 기록하여 메모리에만 두지 않음. |
| Replication | - | 레플리카. 고가용성을 위해 메시지를 여러 노드에 복제. |
| Transaction Message | - | 트랜잭션 메시지. 로컬 트랜잭션과 메시지 전송의 일관성을 보장. |
| Backpressure | - | 배압. 컨슈머가 처리할 수 없을 때 프로듀서에게 속도 감속을 알림. |
| Offset | - | 오프셋. 컨슈머가 파티션 내에서 소비한 위치. |
| Rebalance | - | 리밸런싱. 컨슈머 그룹 멤버 변경 시 파티션 재할당. |