아파트타임에서는 기존에 Kafka를 통해 실시간 알림 이벤트를 처리하고 있었습니다.

하지만 실시간 채팅을 도입하면서 다음과 같은 이유로 이벤트 브로커를 Kafka에서 RabbitMQ로 전환했습니다.
아파트타임의 실시간 알림은 모든 서버 인스턴스가 동일한 이벤트를 동시에 수신할 수 있어야 했습니다.
즉, 알림이 모든 서버에 동일하게 브로드캐스트되는 것이 필수적인 요구사항이었습니다.
하지만 Kafka의 Consumer Group 모델은 기본적으로 파티션 기반의 메시지 분산 처리(로드 밸런싱)를 위해 설계된 구조입니다.
Kafka의 공식 문서에서도 아래와 같이 명시하고 있습니다.
If all the consumer instances have the same consumer group, then this works just like a traditional queue balancing load over the consumers.
If all the consumer instances have different consumer groups, then this works like publish-subscribe and all messages are broadcast to all consumers.
More commonly, however, we have found that topics have a small number of consumer groups, one for each "logical subscriber". Each group is composed of many consumer instances for scalability and fault tolerance. This is nothing more than publish-subscribe semantics where the subscriber is cluster of consumers instead of a single process.
모든 Consumer 인스턴스가 동일한 Consumer Group을 사용하면, 전통적인 queue처럼 동작하여 Consumer 간에 부하를 분산 처리합니다.
모든 Consumer 인스턴스가 서로 다른 Consumer Group을 사용하면, Pub-Sub 방식처럼 작동하여 모든 메시지가 모든 Consumer에게 브로드캐스트됩니다.
그러나 일반적으로는 각 "논리적 구독자"마다 하나의 Consumer Group을 두는 방식으로, 하나의 topic에는 적은 수의 Consumer Group만 존재하는 경우가 많습니다.
각 Consumer Group은 확장성과 장애 허용을 위해 여러 개의 Consumer 인스턴스로 구성됩니다.
이는 하나의 프로세스가 아닌 Consumer 클러스터가 구독자 역할을 하는 Pub-Sub 의미론과 본질적으로 다르지 않습니다.출처: Kafka 공식 문서
Kafka에서는 각 서비스나 기능 같은 Logical Subscriber 단위로 하나의 Consumer Group을 생성하고, 해당 Group 내에서 여러 Consumer 인스턴스를 추가해 파티션을 나눠서 처리하도록 설계하는 것이 일반적입니다.
이 방식은 특정 메시지를 모든 서버가 수신하는 시나리오에는 적합하지 않았지만, Kafka를 도입할 당시에는 이러한 설계 철학과 사용 패턴을 충분히 인지하지 못했습니다.
그래서 모든 서버 인스턴스가 동일한 이벤트를 수신할 수 있도록 서버별로 서로 다른 Consumer Group ID를 생성해 Kafka가 Pub-Sub처럼 동작하도록 구성했고, 결과적으로 실시간 알림은 정상적으로 동작했지만 Kafka의 설계 철학과는 어긋나는 사용 방식이기에 이를 개선하고 싶었습니다.
도입한 아키텍처 내부 설계 철학과 실제 구현 방식의 간극을 해소하고자, RabbitMQ의 Fanout Exchange를 도입하게 되었습니다.
RabbitMQ의 Fanout Exchange는 메시지를 발행하면 연결된 모든 queue로 동일한 메시지를 복제하여 전송하는 브로드캐스트 모델을 제공합니다.
A fanout exchange routes messages to all of the queues that are bound to it and the routing key is ignored.
If N queues are bound to a fanout exchange, when a new message is published to that exchange a copy of the message is delivered to all N queues.
Fanout exchanges are ideal for the broadcast routing of messages.
Fanout Exchange는 해당 Exchange에 바인딩된 모든 queue로 메시지를 라우팅하며, 라우팅 키는 무시됩니다.
N개의 queue가 Fanout Exchange에 바인딩되어 있을 때, 새로운 메시지가 이 Exchange로 발행되면 그 메시지의 복사본이 N개의 모든 queue로 전달됩니다.
Fanout Exchange는 메시지를 브로드캐스트 방식으로 라우팅하는 데 이상적입니다.
출처: RabbitMQ 공식 문서

아파트타임에 RabbitMQ의 Fanout Exchange를 도입함으로써 실시간 알림 전략에 맞게 모든 서버 인스턴스가 별도의 라우팅 설정이나 ID 관리 없이 동일한 알림 메시지를 수신할 수 있게 됐습니다.
RabbitMQ로 새롭게 구축한 실시간 알림 전달 흐름은 아래와 같습니다.

(시퀀스 다이어그램 표현 상의 한계로 notification-queue-admin-1만 명시적으로 나타나있지만, 실제로는 서버 인스턴스의 수에 따라 notification-queue-admin-2, notification-queue-admin-3 등 별도의 queue가 생성되어 동일한 방식으로 notification-exchange로부터 메시지를 수신합니다.)
아파트타임 관리자 시스템의 실시간 알림 기능은 현재로서는 신규 회원가입 알림만 지원되지만, 향후 새로운 공지사항 알림, 새로운 댓글 알림 등을 추가할 계획입니다.
그리고 모든 알림들을 notification-exchange와 각 서버 인스턴스마다 바인딩된 queue를 통해 동일하게 전파하는 방식으로 처리하고 있지만, 알림의 종류가 다양해지고 개별 사용자별로 알림 유형이나 중요도가 달라질 경우에는 지금의 단일 Fanout 구조만으로는 한계가 생길 수도 있다고 생각합니다.
따라서 아키텍처를 유연하게 개편할 수 있도록 지속적으로 고민하고 개선해 나갈 계획입니다.