[ApartTime] RabbitMQ + Fanout 환경에서 발생한 실시간 알림 중복 수신 이슈

고뭉남·2025년 7월 28일

ApartTime

목록 보기
6/6
post-thumbnail

아파트타임 관리자 시스템에 실시간 알림 기능을 지원하기 위해 RabbitMQ를 도입했습니다.

분산 서버 환경을 상정하고 개발을 진행하기 때문에, 애플리케이션 서버에서 발생한 이벤트가 모든 어드민 서버 인스턴스들에 동시에 전파될 수 있어야 했고, 이를 위해 Fanout Exchange 구조를 선택했습니다.

관련 글: Kafka에서 RabbitMQ로 전환한 이유


Fanout Exchange 선택 이유

기존 설계에서 RabbitMQ는 단순히 애플리케이션 서버에서 발생한 이벤트를 어드민 서버로 전달하는 역할만 담당했습니다.

예를 들어, 애플리케이션 서버에 신규 회원가입 이벤트가 발생한다면 애플리케이션 서버는 해당 이벤트를 메시지로 발행하여 RabbitMQ의 Fanout Exchange로 전송합니다.

RabbitMQ의 Fanout Exchange는 연결된 모든 Queue로 동일한 메시지를 복제해 전달하고, 각 어드민 서버 인스턴스는 자기 자신과 바인딩된 Queue를 통해 이 메시지를 소비합니다.

fanout_exchange_flow

각 서버 인스턴스가 자신의 Queue로부터 동일한 메시지를 소비한 뒤, 내부적으로는 Spring의 SimpMessagingTemplate.convertAndSend()를 통해 자신과 WebSocket 세션으로 연결된 클라이언트들에 실시간 알림을 브로드캐스트합니다.

simple_broker_flow

이러한 흐름이 가능했던 이유는 Spring이 자체적으로 제공하는 Simple Broker를 사용했기 때문입니다.

Simple Broker는 Spring 서버 자체가 메시지 Broker 역할까지 수행하게 하는데, 서버 내부의 메모리 기반 Broker가 바인딩된 Queue에서 메시지를 소비하고, 해당 서버와 WebSocket으로 연결된 클라이언트들에만 브로드캐스트를 수행합니다.


Simple Broker의 한계

분산 서버 환경에서 채팅 기능을 추가하기 위한 개발을 진행하면서, 기존의 Simple Broker 기반 구조로는 한계가 발생할 수 밖에 없었습니다.

각 서버가 자체적으로 Broker 역할을 수행하는 Simple Broker 구조에서는 클라이언트 간 메시지 전달이 서버 인스턴스 내에서만 일어났기 때문에, 같은 서버에 연결된 클라이언트들 간의 채팅에는 문제가 없지만 다른 서버에 연결된 클라이언트들 간의 채팅 메시지는 전송이 불가능했습니다.

simple_broker_impossible_flow

물론 AMQP를 활용하면 Simple Broker 환경에서도 분산 서버 간 메시지 전달은 구현할 수 있지만, 이 경우 각 서버 인스턴스가 AMQP 메시지를 소비한 뒤 다시 내부 Simple Broker로 메시지를 전달하는 책임을 지게 됩니다.

그리고 저는 이 구조가 불필요하게 복잡도를 높인다고 판단했습니다.

그래서 메시지 라우팅은 RabbitMQ가 담당할 수 있도록 RabbitMQ를 STOMP Relay Broker로 사용하는 구조로 전환했습니다.


STOMP Relay Broker 도입 후 채팅 흐름

이 구조에서는 Spring은 더 이상 자체 Broker 역할을 수행하지 않고, RabbitMQ가 중앙 메시지 허브 역할을 수행하며 서버 간 메시지를 전달합니다.

rabbitmq_stomp_broker_relay_flow

클라이언트가 채팅 메시지를 전송하면 해당 메시지는 다음의 단계를 거쳐 수신자에게 전달됩니다.


  1. Client 1Server 1

    Client 1/pub/chat/message 경로로 STOMP SEND 요청을 보냅니다.

  2. Server 1RabbitMQ

    Server 1/topic/chat.room.{roomId}로 메시지를 전송합니다.

  3. RabbitMQServer 2

    RabbitMQ/topic/chat.room.{roomId}를 subscribe 중인 Server 2의 Queue로 메시지를 복제해 전달합니다.

  4. Server 2Client 2

    Server 2는 Queue에서 메시지를 받아, 해당 채팅방을 subscribe 중인 Client 2에 WebSocket 세션을 통해 메시지를 전송합니다.


