Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

Pub/Sub 모델을 이용한 실시간 알림과 뉴스 구독 (WebSocket, STOMP 활용) 본문

Middleware/RabbitMQ (메시지 브로커)

Pub/Sub 모델을 이용한 실시간 알림과 뉴스 구독 (WebSocket, STOMP 활용)

studyHub 2026. 2. 22. 15:12

Publish / Subscribe 모델

Pub/Sub은 메시지 발행(Publish)과 구독(Subscribe)의 개념을 기반으로 하는 메시징 패턴(메시지 전달 모델)으로,

하나의 메시지가 복제되어 여러 “독립적인 소비자 그룹”에 각각 전달되는 모델이다.

  • Publisher는 특정 대상이 아니라 주제(이벤트)에 메시지를 발행
  • 여러 Subscriber가 동시에 메시지를 수신 (여러 독립 구독자에게 복제 전달)

하나의 Exchange에 여러 Queue가 바인딩되어 있으면 Pub/Sub

Fanout Exchange와의 차이

  • Pub/Sub = "신문을 여러 사람이 구독"
  • Fanout = "신문을 그냥 무조건 모든 우편함에 배달"

경쟁 소비자 모델과의 차이

  • 큐가 여러 개에 각각의 컨슈머 / 메시지 복제 있음, 여러 구독자가 동일 메시지 수신 → Pub/Sub
  • 큐가 하나에 컨슈머 여러개 / 메시지 복제 없음, 여러 소비자가 나눠 처리 → 경쟁 소비자

 

Pub/Sub vs 경쟁 소비자는 “큐 구조 차이”가 있다.

 

Pub/Sub 모델의 주요 특징

유연성과 확장성이 좋아 여러 subscriber를 쉽게 추가하여도 서로 독립적으로 동작이 가능하다.

  1. 다대다 메시징
    • 하나의 메시지가 여러 Subscriber에게 전달
    • 메시지 복사가 이뤄지므로 Subscriber는 동일한 메시지를 수신.
      동일한 메시지가 여러 큐에 처리되므로 중복 처리 로직이 필요할 수 있음.
  2. 구독자 독립성
    • Publisher는 메시지가 어떤 Subscriber에게 전달될지 알 필요가 없음
    • 메시지의 전달은 브로커가 처리
  3. 비동기 메시지
    • Publisher와 Subscriber는 서로 독립적으로 동작하며, 동시에 실행될 필요가 없음
  4. 확장성
    • 여러 Subscriber를 추가하거나 제거해도 시스템이 영향을 받지 않음
  5. 구독 제어
    • 구독자는 특정 조건(ex.라우팅 키, 토픽)을 기반으로 메시지를 필터링하여 수신할 수도 있음
    • Fanout Exchange는 모든 구독자에게 메시지를 브로드캐스트하는 반면(Routing Key는 필요하지 않음),
      Topic Exchange나 Direct Exchange는 메시지를 선택적으로 전달 가능
    • 구독자가 많을수록 복잡도가 증가

WebSocket과 STOMP

먼저 WebSocket과 STOMP에 대해 알아보자.

  • RabbitMQ/Kafka = 서버 간 메시징
  • WebSocket/STOMP = 서버 → 브라우저 실시간 전송
    • WebSocket = 서버가 먼저 말할 수 있게 만드는 기술
    • STOMP = 그 말을 어떤 방에 보낼지 정하는 규칙
브라우저
     ↓ WebSocket + STOMP
Spring 서버
     ↓
Kafka / Redis / RabbitMQ

WebSocket

서버와 클라이언트가 한 번 연결하면, 끊기 전까지 서로 실시간으로 데이터를 주고 받을 수 있는 통신 방식

(실시간 통신이란, 이벤트가 발생하는 즉시 상대방에게 전달되는 통신 방식)

WebSocket은 HTTP 위에서 시작하지만, 연결 후에는 지속적인 양방향 실시간 통신을 제공하는 프로토콜.

