내가 처음으로 메시지 브로커라는 키워드를 접하게 된 것은 채팅 시스템을 구현해보는 과정에서 검색을 하던 도중이었다. 채팅 시스템을 구현함에 있어 Pub/Sub 구조로 구독을 통해 메시지를 전달하게 되는데 이 때, 메시지 브로커에 대한 종류를 알게 되었다. 단순 구독 시스템이기 때문에 Spring 내장 메시지 브로커를 사용했지만, Redis, RabbitMQ 등을 더 향상된 기능으로 사용할 수 있다는 것을 알았다.
여기서 RabbitMQ라는 것을 알게 되었고, 맨 뒤의 MQ를 보면 알겠지만 Message Queue로 비동기 메시징 큐를 통해 시스템의 부하를 줄이고 성능을 높일 수 있다고 한다.
아직까지 적용해볼 기회가 없어서 실제로 적용해본 적은 없지만, 다음 프로젝트에서 사용해볼 생각이며 어떠한 장점이 있길래 메시지 브로커인 RabbitMQ를 사용하며, RabbitMQ를 검색하면 같이 나오는 Kafka는 무엇인지 궁금해 조사하게 되었다.
비동기 메시징
비동기 메시징을 왜 사용하는가?
마이크로서비스 아키텍쳐에서 서비스 간 통신을 구현할 때, 가장 먼저 떠올리는 방법은 HTTP 기반의 동기 통신이다.(ex. FeignClient)
주문 서비스가 재고 서비스를 호출하고, 결제 서비스를 호출해서 주문 생성 이후의 흐름을 HTTP 통신을 통해서 진행하는 것이 하나의 예시이다.

1번 HTTP 요청에 대해서 정상적으로 진행이 된다면, 주문을 생성하고 그 다음 2번 HTTP 요청을 진행하게 된다. 만약 1번 HTTP 요청에서 오류가 발생한다면 2번의 과정으로는 진행이 될 수 없다.
하지만 이러한 동기 통신 방식은 몇 가지 문제를 발생시킬 수 있다.
- 결합도 : 주문 서비스는 재고 서비스의 응답을 기다려야하기 때문에, 재고 서비스가 다운되거나 트래픽이 증가해 느려진다면, 주문 서비스도 함께 느려지는 것처럼 영향을 받게 된다.
- 확장성 : 트래픽이 급증하는 상황에서, 동기 호출은 쉽게 병목 현상이 발생한다. 호출하는 쪽은 응답을 대기하며 쓰레드를 계속해서 점유하게 되고, 호출받는 쪽은 급증하는 트래픽을 감당하지 못해 Timeout 혹은 장애로 이어질 수 있다.

따라서, 이러한 문제를 해결하기 위해 나온 것이 비동기 메시징 패턴이다. 메시지를 생성하는 발행자와, 이를 구독하는 구독자 사이에 메시지 브로커를 두어, 서비스 간 직접적인 호출을 분리한다.
주문 서비스는 주문이 생성되었을 경우, “주문이 생성됨” 이라는 메시지를 브로커에게 발행하고, 이를 구독하는 재고 및 결제 서비스는 필요한 시점에 메시지를 소비하면 된다.
따라서 발행자는 구독자가 누구인지, 어떤 상태인지 몰라도 되고, 구독자가 일시적으로 다운되더라도 메시지는 브로커에 보관되어 있어 복구 되는 시점에서 다시 소비할 수 있다.
즉, 비동기 메시징을 이용해서 서비스 간 느슨한 결합을 달성하고, 서버의 처리 속도에 맞춰 메시지를 처리할 수 있기 때문에 전체적인 시스템의 확장성 및 안정성을 확보할 수 있다.
일반적으로 비동기 메시징은 Message Queue 와 Pub/Sub 의 2가지 방식으로 나뉘게 된다.
Message Queue와 Pub/Sub
비동기 메시징은 크게 Message Queue를 이용한 방식과, Pub/Sub 방식 두 가지로 나뉘게 된다.
Message Queue
Message Queue는 발행자(Publisher)가 메시지를 만들고 이를 큐(Queue)에 보관한다. 소비자(Consumer)는 큐에 보관되어 있는 메시지를 소비하게 된다.