실시간 알림 중복 수신 이슈 발생

상기했듯이 실시간 알림에 사용된 Fanout Exchange는 하나의 메시지를 수신하면, 바인딩된 모든 Queue로 해당 메시지를 복제하여 전파합니다.

아파트타임 관리자 시스템에서는 이 구조를 이용해 애플리케이션 서버에서 발생한 이벤트를 모든 어드민 서버 인스턴스로 동시에 전달하고 있었습니다.

이 구조는 Simple Broker를 사용하던 시점에는 특별한 문제가 없었습니다.

각 어드민 서버 인스턴스는 자신이 소비한 메시지를 SimpMessagingTemplate.convertAndSend()를 통해 자신과 WebSocket으로 연결된 세션들에게만 전달했기 때문에, 서버 간 Broker가 분리된 상태에서는 알림이 중복 전송될 일이 없었습니다.

그런데 STOMP Broker로 RabbitMQ를 도입한 이후, 구조를 다시 살펴보다가 문득 이런 의문이 들었습니다.

Fanout 구조로 각 서버가 동일 메시지를 소비한 뒤, 그걸 다시 convertAndSend()를 하면 모든 서버가 동일한 메시지를 중복으로 브로드캐스트하는거 아닌가?

즉, RabbitMQ를 STOMP Broker로 등록한 상태에서 이전처럼 SimpMessagingTemplate.convertAndSend()를 각 서버들이 호출하게 되면, 이 메시지는 기존처럼 해당 서버의 세션들에만 전송되는 것이 아니라 STOMP Broker인 RabbitMQ를 통해 해당 destination을 구독 중인 모든 클라이언트들에 브로드캐스트되는 것입니다.

이로 인해 발생할 수 있는 문제는, 실행 중인 어드민 서버 인스턴스의 수만큼 이 SimpMessagingTemplate.convertAndSend()가 반복 호출되어 클라이언트가 동일한 메시지를 실행 중인 서버의 수만큼 중복해서 받게 된다는 점입니다.

rabbitmq_duplicated_call_flow

실제로 해당 문제가 발생하는지 확인하기 위해 로컬에 동일한 RabbitMQ를 바라보는 어드민 서버 인스턴스를 2개 구동시키고 클라이언트가 동일한 채널을 구독한 상태에서 테스트를 진행해봤습니다.

duplicated_notifications

테스트 결과, 예상대로 동일한 메시지가 서버 인스턴스 수만큼 클라이언트에 중복 전송되는 현상을 확인할 수 있었습니다.


중복 알림 이슈 해결을 위한 구조 개선

이 문제를 해결하기 위해 기존 Fanout Exchange 구조를 Direct Exchange 기반으로 리팩토링 했습니다.

핵심은, 애플리케이션 서버에서 발생한 하나의 메시지를 여러 어드민 서버 인스턴스들 중 단 하나만 소비하도록 만드는 것이었습니다.

이를 위해 모든 어드민 서버들이 각자의 Queue가 아닌, 동일한 이름의 단일 Queue에 함께 바인딩되도록 변경했습니다.

RabbitMQ는 이처럼 하나의 Queue에 여러 서버들이 연결된 Work Queue 패턴에서 오직 하나의 서버에만 메시지가 전달되는 것을 보장합니다.

또한, Work Queue 패턴에서는 바인딩 된 서버들이 Round Robin으로 메시지를 소비하도록 합니다.

direct_exchange_rabbitmq_flow

리팩토링 한 해당 구조가 정상적으로 동작하는지 확인하기 위해 이전과 동일한 환경에서 테스트를 진행했습니다.

normal_notifications

Direct Exchange를 도입한 새로운 구조에서는 동일한 메시지에 대해 클라이언트가 오직 한 번의 알림만을 수신하는 것을 확인할 수 있었습니다.


마무리하며

이번 구조 개선을 통해 RabbitMQ에서 단순히 '메시지 전달'에 그치지 않고 '어떤 방식으로 소비되고 어떤 경로로 전송되는지를 설계'하는 것이 중요하다는 것을 느낄 수 있었습니다.

현재는 신규 회원가입 알림만 구현되어 있지만, 앞으로 추가될 다양한 실시간 알림들이 이번에 개선한 구조에서도 문제 없이 동작할지 걱정이 되면서도 한편으로는 이런 구조 개선 경험의 즐거움 때문인지 기대도 됩니다.

profile
개발자 고뭉남입니다.

0개의 댓글