※ 소켓: 네트워크에서 두 프로그램이 데이터를 주고받기 위한 통신의 endpoint . IP + Port + Protocol = Socket

  • HTTP는 요청/응답 1회성
  • WebSocket은 지속 연결 + 양방향 통신
    • WebSocket은 TCP 커넥션을 끊지 않고 계속 유지한다.
      하지만 스레드를 계속 점유하지는 않는다. Spring은 소수의 워커 스레드가 여러 소켓 이벤트를 처리한다.
      TCP 소켓 자체가 세션이다. (Spring은 내부적으로  Map<sessionId, WebSocketSession> 형태로 관리)
    • 클라이언트가 요청하지 않아도 서버가 먼저 보낼 수 있음
  • 실시간 채팅 등에 사용
    • 채팅을 HTTP로 하면 1초마다 서버에 물어봄 (polling) → 부하/지연 발생
  • 매번 연결을 새로 만들지 않아도 되기 때문에 지연(latency)이 거의 없음
  • 데이터를 프레임 단위로 전송하며, 오버헤드가 낮음
  • ws://호스트:포트/경로 형태로 작성 (ws://example.com/chat)
  HTTP WebSocket
연결 방식 요청/응답 지속 연결
통신 방향 단방향 양방향
실시간성 낮음 높음
오버헤드 큼 (헤더 반복) 적음

 

STOMP (Simple Text Oriented Messaging Protocol)

WebSocket 위에서 사용하는 메시지 프로토콜로, 텍스트 기반의 단순한 메시지 규칙.

  • WebSocket = 통신 터널(네트워크 레벨)
  • STOMP = 그 터널 안에서 사용하는 메시지 형식/라우팅 규칙(프로토콜)
클라이언트
     ↓
WebSocket 연결
     ↓
그 위에서 STOMP 프로토콜 사용
    ↓
Spring Message Broker 처리

 

STOMP가 제공하는 기능

기능 설명
SUBSCRIBE 특정 topic 구독
SEND 메시지 전송
ACK 메시지 확인
destination 주소 개념

 

STOMP 메시지 형식

COMMAND
header1:value1
header2:value2
body
---
# 예시
SEND
destination:/topic/chat
{"message":"hello"}

 

STOMP를 쓰는 이유

WebSocket은 그냥 문자열 or byte[] 전송으로, 형식 규칙과 pub/sub 구독 등의 개념이 없다. 그래서 등장한 것이 STOMP 이다.

약속을 기반으로 API 등이 구현되어 있는 것이다.

  • STOMP를 안 쓰면: “WebSocket 통로”만 있고, 채팅방/구독/라우팅/세션관리/재전송 정책을 전부 직접 만들어야 한다.
  • STOMP를 쓰면: WebSocket 위에 목적지(destination) 기반 Pub/Sub 규칙이 올라가서,
    클라이언트는 SUBSCRIBE/SEND만 하면 되고, 서버(Spring)가 구독자 목록 관리 + 라우팅 + 전송을 대신해준다.
  • Spring WebSocket(STOMP) 스택이 STOMP 프레임을 해석해서 구독을 “등록 테이블”에 저장하고, 메시지 전송 시 그 테이블을 조회해 해당 세션으로 push한다.
    이를 쓰려면 WebSocket/STOMP 관련 의존성과 설정이 필요한데, Spring Boot 기준으로 spring-boot-starter-websocket만 추가하면 STOMP 메시징까지 같이 들어온다.
  • STOMP를 쓰면 Spring이 “destination(목적지) ↔ 구독 세션 목록”을 메모리(또는 외부 브로커)에서 관리하고,
    메시지를 보내면 해당 destination을 구독한 세션들에게 자동으로 fan-out 해준다.
  • STOMP는 destination이라는 개념을 제공하고, 실제 라우팅은 SimpleBroker 사용
    : Spring이 내부 메모리로 간단한 브로커 역할 수행(destination -> 세션 목록)
    Client
       ↓
    WebSocket
       ↓
    STOMP
       ↓
    Spring SimpleBroker (메모리 Map)
       ↓
    다른 Client

채팅방 room1에 A가 메시지를 보내면 roo1에 있는 모든 사람(B,C,,)에게 즉시 전달되는 프로그램을 만든다고 가정해보자.

 

1) 클라이언트가 이런 STOMP 프레임을 보낸다고 치면:

SUBSCRIBE
destination:/topic/room1
id:sub-1

 

