[참고 자료]
현대 서버 애플리케이션 구조상 메시징 시스템은 필연적이다.
로직을 분리해 응답 속도 향상, 안정성, 실행 보장, 다른 애플리케이션 서버와 통신 등 목적은 다양할 것이다.
그리고, 많은 개발팀들이 어떤 기술을 사용할 지 고민할것이다.
나 또한 "우리 팀은 왜 Kafka 가 아닌 RabbitMQ 를 사용할까?" 라는 궁금증에서 시작했고,
그 과정에서 나름대로의 답을 찾은거 같아서 공유하기 위해 작성했다.

RabbitMQ는 AMQP(Advanced Message Queuing Protocol) 를 구현한 메시지 브로커이며, MQTT 프로토콜도 제공한다.
중심 아이디어는 브로커(RabbitMQ)가 라우팅, 메시지 보관 및 전달 로직의 대부분을 담당
AMQP?
Advanced Message Queueing Protocol: 메시지 지향 미들웨어를 위한 표준 프로토콜
서로 다른 시스템, 애플리케이션이 메시지를 안정적으로 주고 받게 해주는 규약
- 상호 운용성: 서로 다른 언어, 플랫폼으로 개발되어도 원활히 통신
- 안정성, 신뢰성: 메시지를 유실되지 않고 안정적으로 전달되는 것을 보장 - 메시지 확인 (ACK), 메시지 영속성, 트랜잭션
- 유연한 라우팅: Exchange 를 통해 라우팅 - 생산자는 Exchange 에 보내고, Exchange 가 설정 규칙에 따라 적절한 큐로 메시지 분배
"이 Exchange는 라우팅 규칙에 따라 이 Queue로 메시지를 보내라")핵심 흐름: Producer → Exchange → (Binding Rule) → Queue → Consumer
=> 우체국(브로커)이 모든 편지를 분류하고 주소를 알려주며, 집배원(소비자)이 자신의 구역에 할당된 편지를 배달하는 시스템

Kafka는 분산 스트리밍 플랫폼이며, 메시지를 변경 불가능한 로그(immutable log)의 연속된 스트림으로 취급
브로커는 데이터를 저장하고 관리, 복잡한 라우팅 로직은 소비자가 담당
Broker (브로커): Kafka 서버 인스턴스. 메시지를 저장하고 관리하며, 여러 브로커가 모여 Cluster를 구성
Cluster (클러스터): 여러 브로커로 구성되어 데이터 복제, 장애 허용(fault tolerance), 고가용성을 제공