그림과 같이, 소비자들은 동일한 메시지를 처리하는 것이 아니라, 5건의 요청이 오면 그 중 3건의 요청을 나눠서 처리하게 된다.
즉 1건의 메시지 당 1건의 소비가 일어난다. 여러 대의 서버에 로드 밸런서를 적용해 부하를 분산하는 것과 비슷하게 느껴진다. 즉, 작업의 분산 처리에 초점을 맞춘 방식이다.
일반적으로 이메일 발송, 파일 변환, 데이터 동기화 등 즉시 완료될 필요는 없지만 반드시 처리되어야 하는 작업들에서 주로 사용하게 된다.
메시지 큐의 메시지 소비 방식은 크게 두 가지로 나뉜다.
- Pull 방식 : 소비자가 능동적으로 큐에 접근해서 메시지를 가져간다. Kafka의 기본 방식이다
- Push 방식 : 메시지 브로커가 소비자에게 메시즈를 전달하는 방식이다. 큐에 메시지가 쌓이면 브로커가 등록된 소비자들에게 메시지를 보내서 소비시킨다. RabbitMQ가 기본적으로 이 방식을 사용한다
Pub/Sub
Pub/Sub 방식은 이벤트의 브로드 캐스팅에 초점을 맞춘 방식이다.
발행자가 특정 토픽에 메시지를 발생하면, 해당 토픽을 구독하고 있는 모든 구독자에게 메시지가 복사되어 전달된다.
즉, 구독자들은 모두 동일한 내용의 메시지를 받는 것이다. 가장 쉬운 예시로는 채팅방을 들 수 있다. 채팅방에 속한 모든 유저는 동일한 메시지를 받게 되는 것과 같은 것이다.

Pub/Sub 방식의 장점으로는 확장 가능성이다. 새로운 기능을 추가할 때, 이 기능이 이벤트에 반응해야 한다면 단순히 해당 토픽을 구독하기만 하면 된다.
발행자는 누가 이 토픽을 구독하는지는 몰라도 되기 때문에, 결합도 또한 느슨하게 가져갈 수 있다.
동일한 작업을 빠르게 처리해야 한다 → Message Queue
하나의 이벤트에 여러 서비스가 독립적으로 반응해야 한다 → Pub/Sub
일반적으로 많이 사용하는 RabbitMQ / Kafka 모두 Message Queue 방식과 Pub/Sub 방식을 모두 지원한다.
RabbitMQ
RabbitMQ란 AMQP를 구현한 오픈 소스 메시지 브로커이다.
💡 AMQP?
Advanced Message Queing Protocol 의 약자로, 메시지 지향 미들웨어(MOM)을 위한 개방형 표준 응용 계층 프로토콜이다.
서로 다른 시스템들 간의 최대한 효율적인 방법으로 메세지를 교환하기 위해 탄생했다.
메시지 관리, 큐잉, 라우팅(peer to peer, pub-sub), 신뢰성, 보안 등에 대해 정의하고 있다
RabbitMQ의 구조