2) Spring이 프레임을 파싱하고 “구독 등록”을 한다. Spring 내부에서는 대략 아래와 같은 식의 매핑을 유지한다.

destination("/topic/room1") -> [sessionA, sessionB, ...]
  • sessionId(WebSocket 연결 단위)별로 어떤 destination을 구독했는지 저장
  • destination별로 어떤 세션들이 붙어있는지 인덱싱

3) 서버가 destination으로 메시지 발행

서버에서 아래를 호출하면

messagingTemplate.convertAndSend("/topic/room1", payload);

 

Spring은 "/topic/room1"을 구독한 세션 목록을 찾고, 그 세션들에게 WebSocket으로 메시지를 push한다.

즉, “누가 구독했는지”를 Spring이 이미 알고 있기 때문에 서버 코드는 “어디로 보낼지(destination)”만 말하면 끝이다.

 

이번에는 javascript와 java 코드로 살펴보자.

 

1. 서버에서 규칙(prefix)을 먼저 정한다.

Spring 설정에서 보통 아래와 같이 나눈다:

  • 클라이언트가 서버로 보내는 목적지(prefix): /app
  • 서버가 클라이언트에게 보내는 목적지(prefix): /topic, /queue
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

  @Override
  public void configureMessageBroker(MessageBrokerRegistry registry) {
    // 서버 → 클라(구독 대상)
    // SimpleBroker: Spring이 내부 메모리로 간단한 브로커 역할을 하겠다 (RabbitMQ 아님)
    registry.enableSimpleBroker("/topic", "/queue");
    // 클라 → 서버(@MessageMapping으로 들어옴)
    registry.setApplicationDestinationPrefixes("/app");
  }
}

 

  • 클라가 SEND /app/... 하면 → 서버의 @MessageMapping("...")로 라우팅
  • 서버가 convertAndSend("/topic/...") 하면 → 그 destination 구독자에게 push

 

2. "어디로 보낼지"는 결국 2가지 중 하나로 결정한다. 

A) 브로드캐스트(채팅방)로 보낼 때

클라이언트(구독)

stompClient.subscribe("/topic/room.1", onMessage);

 

서버(전송)

messagingTemplate.convertAndSend("/topic/room.1", payload);

둘이 destination 문자열이 같으니까 “room.1을 구독한 사람 모두”가 받는다.

 

B) 서버로 보내서(핸들러 타서) 처리한 뒤, 다시 뿌릴 때

클라이언트(서버로 SEND)

stompClient.send("/app/chat.send", {}, JSON.stringify({
  roomId: 1,
  text: "안녕"
}));

 

서버(받는 곳)

@MessageMapping("/chat.send") // 앞의 /app 은 설정에서 떼고 매핑됨
public void handle(ChatMessage msg) {
  messagingTemplate.convertAndSend("/topic/room." + msg.roomId(), msg);
}

여기서 “어디로 보낼지”는 서버 코드가 결정: "/topic/room." + roomId


WebSocket과 Pub/Sub을 통한 실시간 웹 알림 구현(Notification)

웹 페이지를 하나 만들고, WebSocket을 이용하여 API를 연동한 뒤,
서버에서 publish 한 메시지를 web 알림 영역에 실시간으로 표시하는 프로그램을 만들어보자.

구현

RabbitMQConfig : 3개의 Bean(Queue, Exchange, Binding)을 설정

@Configuration
public class RabbitMQConfig {
	// 큐 네임 설정
	public static final String QUEUE_NAME = "notificationQueue";
	public static final String FANOUT_EXCHANGE = "notificationExchange";

	//Spring이 시작될 때 RabbitMQ 서버에 “이 큐를 생성하라”라고 선언하기 위해
	@Bean
	public Queue queue() {
		//QUEUE_NAME은 메시지가 쌓이고 처리될 큐의 이름을 정의
		return new Queue(QUEUE_NAME, false); //영속화 여부
	}

	@Bean
	public FanoutExchange fanoutExchange() {
		//메시지를 수신하면 모든 큐로 브로드캐스트
		return new FanoutExchange(FANOUT_EXCHANGE);
	}

	@Bean
	public Binding bindingNotification(Queue notificationQueue, FanoutExchange fanoutExchange) {
		//큐와 익스체인지를 연결
		return BindingBuilder.bind(notificationQueue).to(fanoutExchange);
	}
}

 