Producer (생산자): 메시지(레코드)를 생성하여 특정 Topic으로 전송
Consumer (소비자): Topic에서 메시지를 가져와 처리
Offset (오프셋): 파티션 내에서 각 메시지가 갖는 고유한 순번(ID)입니다. 소비자는 오프셋을 통해 어디까지 메시지를 읽었는지 추적하고 제어
여기까지 오프셋을 처리했다는 것을 확인하는 오프셋어디까지 메시지를 읽었는지는 것을 확인하는 오프셋Consumer Group (소비자 그룹): 하나 이상의 소비자를 묶은 그룹. 하나의 Topic에 대해, 각 파티션은 소비자 그룹 내의 단 하나의 소비자에게만 할당
하나의 토픽 내 파티션 개수보다 더 많은 컨슈머를 추가하는게 의미 없음
핵심 흐름: Producer → Topic (Partition) → Consumer (Consumer Group)
=> 거대한 도서관 (브로커) 은 책 (메시지) 을 책장 (파티션) 에 계속 꽂아두기만 할 뿐
독자 (소비자) 가 직접 도서관 찾아와 자기가 읽을 부분 (오프셋) 을 기억하고, 다음 읽을 책도 꺼내가는 형태
Gemini 비유 좋다...!
두 개가 구조가 비슷하고, 성능만 차이 난다고 오해를 할 수 있는데 (또는, RabbitMQ 가 사용하지 않는 메시징 시스템이라던가...)
이 개념들을 명확히 이해해야 두 시스템의 아키텍처 철학 차이를 제대로 파악할 수 있다.
팬아웃: 하나의 메시지를 여러 독립적 소비자가 각각 동일한 복사본 받아 처리하는 패턴
가장 흔한 오해 중 하나는 Kafka는 Pub/Sub, RabbitMQ는 Work-Queue라는 이분법적 시각이다.
사실 Kafka는 이 두 가지 모델을 소비자 그룹(Consumer Group) 이라는 개념을 통해 우아하게 통합해준다고 한다.
그룹 간(Inter-Group)에는 Pub/Sub (팬아웃/방송):
서로 다른 소비자 그룹은 같은 토픽을 구독하더라도 메시지 스트림 전체를 독립적으로 소비한다.
EX) order-events라는 토픽이 있을 때, 재고 관리 서비스(그룹 A)와 데이터 분석 팀(그룹 B) 등 각각 별개의 소비자 그룹으로 구독이 가능하다.
이 경우, 두 그룹 모두 order-events의 모든 메시지를 처음부터 끝까지 받고 개별적으로 처리할 수 있다.
그룹 내(Intra-Group)에서는 Work-Queue (분산 처리):
단, 하나의 같은 소비자 그룹 내에서는 이야기가 달라진다. 그룹에 속한 소비자가 토픽의 파티션(Partition)들을 나누어 처리한다. (하나의 파티션은 그룹 내 단 하나의 소비자에게만 할당)
만약 토픽에 4개의 파티션이 있고, 그룹에 4개의 소비자가 있다면, 각 소비자는 하나의 파티션을 전담하여 메시지를 처리한다.
-> 이는 작업 부하를 분산하고 처리량을 높이는 '워크큐' 모델과 동일하게 처리
핵심: Kafka는 소비자 그룹을 통해 서로 다른 시스템 간에는 데이터를 복제/방송하고, 단일 시스템 내에서는 작업을 분산하여 처리량을 극대화하는 두 가지 방식 모두 적용
두 시스템의 가장 근본적인 차이는 메시지를 다루는 방식이다.
Kafka: 데이터는 '변경 불가능한 로그(Immutable Log)'
Kafka는 소비자가 메시지를 읽어가도 즉시 삭제하지 않는다. 메시지는 설정된 보존 기간(예: 7일) 또는 용량에 도달할 때까지 토픽에 안전하게 보관한다.
소비자는 단지 '어디까지 읽었는지'를 나타내는 오프셋(Offset) 만 관리한다.
RabbitMQ: 데이터는 '처리해야 할 일(Transient Task)'
전통적인 RabbitMQ에서 메시지는 '처리되어야 할 작업'. 소비자가 메시지를 가져가 성공적으로 처리했다고 확인(ack) 신호를 보내면, 메시지는 큐에서 영구적으로 제거된다.
이 방식 특징:
이메일을 보내라, 이미지를 생성해라 ,"이미지를 최적화해라" 와 같이 한 번 처리되고 나면 더 이상 필요 없는 작업들을 관리하는 데 매우 효율적이다.참고: RabbitMQ도 시대의 흐름에 맞춰 Streams라는 새로운 큐 타입을 도입. (Kafka처럼 오프셋 기반의 비파괴 소비를 지원하여 로그와 같은 동작을 유사)
하지만 RabbitMQ 는 기본적으로 '소비-제거' 방식의 큐를 위해 사용한다.
RabbitMQ도 팬아웃의 방식은 Kafka와는 다르다. RabbitMQ의 라우팅 능력의 핵심에는 교환기(Exchange) 를 통해 수행한다.
생산자는 메시지를 큐에 직접 보내는 것이 아니라, 교환기에 보낸다. 그러면 교환기가 설정된 타입과 규칙에 따라 메시지를 어떤 큐에 보낼지 결정한다.
fanout 교환기: 자신에게 연결(binding)된 모든 큐에 메시지를 복사해서 보낸다. 가장 순수한 형태의 방송(Broadcast) 모델topic 교환기: 라우팅 키와 바인딩 패턴을 와일드카드(*, #)로 매칭, 조건에 맞는 큐에만 메시지를 선택해서 보낸다. (멀티캐스트)direct 교환기: 라우팅 키가 바인딩 키와 정확히 일치하는 큐에만 메시지를 보냅니다. (유니캐스트)핵심: RabbitMQ에서는 브로커(교환기)가 '스마트'하게 라우팅 규칙을 해석하여 메시지를 분배
반면 Kafka에서는 생산자가 토픽을 지정하고, 소비자가 '스마트'하게 그룹을 지어 메시지를 가져간다.
이제 두 시스템의 구체적인 기능과 특성을 7가지 핵심 주제로 나누어 깊이 있게 비교해보자.

철학: Kafka는 데이터를 '일시적인 메시지'가 아닌 '영구적인 사실의 기록(log of facts)'으로 취급한다.
메시지는 소비되어도 삭제되지 않고 retention.ms (시간) 또는 retention.bytes (용량) 설정에 따라 디스크에 보관된다.
auto.offset.reset = 'earliest' 설정을 하면 토픽의 가장 처음부터 모든 이벤트를 다시 읽어올 수 있다.cleanup.policy=compact로 설정하면, Kafka는 토픽의 모든 메시지를 보관하지 않고 각 메시지 키(key)에 대한 가장 최신 값만을 유지한다.Kafka Log Compaction 내용을 참고
RabbitMQ: 소비 후 제거되는 작업 큐
ack) 큐에서 제거한다. (데이터 보존보다 안정적인 작업 전달에 초점을 맞춘다)ack되면 사라지므로 Kafka와 같은 자유로운 재생이 불가능하다.nack (또는 reject)하여 큐에 다시 넣는 방식을 사용한다.Kafka: 파티션 단위의 엄격한 순서 보장
user_id 관련 이벤트를 동일한 user_id를 키로 전송하면, Kafka는 해시 값을 계산해 항상 동일한 파티션으로 해당 이벤트를 보낸다. - 모든 이벤트 순서대로 처리)RabbitMQ: 경쟁적 소비자 환경에서의 순서 불확실성
RabbitMQ 로 순서를 제어하는건 생각보다 꽤나 어렵다.. 메시지를 기반으로 다음 메시지를 연쇄적으로 발행시키는 방법으로 순서를 보장하자.
Kafka: Exactly-Once (정확히 한 번 전달) 지원
[참고 자료] Kafka의 Exactly-Once 심층 분석:
Exactly-Once Semantics Are Possible: Here’s How Kafka Does It 내용 참고
RabbitMQ: At-least-once가 기본, Exactly-once는 애플리케이션의 몫
At-least-once (최소 한 번): 소비자가 메시지를 처리하고 브로커에게 ack를 보내기 전에 연결이 끊어지면, 브로커는 해당 메시지를 다른 소비자에게 다시 전달하여 중복이 발생할 수 있다.
At-most-once (최대 한 번): auto-ack 모드를 사용하면, 소비자가 메시지를 받는 즉시 브로커가 ack된 것으로 간주한다. 만약 소비자가 메시지를 처리하던 중 실패하면 해당 메시지는 유실된다.
(서비스 특성에 따라 주의깊게 사용)
Exactly-once의 부재: RabbitMQ는 프로토콜/브로커 수준에서 EOS를 직접 지원하지 않는다. EOS를 구현하려면, 소비자가 직접 멱등성 로직을 구현해야 한다.
근데, At-least-once 가 전혀 나쁜 설정이 아니다.
왜냐하면, 소비자가 원한 특정 로직을 무조건 해줘야 할 필요가 있는데 특정 컨슈머가 처리 못해도 다른 컨슈머가 무조건 처리해주는 걸 보장해줄 수 있기 때문이다.
데이터 처리한 내용을 DB 에 반영하는건, 다른 DB 가 다른 메시지로 처리해주면 된다.
Kafka: Pull 기반 (소비자가 주도)
poll() 메소드를 호출할 때만 데이터 전송poll()을 호출하지 않고, 데이터는 브로커에서 대기한다.max.poll.records (한 번에 가져올 최대 메시지 수), fetch.min.bytes (최소 이만큼 데이터가 쌓여야 응답) 등 옵션으로 소비 속도를 세밀하게 튜닝 가능)RabbitMQ: Push 기반 (브로커가 주도) + Prefetch
prefetch 값(QoS - Quality of Service) 을 설정.prefetch 값에 도달하면 더 이상 메시지를 보내지 않고 대기.RabbitMQ에서
prefetch값 설정은 성능과 안정성에 매우 중요. 너무 작으면 네트워크 왕복이 잦아져 처리량이 낮아지고, 너무 크면 소비자의 메모리가 고갈 가능
( 특이한 도메인은 메시지를 무조건 하나씩 받아야 할 수 있다. - AI 이미지 생성 로직이라면 GPU 를 쓰고 한번에 하나의 작업만 보장하기 위해 prefetch 1 )
위에서 이미 작성한 내용이라 간단하게만 작성한다.
Kafka: 단순한 토폴로지, 스마트한 클라이언트
RabbitMQ: 유연한 라우팅, 스마트한 브로커
direct, topic, fanout, headers)과 라우팅 규칙(binding)에 따라 메시지를 적절한 큐로 분배Kafka: 파티션 기반의 완벽한 수평 확장
RabbitMQ: Quorum 큐와 스트림을 통한 현대적인 HA
리더 -팔로워 모델의 문제?
- 쓰기 병목: 메시지 발행하면 무조건 리더 큐에 먼저 도달, 팔로워들이 메시지 복제하고 확인 응답 보낼때 까지 대기
- 읽기 병목:소비자는 리더 큐에만 연결해서 메시지 가져올 수 있음
=> 발행과 소비 트래픽이 하나의 리더 노드에 몰리게 된다.
Kafka: 데이터 파이프라인 플랫폼
Kafka는 단순한 메시지 브로커를 넘어, 그 자체로 하나의 거대한 데이터 플랫폼을 형성
RabbitMQ: 다재다능한 메시지 브로커
RabbitMQ는 특정 목적에 맞춰 유연하게 사용할 수 있는 강력한 브로커 의 느낌
AI 가 요약해 준 내용이자, 팀에서 RabbitMQ 를 왜 쓰는가에 대해 고민할 때 이정도로 생각난 내용으로 당연히 틀릴 내용일 수 있다.
"과거 데이터를 다시 처리해야 하는 경우가 있는가?"
"메시지마다 복잡한 조건에 따라 다른 곳으로 보내야 하는가?"
"주된 용도가 백그라운드에서 실행될 비동기 작업을 처리하는 것인가?"
"수많은 독립적인 서비스들이 동일한 이벤트 스트림을 각자의 목적에 맞게 소비해야 하는가?"
"초당 수십만 건 이상의 매우 높은 처리량이 필수적인가?"
"메시지별 TTL, 지연 처리, 요청-응답(RPC) 패턴이 필요한가?"
꼭 메시지 큐잉 시스템을 Kafka 로 써야한다고 생각에 빠지지 말자.
회사 인프라, 팀 내 기술, 프로젝트 내 요구사항 등 다양한 것들이 고려되어 결정이 되어야 한다.
우리팀의 메시지 큐잉 시스템은
이런점들을 통해 RabbitMQ 를 사용한다고 생각한다.