메시지 큐 — Kafka vs RabbitMQ, 이벤트 드리븐 아키텍처
메시지 큐 관련 핵심 개념들을 한 곳에 정리했다. Kafka와 RabbitMQ 비교, 이벤트 드리븐 아키텍처, 전달 보장 전략까지 실무 맥락과 함께 다룬다.
메시지 큐, 왜 필요한가
서비스가 하나일 때는 메서드 호출 한 번이면 끝이에요. 그런데 서비스가 쪼개지기 시작하면 얘기가 달라집니다.
주문 서비스에서 결제 → 재고 차감 → 알림 발송을 동기로 다 처리한다고 생각해보세요. 결제 API가 3초 걸리면 사용자는 그냥 3초를 기다려야 하고, 알림 서버가 죽어버리면 주문 자체가 실패해요. 말이 안 되는 구조죠.
메시지 큐는 이런 문제를 해결합니다.
- **비동기 처리 **: 응답이 필요 없는 작업은 큐에 던져놓고 바로 리턴하면 됩니다. 사용자 경험이 좋아져요.
- ** 서비스 간 결합도 감소 **: 생산자는 큐에 메시지만 넣으면 되고, 소비자는 큐에서 꺼내 처리하면 됩니다. 서로의 존재를 몰라도 돼요.
- ** 부하 분산(Load Leveling)**: 트래픽이 갑자기 몰려도 큐가 버퍼 역할을 해서 소비자가 자기 속도에 맞춰 처리할 수 있습니다. 스파이크성 트래픽에 강해요.
핵심은 "지금 당장 안 해도 되는 일은 나중에 하자", 그리고 "보내는 쪽과 받는 쪽을 분리하자" 이 두 가지입니다.
메시지 브로커 vs 이벤트 스트리밍 플랫폼
메시지 큐를 쓴다고 할 때, 크게 두 가지 접근 방식이 있어요.
** 메시지 브로커 (Message Broker)**
- 대표: RabbitMQ, ActiveMQ
- 메시지를 소비자에게 ** 전달(push)** 하는 것이 목적
- 소비자가 메시지를 ACK하면 큐에서 삭제됩니다
- 라우팅이 유연하고, 메시지별로 우선순위를 줄 수 있어요
** 이벤트 스트리밍 플랫폼 (Event Streaming Platform)**
- 대표: Apache Kafka, Amazon Kinesis
- 이벤트를 ** 로그처럼 영구 저장 **하고, 소비자가 원하는 시점부터 읽습니다(pull)
- 소비 후에도 데이터가 삭제되지 않아요 (retention 정책에 따라 유지)
- 대용량 스트리밍, 이벤트 재처리에 적합합니다
쉽게 말하면 RabbitMQ는 우체국, Kafka는 신문사에 가깝습니다. 우체국은 편지를 배달하면 끝이고, 신문사는 발행한 신문을 보관하면서 구독자가 원할 때 과거 호도 다시 볼 수 있게 해줘요.
RabbitMQ
RabbitMQ는 AMQP(Advanced Message Queuing Protocol) 기반의 메시지 브로커입니다. Erlang으로 만들어져 있고, 안정성이 검증되어 있어 엔터프라이즈에서 많이 씁니다.
핵심 구조
Producer → Exchange → (Binding) → Queue → Consumer
생산자가 메시지를 직접 큐에 넣는 게 아니라, Exchange에 보내면 Exchange가 바인딩 규칙에 따라 적절한 큐로 라우팅해요.
Exchange 종류
| Exchange | 라우팅 방식 | 용도 |
|---|---|---|
| Direct | Routing Key가 정확히 일치하는 큐에 전달 | 특정 작업을 특정 워커에 분배 |
| Topic | 와일드카드 패턴(*, #) 매칭 | order.created, order.cancelled 같은 패턴 기반 라우팅 |
| Fanout | 바인딩된 모든 큐에 브로드캐스트 | 이벤트를 여러 서비스에 동시에 전파 |
| Headers | 메시지 헤더 값으로 매칭 | Routing Key 대신 헤더 속성 기반 라우팅이 필요할 때 |
Consumer와 ACK
소비자가 큐에서 메시지를 가져가면, 처리 완료 후 ACK(Acknowledge)를 보내야 합니다. ACK를 보내야 RabbitMQ가 해당 메시지를 큐에서 제거해요.
만약 소비자가 처리 중 죽어버리면 어떻게 될까요? ACK가 안 갔으니 RabbitMQ는 메시지를 다시 큐에 올려서 다른 소비자에게 전달합니다. 이게 메시지 신뢰성을 보장하는 핵심 메커니즘이에요.
basic.ack— 처리 완료basic.nack/basic.reject— 처리 실패, 메시지를 다시 큐에 넣거나 버립니다
Kafka
Kafka는 LinkedIn에서 만든 분산 이벤트 스트리밍 플랫폼입니다. 로그 기반으로 데이터를 저장하기 때문에, 메시지 브로커와는 철학 자체가 달라요.
핵심 개념
Topic
메시지를 분류하는 카테고리입니다. order-events, user-activity 같은 이름으로 만들어요.
Partition Topic은 여러 Partition으로 나뉩니다. 파티션이 병렬 처리의 단위가 돼요. 같은 키를 가진 메시지는 같은 파티션에 들어가기 때문에, 파티션 내에서는 순서가 보장됩니다. 전체 Topic 수준에서는 순서 보장이 안 된다는 점이 중요해요.
Consumer Group 여러 Consumer를 하나의 그룹으로 묶으면, 각 파티션을 그룹 내 하나의 Consumer만 담당합니다. 자연스럽게 병렬 처리가 되면서도 메시지 중복 소비를 방지할 수 있어요. 파티션이 3개인데 Consumer가 5개면? 2개는 놀게 됩니다. 반대로 Consumer가 2개면 하나가 파티션 2개를 담당해요.
Offset 각 Consumer가 어디까지 읽었는지 기록하는 값입니다. Consumer가 죽었다 살아나도 마지막 Offset부터 이어서 읽으면 돼요. Offset을 되감으면 과거 데이터를 다시 처리할 수도 있습니다.
** 로그 기반 저장** Kafka는 메시지를 디스크에 순차적으로 append합니다. 소비자가 읽어간다고 삭제하지 않아요. retention 기간(기본 7일)이 지나야 삭제됩니다. 덕분에 새로운 Consumer를 붙여서 과거 이벤트를 처음부터 다시 읽는 것도 가능해요.
Kafka vs RabbitMQ 비교
| 항목 | Kafka | RabbitMQ |
|---|---|---|
| ** 처리량** | 초당 수십만~수백만 건 | 초당 수만 건 |
| ** 순서 보장** | 파티션 내 보장 | 큐 내 보장 |
| ** 메시지 저장** | 디스크에 로그로 보관 (retention 설정) | 소비 후 삭제 |
| ** 재처리** | Offset 되감기로 가능 | 기본적으로 불가 (DLQ로 우회) |
| ** 라우팅** | Topic 기반, 단순 | Exchange/Binding으로 유연한 라우팅 |
| ** 프로토콜** | 자체 바이너리 프로토콜 | AMQP |
| ** 소비 방식** | Consumer가 Pull | Broker가 Push (Pull도 가능) |
| ** 적합한 용도** | 로그 수집, 이벤트 소싱, 실시간 스트리밍 | 작업 큐, RPC, 라우팅이 복잡한 시나리오 |
한 줄로 요약하면: ** 대용량 + 재처리가 필요하면 Kafka, 라우팅이 복잡하고 메시지별 세밀한 제어가 필요하면 RabbitMQ입니다.**
이벤트 드리븐 아키텍처
이벤트 드리븐 아키텍처(EDA)는 시스템 간 통신을 이벤트 발행/구독으로 처리하는 아키텍처 패턴입니다. 서비스끼리 직접 호출하지 않고, "이런 일이 일어났어"라는 이벤트를 발행하면 관심 있는 서비스가 알아서 가져가는 방식이에요.
이벤트 소싱 (Event Sourcing)
현재 상태를 직접 저장하는 대신, ** 상태 변경 이벤트를 순서대로 저장 **하는 패턴입니다.
예를 들어 계좌 잔액이 10만 원이라고 바로 저장하는 게 아니라:
계좌생성 → 입금 5만원 → 입금 8만원 → 출금 3만원
이렇게 이벤트 시퀀스를 저장하고, 필요할 때 이벤트를 replay해서 현재 상태를 구합니다. 은행 거래 내역을 떠올리면 이해가 빠를 거예요. 잔액만 저장하면 "왜 이 금액이 됐지?" 추적이 불가능한데, 이벤트를 다 기록하면 언제든 추적할 수 있습니다.
장점은 완벽한 감사 로그(audit log)가 생기고, 특정 시점으로 상태를 복원할 수 있다는 거예요. 단점은 이벤트가 쌓이면 replay가 느려지기 때문에 스냅샷을 주기적으로 찍어줘야 합니다.
CQRS (Command Query Responsibility Segregation)
읽기(Query)와 쓰기(Command)를 분리하는 패턴입니다. 쓰기 모델과 읽기 모델을 각각 최적화할 수 있어요.
이벤트 소싱과 같이 쓰이는 경우가 많습니다. 쓰기는 이벤트 저장소에 이벤트를 쌓고, 읽기는 이벤트를 materialized view 형태로 가공해서 빠르게 조회해요.
[Command] → Write Model (Event Store)
↓ (이벤트 발행)
[Query] ← Read Model (Materialized View)
강력하지만 복잡도가 올라가기 때문에, 읽기/쓰기 패턴이 극단적으로 다를 때만 도입하는 게 맞습니다. 단순 CRUD에 CQRS 도입하면 오버엔지니어링이에요.
메시지 전달 보장
분산 시스템에서 메시지 전달에는 세 가지 보장 수준이 있습니다.
At-most-once (최대 한 번)
메시지를 보내고 확인하지 않습니다. 유실될 수 있지만 중복은 없어요. 성능은 가장 좋습니다.
→ 로그, 메트릭 수집처럼 일부 유실이 괜찮은 경우에 사용해요.
At-least-once (최소 한 번)
메시지 전달을 확인하고, 실패하면 재전송합니다. 유실은 없지만 중복이 발생할 수 있어요.
→ 대부분의 실무 시스템이 이 수준입니다. 소비자 측에서 ** 멱등성(Idempotency)** 을 보장해야 중복 처리 문제가 안 생겨요.
Exactly-once (정확히 한 번)
유실도 없고 중복도 없습니다. 이상적이지만 구현이 어려워요.
→ Kafka는 0.11 버전부터 Idempotent Producer + Transactional API로 Exactly-once semantics를 지원합니다. 다만 이건 Kafka 내부에서의 보장이고, 외부 시스템까지 포함하면 결국 At-least-once + 멱등성 조합이 현실적이에요.
실무 활용 패턴
주문 처리
주문 생성 → [메시지 큐] → 결제 서비스
→ 재고 서비스
→ 알림 서비스
주문 서비스는 "주문이 생성됐다"는 이벤트만 발행하면 끝입니다. 각 서비스가 독립적으로 처리해요. 알림 서비스가 잠시 죽어도 주문은 정상 처리되고, 알림은 서비스가 복구되면 큐에서 밀린 메시지를 가져가서 처리합니다.
알림 시스템
이메일, SMS, 푸시 알림을 동기로 보내면 응답 시간이 길어집니다. 큐에 알림 요청을 넣으면 사용자에게는 바로 응답을 주고, 워커가 비동기로 발송을 처리해요. 발송 실패 시 재시도 로직도 큐 레벨에서 깔끔하게 처리할 수 있습니다.
로그 수집
애플리케이션 → Kafka → Elasticsearch/S3 파이프라인이 전형적입니다. 수십 대 서버에서 쏟아지는 로그를 직접 Elasticsearch에 넣으면 부하가 심하지만, Kafka를 버퍼로 두면 Elasticsearch가 자기 속도에 맞춰 소비하면 돼요. Kafka의 retention 덕분에 Elasticsearch가 잠시 장애가 나도 로그가 유실되지 않습니다.
주의할 점
Kafka가 빠른 이유
Sequential I/O Kafka는 디스크에 데이터를 순차적으로 씁니다. 랜덤 I/O 대비 순차 I/O가 훨씬 빠른 건 디스크 특성상 당연한 건데, 실제로 순차 쓰기는 메모리 랜덤 접근보다도 빠를 수 있어요. Kafka가 디스크 기반인데도 빠른 이유가 바로 이것입니다.
Zero-Copy
일반적으로 데이터를 네트워크로 보내려면 커널 버퍼 → 유저 버퍼 → 소켓 버퍼 → NIC 이런 복사가 일어납니다. Kafka는 sendfile() 시스템 콜로 커널 버퍼에서 바로 NIC로 전송해요. 불필요한 복사가 없으니 CPU 사용량과 지연이 줄어듭니다.
** 배치 처리** Producer도, Consumer도 메시지를 개별로 보내지 않고 배치로 묶어서 처리합니다. 네트워크 왕복 횟수가 줄어들어요.
Consumer Lag
Consumer가 Producer를 따라가지 못해서 밀리는 양을 Consumer Lag이라고 합니다. 현재 Offset - 마지막으로 소비한 Offset = Lag인데, 이 값이 계속 증가하면 Consumer가 처리 속도를 못 따라가고 있다는 의미예요. 모니터링 도구로 체크해야 하고, Lag이 커지면 Consumer를 늘리거나 파티션을 추가해서 병렬 처리를 확대합니다.
Dead Letter Queue (DLQ)
처리에 계속 실패하는 메시지를 무한 재시도하면 시스템이 멈춥니다. 일정 횟수 재시도 후에도 실패하면 DLQ라는 별도 큐로 보내서 나중에 원인을 분석하거나 수동 처리해요. RabbitMQ에서는 DLX(Dead Letter Exchange)라는 이름으로 지원하고, Kafka에서는 별도 토픽을 DLQ로 사용하는 패턴을 직접 구현합니다.
파생 개념
메시지 큐와 이벤트 드리븐 아키텍처를 공부하다 보면 자연스럽게 연결되는 개념들이 있어요.
MSA (Microservice Architecture)
메시지 큐가 진가를 발휘하는 환경이 바로 MSA입니다. 서비스 간 동기 호출을 줄이고 이벤트 기반으로 통신하면 각 서비스의 독립성이 높아져요. 하나의 서비스가 장애 나도 다른 서비스에 영향이 최소화됩니다.
분산 트랜잭션과 SAGA 패턴
모놀리식에서는 하나의 트랜잭션으로 묶으면 끝이었는데, MSA에서는 서비스마다 DB가 다르니까 분산 트랜잭션 문제가 생깁니다. 2PC(Two-Phase Commit)는 성능이 나쁘고 가용성도 떨어져요.
SAGA 패턴은 각 서비스의 로컬 트랜잭션을 순차적으로 실행하고, 중간에 실패하면 보상 트랜잭션(Compensating Transaction)을 실행해서 되돌립니다.
- Choreography: 각 서비스가 이벤트를 발행하고, 다음 서비스가 구독해서 처리해요. 중앙 조정자가 없어서 단순하지만, 흐름 파악이 어렵습니다.
- Orchestration: 중앙 Orchestrator가 각 서비스에 명령을 내립니다. 흐름이 한눈에 보이지만, Orchestrator가 단일 장애점이 될 수 있어요.
Docker / Kubernetes
Kafka나 RabbitMQ를 운영하려면 결국 컨테이너화해서 K8s 위에 올리는 경우가 많습니다. Kafka의 경우 StatefulSet으로 배포하고, Persistent Volume으로 로그 데이터를 보존해요. Helm Chart(Bitnami/Kafka 등)를 쓰면 클러스터 구성이 한결 편해집니다.
정리
| 개념 | 핵심 포인트 |
|---|---|
| 메시지 큐 | 비동기, 결합도 감소, 부하 분산 |
| RabbitMQ | AMQP, Exchange 라우팅, Push 기반, 메시지 소비 후 삭제 |
| Kafka | 로그 기반, Partition/Offset, Pull 기반, 대용량 스트리밍 |
| 이벤트 소싱 | 상태 대신 이벤트 시퀀스를 저장 |
| CQRS | 읽기/쓰기 모델 분리 |
| 전달 보장 | At-most-once / At-least-once / Exactly-once |
| SAGA | 분산 트랜잭션을 로컬 트랜잭션 + 보상 트랜잭션으로 해결 |
Kafka를 왜 선택했는지, RabbitMQ 대신 Kafka인 이유가 뭔지, 어떤 전달 보장 수준으로 운영했는지까지 설명할 수 있어야 합니다. 도구의 특성을 이해하고 상황에 맞는 선택 근거를 설명할 수 있는 게 핵심이에요.