Controller

두 가지 경로로 알림을 클라이언트에 브로드캐스트하므로 Controller도 두개이다.

 

A) StompController
STOMP 클라이언트가 /app/send로 보낸 메시지를 @MessageMapping이 받아 SimpMessagingTemplate.convertAndSend("/topic/notifications", ...)로 브로드캐스트

@Controller
@RequiredArgsConstructor
public class StompController {

	private final SimpMessagingTemplate simpMessagingTemplate;

	@MessageMapping("/send")
	public void sendMessage(NotificationMessage notificationMessage) {
		// 수신된 메시지를 브로드캐스팅
		String message = notificationMessage.message();
		System.out.println("[#] message = " + message);

		// 클라이언트에 메시지 브로드캐스트
		simpMessagingTemplate.convertAndSend("/topic/notifications", message);
	}
}

 

B) NotificationController
HTTP(REST)로 들어온 알림을 NotificationPublisher가 RabbitMQ Exchange에 publish
→ NotificationSubscriber가 큐에서 수신
→ SimpMessagingTemplate.convertAndSend("/topic/notifications", ...)로 브로드캐스트

    * SimpMessagingTemplate은 Spring에서 WebSocket(STOMP) 클라이언트에게 메시지를 보내는 서버 측 유틸리티 클래스

curl -X POST 'http://localhost:8080/notifications' -H 'Content-Type: application/json' -d 'Hello! Subscriber!!'
@RestController
@RequestMapping("/notifications")
@RequiredArgsConstructor
public class NotificationController {

	private final NotificationPublisher publisher;

	@PostMapping
	public String sendNotification(@RequestBody String message) {
		publisher.publish(message);
		return "[#] Notification sent: " + message + "\n";
	}
}

 

Publisher: RabbitTemplate을 이용해 convertAndSend

@Component
@RequiredArgsConstructor
public class NotificationPublisher {

	private final RabbitTemplate rabbitTemplate;

	public void publish(String message) {
		rabbitTemplate.convertAndSend(RabbitMQConfig.FANOUT_EXCHANGE, "", message);
		System.out.println("[#] Published Notification: " + message);
	}
}

 

Subscriber

  • @RabbitListener로 큐 네임 지정
    • Spring이 메시지 수신을 자동화하고, 비동기적으로 처리. 코드가 간결
    • 내부적으로 Spring의 MessageListenerContainer를 사용하여 Queue를 지속적으로 모니터링
    • 메시지 수신, 메서드 호출과 변환, Ack 전송 등의 역할
  • SimpMessageingTemplate을 통해 특정 경로에 메시지 전달
    • Spring에서 WebSocket(STOMP) 메시지를 클라이언트에게 보내는 템플릿 클래스
      쉽게 말해 웹소켓용 RabbitTemplate/KafkaTemplate 같은 것
    • WebSocket 메시지 브로커와 통신하며, 클라이언트가 구독하는 특정 경로로 메시지를 전송
    • HTTP 응답이 아니라, 실시간 push 메시지 전송용
      Server (Spring)
         ↓
      SimpMessagingTemplate
         ↓
      WebSocket Broker
         ↓
      Client (브라우저)

 

@Component
@RequiredArgsConstructor
public class NotificationSubscriber {

	public static final String CLIENT_URL = "/topic/notifications";

	// WebSocket으로 메시지를 전달하기 위한 Spring의 템플릿 클래스
	private final SimpMessagingTemplate simpMessagingTemplate;

	// RabbitMQ Queue에서 메시지 수신
	// RabbitListener에 의해 QUEUE_NAME을 바라보다가 exchange에 메시지가 도착하면 Queue로 발행되고 이 Queue가 메시지를 수신
	// String message = (String) rabbitTemplate.receiveAndConvert(RabbitMQConfig.QUEUE_NAME); 과 같은 번거로운 코딩 생략
	@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
	public void subscribe(String message) {
		System.out.println("[#] Received Notification: " + message);
		// WebSocket을 통해 클라이언트로 메시지를 전달
		simpMessagingTemplate.convertAndSend(CLIENT_URL, message); // 클라이언트에 브로드캐스트
	}
}

 