- Producer : 메시지를 발행하는 주체이다. AMQP, STOMP 등의 프로토콜로 RabbitMQ와 통신하며 메세지를 생성해 전달한다.
- Exchange : Producer로부터 받은 메시지를 어떤 Queue로 보낼지 결정하는 라우터 역할을 한다. 타입에 따라 라우팅 방식이 달라진다.
- Direct : 라우팅 키가 정확히 일치하는 Queue로만 메시지를 보낸다. 로그 레벨별로 메시지를 분류할 때 유용하다.
- Topic : 패턴 매칭을 사용한다. order.created, order.cancelled 같은 라우팅 키를 사용하고, Queue는 order.* 패턴으로 구독할 수 있다.
- Fanout : 라우팅 키를 무시하고 연결된 모든 Queue에 메시지를 브로드캐스트 한다. Pub/Sub패턴으로, 하나의 이벤트를 여러 서비스가 알아야 할 때 사용된다.
- Headers : 라우팅 키 대신 메시지 헤더의 속성을 기반으로 라우팅한다.
- Binding : Exchange와 Queue를 연결하는 역할을 한다. Exchange는 입력된 메세지를 보고 Binding 정보를 참조해 Exchange 타입에 따라 Queue를 선택한다.
- Queue : Exchange에서 라우팅 된 메세지가 적재되는 대기열이다. FIFO 방식으로 동작한다. 여러 속성을 가질 수 있다.
- Durability(영속성) : RabbitMQ가 재시작해도 Queue가 사라지지 않는다.
- TTL(Time To Live) : 일정 시간 이후 메시지가 자동으로 삭제된다.
- Auto Delete : 마지막 Consumer가 삭제되는 경우 Queue를 삭제 가능
- Consumer : Queue의 메세지를 소비하는 주체이다. ACK를 처리하는 전략, 안정성 관련 설정을 커스텀할 수 있다.
- Auto ACK : 메시지 수신 즉시 ACK 전송 - 성능 우선
- Manual ACK : 명시적 ACK 전송 - 안정성 우선
- Exclusive : 특정 Connection에서만 처리 - 독점 작업 처리
- Prefetch : 한 번에 처리할 메시지 개수를 제한 - Worker 과부하 방지
- Priority : Consumer 우선 처리 - 긴급 메시지 처리
RabbitMQ의 장점 및 단점
장점
- 유연한 메시지 라우팅 : Exchange와 Binding을 통한 유연한 라우팅. 하나의 메시지를 여러 Queue로 복사하거나, 특정 조건에 따른 Queue로 보내는 등 유연한 구성 가능
- 즉각적인 메시지 전달 : Push 방식을 통해, Queue에 메시지가 도착하면 즉시 Consumer에게 전달
- 다양한 프로토콜 지원 : AMQP 뿐 아니라 STOMP, MQTT, HTTP 등 다양한 프로토콜을 지원한다.
단점
- 제한적인 처리량 : 초당 수만 건의 메시지를 처리할 수 있지만, Kafka와 비교하면 낮은 처리량을 보인다(Kafka는 초당 수십만~수백만 건을 처리)
- 메시지 재처리 문제 : 메시지를 소비하면 Queue에서 삭제된다. 따라서, 이미 처리한 메시지를 다시 읽을 수 없다. DLQ(Dead Letter Queue)를 활용할 수는 있지만, Kafka의 offset 기반 재처리만큼 유연하지는 않다.
- 메모리 의존성 : RabbitMQ는 메시지를 주로 메모리에 보관한다. 처리량이 급증해서 메시지가 대량으로 쌓이면 메모리 부족 문제(OOM)가 발생할 수 있다.
- 수평 확장 복잡성 : 클러스터링을 지원하지만, 설정과 운영이 복잡하다.
- 메시지 순서 보장의 제약 : 여러 Consumer가 하나의 Queue를 구독하면, 메시지 처리 순서를 완벽히 보장하기 어렵다. 늦게 메시지를 받은 Consumer가 먼저 처리를 완료할 수 있어 순서가 중요한 경우 Consumer를 하나만 띄우거나, 별도의 순서 제어 로직을 구현해야 함
Kafka
Kafka란 LinkedIn에서 개발한 분산 이벤트 스트리밍 플랫폼이다. 대용량 실시간 데이터 피드를 처리하기 위해 설계되었으며, 높은 처리량과 확장성을 제공한다.
💡이벤트 스트리밍 플랫폼?
단순히 메시지를 전달하는 것을 넘어, 데이터를 스트림으로 취급하여 저장,처리,분석할 수 있는 플랫폼이다.
메시지를 소비한 이후에도 데이터를 보관하여, 여러 Consumer가 동일한 데이터를 반복적으로 읽을 수 있다.
실시간 데이터 파이프라인과 스트리밍 애플리케이션을 구축할 수 있다.
Kafka의 특징
- Event-Oriented : 발생하는 이벤트가 중심이 되어 시스템이 구성된다. 이벤트를 실시간으로 수집하고 처리한다.
- Pub/Sub 모델
- Consumer가 어느 Topic에 대해서 메시지를 구독할 수 있다.
- Producer가 메시지를 Topic에 발행한다.
- Consumer는 구독한 Topic에 대해서 메시지를 실시간으로 스트리밍을 통하여 받을 수 있다.
- Pub/Sub 구조이기 때문에 하나의 메시지에 대해 다양한 서비스(알림, 로깅, 분석 등)가 동시에 처리 가능하다
- 분산 아키텍쳐 : Kafka는 여러 브로커 노드로 이루어진 분산 시스템으로 구성되어 있다.
- 확장성 : Kafka 클러스터는 여러 대의 서버로 확장 가능하며, 수천 대의 브로커와 수십만 개의 Partition으로 이루어진 대규모 클러스터를 구성할 수 있다.
Kafka의 구조

