이전 게시글에서는 웹소켓에서 STOMP 프로토콜을 추가해 채팅 서버를 구현해보았습니다. 이번에는 STOMP에 외부 메시지 브로커인 RabbitMQ를 추가해보았습니다.
@Configuration
@EnableWebSocketMessageBroker
public class StompWepSocketConfig implements WebSocketMessageBrokerConfigurer {
//웹소켓 핸드셰이크 커넥션을 생성할 경로
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp/chat").setAllowedOrigins("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/pub");
registry.enableSimpleBroker("/sub");
}
STOMP 프로토콜로만 채팅서버를 구현했을 때는 다음과 같습니다.
- Spring 환경에서 STOMP 프로토콜을 사용한다면 메시지 브로커로 In Memory Broker을 사용하게 됩니다.
registry.enableSimpleBroker("/sub");
이 부분이 내장된 인메모리 메시지 브로커를 활성화하는 것입니다.
/sub
로 시작하는 대상 주제를 가진 클라이언트에게 메시지를 브로드캐스트합니다.- 즉 ,
/sub
로 시작하는 대상 주제를 구독하면 해당 주제에 대한 메시지를 수신할 수 있습니다.
결론적으로는 스프링 내부에 있는 인 메모리 브로커로는 사용자의 수가 많아 졌을 때와, 서버가 여러 대 있을 경우 한계가 있어 RabbitMQ같은 외부 메시지 브로커를 적용시킵니다.
brew services start rabbitmq
(백그라운드) 하면 실행, brew services stop rabbitmq
는 종료./rabbitmq-server
명령어를 치면 rabbitmq 서버가 실행하게 됩니다.brew info rabbitmq
를 입력하면 설치 경로를 확인할 수 있습니다.@Configuration
@EnableWebSocketMessageBroker
public class StompWepSocketConfig implements WebSocketMessageBrokerConfigurer {
//웹소켓 핸드셰이크 커넥션을 생성할 경로
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp/chat").setAllowedOrigins("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher(".")); // URL을 / -> .으로
registry.setApplicationDestinationPrefixes("/pub"); // @MessageMapping 메서드로 라우팅된다. Client에서 SEND 요청을 처리
registry.enableStompBrokerRelay("/queue", "/topic", "/exchange", "/amq/queue");
}
}
@Configuration
@EnableRabbit
public class RabbitConfig {
private static final String CHAT_QUEUE_NAME = "chat.queue";
private static final String CHAT_EXCHANGE_NAME = "chat.exchange";
private static final String ROUTING_KEY = "*.room.*";
//Queue 등록
@Bean
public Queue queue() {
return new Queue(CHAT_QUEUE_NAME, true);
}
//Exchange 등록
@Bean
public TopicExchange exchange() {
return new TopicExchange(CHAT_EXCHANGE_NAME);
}
// Exchange와 Queue바인딩
@Bean
public Binding binding(Queue queue, TopicExchange exchange){
return BindingBuilder
.bind(queue)
.to(exchange)
.with(ROUTING_KEY);
}
// RabbitMQ와의 메시지 통신을 담당하는 클래스
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
rabbitTemplate.setMessageConverter(jsonMessageConverter());
return rabbitTemplate;
}
// RabbitMQ와의 연결을 관리하는 클래스
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("guest");
factory.setPassword("guest");
return factory;
}
// 메시지를 JSON형식으로 직렬화하고 역직렬화하는데 사용되는 변환기
// RabbitMQ 메시지를 JSON형식으로 보내고 받을 수 있음
@Bean
public Jackson2JsonMessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
factory.setHost("localhost");
은 현재 실행 중인 로컬 시스템의 RabbitMQ 서버에 연결하려는 것을 의미하고 factory.setVirtualHost("/");
는 가상 호스트(Virtual Host)를 설정합니다,. RabbitMQ는 가상 호스트를 사용하여 여러 개의 독립적인 브로커 환경을 구성할 수 있습니다. 각 가상 호스트는 독립적인 메시지 브로커로 간주되며, 큐, 익스체인지, 바인딩, 사용자 권한 등이 해당 가상 호스트 내에서 관리됩니다.다음 사진에서 볼 수 있듯이 apic(STOMP 툴)을 통해 STOMP 연결을 하면 각 연결마다 큐(stomp-subscription-…)가 생성된 것을 확인할 수 있고 이때 RabbitConfig에 등록한 큐(chat.queue)에 메세지들을 보존 할 수 있습니다.
@Controller
@RequiredArgsConstructor
public class MessageController {
private final MessageService messageService;
private final RabbitTemplate rabbitTemplate;
// 채팅방 입장
@MessageMapping("chat.enter.{roomId}")
public MessageInfo enterUser(@DestinationVariable("roomId") Long roomId, @Payload MessageCreateRequest message) {
message.setMessage(message.getSender() + "님이 채팅방에 입장하였습니다.");
rabbitTemplate.convertAndSend("chat.exchange", "enter.room." + roomId, message);
return messageService.saveMessage(message);
}
// 채팅방 대화
@MessageMapping("chat.talk.{roomId}")
public MessageInfo talkUser(@DestinationVariable("roomId") Long roomId, @Payload MessageCreateRequest message) {
rabbitTemplate.convertAndSend("chat.exchange", "*.room." + roomId, message);
return messageService.saveMessage(message);
}
// 채팅방 퇴장
@MessageMapping("chat.exit.{roomId}")
public MessageInfo exitUser(@DestinationVariable("roomId") Long roomId, @Payload MessageCreateRequest message){
message.setMessage(message.getSender() + "님이 채팅방에 퇴장하였습니다.");
rabbitTemplate.convertAndSend("chat.exchange", "exit.room." + roomId, message);
return messageService.saveMessage(message);
}
}
"*.room." + roomI
로 설정해놨는데, 예를들어 x.room.1을 구독한 사람은 enter.room.1 , talk.room.1 으로 보넨 메시지를 모두 받을 수 있습니다. (밑에 예시 사진) ⬇️ 채팅 중에 다른 사람이 채팅방에 들어왔을 경우 입장 메시지를 받는 경우입니다."*.room." + roomI
로 설정하면 Subscription URL을 ~/enter.room.1 or ~/talk.room.1으로 하면 알아서 라우팅키가 enter.room.1 , talk.room.1이 됩니다.apic을 통해 채팅 시스템이 잘되는 것을 확인하였고 데이터베이스에도 채팅 데이터가 잘 저장됩니다!
채팅 서버를 구현해보기 위해 Web socket → + STOMP → + RabbitMQ를 적용해보았습니다. 이번 기회에 메시지 브로커인 Rabbitmq와 좀 더 친해졌다고 생각했고, 스마트폰을 가지고 있는 대한민국 국민들이 대부분 사용하는 채팅 서비스인 카카오톡은 정말 대단한 것 같습니다.
이 채팅기능에 서비스에 대한 추가적인 아이디어를 부여하고 그것에 맞는 기능들을 즉, 살을 붙여가며 다양한 기술들도 접목시키며 시스템을 확장시켜보면 좋을 것 같습니다.
참고 :
STOMP Plugin — RabbitMQ
WebSocket Support
Spring Websocket & STOMP
[Spring Boot] WebSocket과 채팅 (4) - RabbitMQ
WebSocket - In Memory 대신 외부 브로커 사용하는 이유
상세한 설명 글 덕분에 많은 도움이 되었습니다.
질문이 하나 있는데요
// 채팅방 대화
@MessageMapping("chat.talk.{roomId}")
public MessageInfo talkUser(@DestinationVariable("roomId") Long roomId, @Payload MessageCreateRequest message) {
rabbitTemplate.convertAndSend("chat.exchange", "*.room." + roomId, message);
return messageService.saveMessage(message);
}
=> 여기서 rabbitTemplate.convertAndSend("chat.exchange", "*.room." + roomId, message);
이 코드를 실행하면 stomp-subscription-임의의문자열 의 큐가 자동으로 생성되는 건가요?
아무리 찾아봐도 이해가 안가서요 ㅜㅜ