WebSocketConfig

  • WebSocketMessageBroker 활성화
  • WebSocketMessageBrokerConfigurer를 구현
    Spring에서 WebSocket 메시지 브로커를 구성하기 위한 인터페이스.
    웹소켓 연결, 메시지 브로커 설정 및 라우팅 등의 웹 소켓 관련 확장 기능 제공
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		// SimpleBroker: Spring이 내부 메모리로 간단한 브로커 역할을 하겠다 (RabbitMQ 아님)
		// 서버 → 클라(구독 대상). 서버가 convertAndSend("/topic/...") 하면 → 그 destination 구독자에게 push(세션 뒤져서)
		registry.enableSimpleBroker("/topic"); // 클라이언트가 구독할 수 있는 경로 설정. SimpleBroker가 /topic/*에 대한 구독을 관리
		// 클라 → 서버(@MessageMapping으로 들어옴). 클라가 SEND /app/... 하면 → 서버의 @MessageMapping("...")로 라우팅
		registry.setApplicationDestinationPrefixes("/app"); // 클라이언트가 메시지를 보낼 때 사용할 접두사 설정
	}

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/ws") // 클라이언트가 WebSocket 연결을 시도할 때 사용할 엔드포인트 설정
			.setAllowedOriginPatterns("*") // CORS 설정
			.withSockJS(); // SockJS 지원
	}
}

 

HTML 작성

  • 필요 라이브러리
    • sock.js : WebSocket 연결을 지원하지 않는 브라우저에서도 동작하도록 fallback 기능을 제공하는 라이브러리로 다음과 같은 전송 타입을 지원: WebSocket, HTTP Streaming, HTTP Long Polling
    • stomp.js: STOMP 프로토콜을 지원하는 JavaScript 클라이언트 라이브러리. 
      텍스트 기반 프로토콜로, 메시지의 유형, 내용을 정의하여 클라이언트와 서버 간 메시지를 주고 받음.
      RabbitMQ와 같은 브로커와 통신하는 데 사용.
<!-- html 생략 -->
<script>
    const socket = new SockJS('/ws'); // 서버의 WebSocket Endpoint 연결
    const stompClient = Stomp.over(socket); // SockJS 객체를 STOMP 클라이언트로 wrapping

    stompClient.connect({}, function () {
        console.log('Connected to WebSocket');
        // STOMP 연결 후 stompClient.subscribe('/topic/notifications', ...) 호출 → 서버의 SimpleBroker에 구독 등록
        // WebSocketConfig.configureMessageBroker(...)에서 registry.enableSimpleBroker("/topic") -> SimpleBroker가 /topic/*에 대한 구독을 관리
        // 서버가 /topic/notifications로 메시지 전송하면(서버 발행)
        // SimpleBroker가 해당 destination의 구독 목록을 찾아 세션별로 메시지를 전송 → 클라이언트의 subscribe 콜백으로 도달
        stompClient.subscribe('/topic/notifications', function (message) {
            const notificationsDiv = document.getElementById('notifications');
            const newNotification = document.createElement('div');
            newNotification.textContent = message.body;
            notificationsDiv.appendChild(newNotification);
        });
        // 서버로 전송도 가능
        const form = document.getElementById('notificationForm')
        form.addEventListener('submit', function (event) {
            event.preventDefault();
            const messageInput = document.getElementById('notificationMessage');
            const message = messageInput.value;

            stompClient.send('/app/send', {}, JSON.stringify({ message: message }));
            messageInput.value = '';
        })
    });
</script>

여러 큐를 소비하는 Fanout Exchange 예제 (관심사 기반의 뉴스 레터 발행/구독 모델)

뉴스 레터 구독 프로그램을 만들어보자.

각 사용자는 관심있는 개발 뉴스 레더를 체크하고 구독한다. (Java/Spring/Vue)

각각의 관심사별 Exchange 연결에 따른 메시지 발행과 구독을 구현하면 된다.

시퀀스 A: STOMP → 서버 → RabbitMQ → WebSocket 브로드캐스트
시퀀스 B: HTTP REST → RabbitMQ → WebSocket 브로드캐스트

 

구현