- Producer : 메시지를 발생하는 주체이다. Topic에 메시지를 발행하며, 어느 Partition으로 보낼지 결정할 수 있다. Key를 지정하면 동일한 Key를 가진 메시지는 항상 같은 Partition으로 전송된다.
- Topic : 메시지가 저장되는 논리적인 개념이다. RabbitMQ의 Exchange + Queue와 유사하지만, 메시지를 소비해도 삭제되지 않고 설정된 보관 기간 동안 유지된다. 하나의 Topic은 여러 Partition으로 나뉘어진다.
- Partition : Topic을 물리적으로 분할한 단위. 각 Partition은 순서가 보장되는 불변의 로그 파일이다. Partition을 여러 개 두면 병렬 처리가 가능해져 처리량이 증가한다. 메시지는 Partition 내에서 Offset이라는 고유 번호로 식별된다.
- Broker : Kafka 서버를 의미한다. 여러 대의 Broker가 클러스터를 구성하며, Topic의 Partition들을 분산 저장한다. 하나의 Partition은 여러 Broker에 복제(Replication)되어 장애 대응이 가능하다
- Consumer : Topic의 메시지를 소비하는 주체이다. Pull 방식으로 메시지를 가져오며, 처리할 수 있을 때 자신의 속도로 메시지를 읽는다. Consumer는 자신이 어디까지 읽었는지 Offset을 관리한다.
- Consumer Group : 여러 Consumer를 묶은 그룹이다. 같은 그룹 내의 Consumer들은 Topic의 Partition을 나눠서 읽는다. 예를 들어 Topic에 3개의 Partition이 있고 Consumer Group에 3개의 Consumer가 있다면, 각 Consumer는 1개씩의 Partition을 담당한다. 이를 통해 Message Queue처럼 부하 분산이 가능하다. 하지만, Consumer 수가 Partition 수보다 많다면 일부 Consumer는 놀게 되는 문제가 발생한다.
- Zookeeper / KRaft : Kafka 클러스터의 메타데이터를 관리한다. Broker들의 상태, Topic 정보, Partition의 리더 정보 등을 저장한다. 최신 버전(Kafka 3.x 이상)에서는 Zookeeper 없이 KRaft 모드로 동작할 수 있다.