RabbitMQConfig

  • 3개의 Queue Bean, 1개의 Exchange Bean, 3개의 큐 Binding에 대한 bind().to() 설정
    (사실 해당 예제는 Topic Exchange가 적합하나, 아직 배우기 전이라 Fanout Exchange로 구현했다.)
  • 매개변수 이름은 실제로 주입되는 빈 이름과 일치하지 않아도 동작하지만, 타입이 같은 여러개의 빈이 존재하면 @Qualifier를 사용해 이름으로 구분 지어야 함
@Configuration
public class RabbitMQConfig {
	// 큐 네임 설정
	public static final String JAVA_QUEUE = "javaQueue";
	public static final String SPRING_QUEUE = "springQueue";
	public static final String VUE_QUEUE = "vueQueue";

	public static final String FANOUT_EXCHANGE_FOR_NEWS = "newsExchange";

	//Spring이 시작될 때 RabbitMQ 서버에 “이 큐를 생성하라”라고 선언하기 위해
	@Bean
	public Queue javaQueue() {
		return new Queue(JAVA_QUEUE, false);
	}

	@Bean
	public Queue springQueue() {
		return new Queue(SPRING_QUEUE, false);
	}

	@Bean
	public Queue vueQueue() {
		return new Queue(VUE_QUEUE, false);
	}

	@Bean
	public FanoutExchange fanoutExchange() {
		// 메시지를 수신하면 연결된 모든 큐로 브로드캐스트
		return new FanoutExchange(FANOUT_EXCHANGE_FOR_NEWS);
	}

	@Bean
	public Binding javaBinding(Queue javaQueue, FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(javaQueue).to(fanoutExchange);
	}

	@Bean
	public Binding springBinding(Queue springQueue, FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(springQueue).to(fanoutExchange);
	}

	@Bean
	public Binding vueBinding(Queue vueQueue, FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(vueQueue).to(fanoutExchange);
	}
}

 

컨트롤러

웹용(STOMP 기반) 컨트롤러와 API용 컨트롤러 모두 publisher를 호출해 메시지 발행

 

메시지발행(publisher)

fanout이므로 라우팅 키는 무시되고 모든 큐에 삽입됨

@Component
@RequiredArgsConstructor
public class NewsPublisher {

    private final RabbitTemplate rabbitTemplate;

    private String publishMessage(String news, String messageSuffix) {
       String message = news + messageSuffix;
       rabbitTemplate.convertAndSend(RabbitMQConfig.FANOUT_EXCHANGE_FOR_NEWS, news, message);
       System.out.println("News Published: " + message);
       return message;
    }

    public String publish(String news) {
       return publishMessage(news, " 관련 새 소식이 나왔습니다.");
    }

    public String publishAPI(String news) {
       return publishMessage(news, " 관련 새 소식이 나왔습니다. (API)");
    }
}

 

컨슈머(subscriber)

큐에서 메시지를 수신하면 웹 소켓으로 메시지를 전달한다. 이때, destination을 보고 구독한 클라이언트만 메시지를 수신한다.

즉, Fanout으로 모든 큐에 메시지를 브로드캐스트하지만 WebSocket을 이용해 특정 뉴스 타입만 선택적으로 구독하는 것이다.

@Component
@RequiredArgsConstructor
public class NewsSubscriber {

	// WebSocket으로 메시지를 전달하기 위한 Spring의 템플릿 클래스
	private final SimpMessagingTemplate simpMessagingTemplate;

	// RabbitListener에 의해 QUEUE_NAME을 바라보다가 exchange에 메시지가 도착하면 Queue로 발행되고 이 Queue가 메시지를 수신
	@RabbitListener(queues = RabbitMQConfig.JAVA_QUEUE)
	public void javaNews(String message) {
		// 해당 destination으로 구독한 세션들에게 메시지 push
		simpMessagingTemplate.convertAndSend("/topic/java", message);
	}

	@RabbitListener(queues = RabbitMQConfig.SPRING_QUEUE)
	public void springNews(String message) {
		simpMessagingTemplate.convertAndSend("/topic/spring", message);
	}

	@RabbitListener(queues = RabbitMQConfig.VUE_QUEUE)
	public void vueNews(String message) {
		simpMessagingTemplate.convertAndSend("/topic/vue", message);
	}
}

참고 자료 & 이미지 출처
RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기