위 그림에서, 하나의 Topic은 여러 개의 Partition으로 분할된다.
- Offset : 각각의 파티션의 레코드들에는 일련번호(Offset)이 지정되게 된다. Consumer는 Offset을 통해 어디까지 읽었는지 기록하고, 원하는 시점의 메시지를 다시 읽을 수 있다.
- 순서 보장 : 같은 Partition 내에서의 메시지의 삽입 순서는 보장된다.
Kafka 메시지 흐름
- Producer가 메시지를 생성하고 특정 Topic의 Partition에 발행한다.
- Broker는 메시지를 Partition의 로그 파일에 순차적으로 기록한다.
- 메시지는 설정된 보관 기간 동안 디스크에 저장된다.
- Consumer는 Consumer Group을 구성하여 Partition을 나눠서 읽는다.
- Consumer는 메시지를 처리한 후 Offset을 커밋하여 진행 상황을 기록한다.
- 장애가 발생하면 마지막 커밋된 Offset부터 다시 읽어 재처리할 수 있다.
Kafka의 장점 및 단점
장점
- 높은 처리량 : Partition을 통한 병렬 처리로 초당 수백만 건의 메시지를 처리할 수 있다. 순차적인 디스크 I/O를 활용하여 메모리만큼 빠른 속도를 낸다.
- 메시지 영속성과 재처리 : 메시지를 디스크에 저장하고 설정된 기간 동안 보관한다. 이미 처리한 메시지를 Offset을 되돌려 다시 읽을 수 있어, 데이터 분석이나 재처리가 필요한 경우 유용하다.
- 수평 확장성 : Partition 수를 늘리면 처리량이 선형적으로 증가한다. Broker를 추가하면 자동으로 Partition이 재분배되어 부하가 분산된다. 클러스터 운영이 RabbitMQ보다 간단하다.
- 순서 보장 : Partition 내에서는 메시지 순서가 엄격하게 보장된다. 동일한 Key를 가진 메시지는 항상 같은 Partition으로 전송되므로, 특정 Key에 대한 순서 처리가 가능하다.
- 장애 복구 : Partition을 여러 Broker에 복제하여 일부 Broker가 다운되어도 서비스가 중단되지 않는다. Leader-Follower 구조로 자동 Failover가 이루어진다.
단점
- 실시간성 부족 : Consumer가 Pull 방식으로 메시지를 가져오기 때문에, Polling 주기만큼 지연이 발생한다. RabbitMQ의 Push 방식처럼 즉각적인 전달은 어렵다. 실시간 알림이나 긴급 처리가 필요한 경우 부적합하다.
- 운영 복잡도 : Zookeeper 관리, Partition Rebalancing, Offset 관리 등 운영에 필요한 지식이 많다. 초기 학습 곡선이 높고, 모니터링 도구도 별도로 구축해야 한다.
- 리소스 사용량 : 메시지를 디스크에 저장하고 복제하기 때문에 디스크 용량을 많이 사용한다. 메모리도 Page Cache로 많이 사용하며, Broker당 최소 4~8GB 이상의 메모리가 권장된다.
- 작은 메시지에 비효율적 : Kafka는 배치 처리에 최적화되어 있다. 한 건씩 메시지를 보내고 받으면 오버헤드가 크다. 소량의 메시지를 빠르게 처리하는 용도로는 RabbitMQ가 더 적합하다.
- 복잡한 라우팅 불가 : RabbitMQ의 Exchange처럼 유연한 라우팅 기능이 없다. Topic과 Partition만으로 메시지를 분류해야 하므로, 복잡한 메시지 라우팅이 필요한 경우 애플리케이션 레벨에서 처리해야 한다.
왜 RabbitMQ는 초당 수만건, Kafka는 초당 수십만~수백만 건의 처리가 가능할까?
- 순차 I/O 활용 : RabbitMQ는 메시지를 소비하면 큐에서 삭제하고, 중간에 메시지를 삽입할 수 있어 랜덤 I/O가 발생합니다. 반면 Kafka는 Append-Only 방식으로 Partition 끝에 메시지를 계속 추가만 하는 순차 I/O를 사용합니다.
- 배치 처리 : RabbitMQ는 각 메시지를 개별적으로 처리하여 메시지마다 네트워크 왕복과 ACK 처리가 발생합니다. Kafka는 여러 메시지를 배치로 묶어서 한 번에 전송하고 기록합니다. → 오버헤드 감소
- Partition 기반 병렬화 : RabbitMQ에서 하나의 큐는 기본적으로 순차 처리되고, 병렬화를 위해서는 여러 큐를 만들어야 합니다. Kafka는 Topic을 여러 Partition으로 나누고 각 Partition이 독립적으로 병렬 처리됩니다.
'Back-End' 카테고리의 다른 글
| MSA 학습 - 로드밸런싱(FeignClient, Ribbon) (0) | 2025.09.23 |
|---|---|
| MSA 학습 - Spring Cloud와 Eureka (0) | 2025.09.22 |
| @Transactional(readOnly=true)는 왜 써야하는걸까? (0) | 2025.09.18 |
| [JPA] 외래키 주인으로 알아보는 엔티티 연관관계 (0) | 2025.09.17 |
| 테스트 코드 작성 전략 (1) | 2025.09.16 |
