본문으로 바로가기

채팅 서버 구축 (1) - Websocket

웹 소켓만으로 채팅 서버를 구현할 때는 세션을 서버에서 따로 관리(Map 자료구조 이용)하였고, 메시지를 어떻게 처리할 지 직접 구현하였다. Websocket만 사용해서 구현하게 되면 해당 메시지가 어떤 요청이고, 어떤 포맷으로 오며 또 어떻게 메시지 통신 과정을 처리해야 하는지 등이 정해져있지 않아서 모두 일일이 구현해야 한다. 따라서 STOMP 프로토콜을 이용하여 메시징을 더 효율적으로 전송해보려고 한다.

 

목차

  • STOMP 개요
    • PUB/SUB(발행/구독) 이해하기
    • STOMP란
    • Message Broker
  • Spring WebSocket STOMP 
    • STOMP를 이용한 채팅 구현
    • 외부 메시지 브로커가 필요한 이유

 

STOMP 개요

PUB/SUB(발행/구독) 이해하기

  • 메시지를 공급하는 주체와 소비하는 주체를 분리하여 제공하는 메시징 방법
    • ex) 우체국(topic), 집배원(publisher), 구독자(subscriber)
    •  집배원이 신문을 우체통에 배달하는 액션이 있고, 우체통에 신문이 배달되는 것을 기다렸다가 오면 빼서 보는 구독자의 액션이 존재
  • publisher가 topic에 메시지를 보내면, 해당 topic을 구독해놓은 모든 subscriber에게 메시지가 전송되면서 데이터 교확이 이루어지는 방법
    • publisher: message를 생성한 뒤, topic에 담아두는 서버
    • message: publisher로부터 subscriber에게 최종적으로 전달되는 데이터
    • topic, channel: publisher가 message를 전달하는 리소스로, 구독 채널
    • subscription: message가 subscriber에게 전달되기 위한 구독 과정
    • subscriber: message를 수신하려는 서버
  • 채팅 서버에 대한 pub/sub 콘셉트
    • 채팅방을 생성한다. ➜ pub/sub 구현을 위한 Topic/Channel이 하나 생성된다.
    • 채팅방에 입장한다. ➜ Topic을 구독한다.
    • 채팅방에 메시지를 보내고 받는다. ➜ 해당 Topic으로 메시지를 발송(pub)하거나 수신(sub)받는다.

 

STOMP(Simple Text Oriented Messaging Protocol)란?

  • 메시징 전송을 효율적으로 하기 위한 프로토콜로, pub/sub 기반으로 동작한다.
    • 메시지의 송신과 수신에 대한 처리와 이동 경로를 명확하게 정의할 수 있다.
  • 특징
    • TCP 또는 Websocket과 같은 양방향 네트워크 프로토콜 기반으로 동작한다.
    • 메시지의 헤더에 값을 세팅할 수 있어 헤더 값을 기반으로 통신 시, 인증처리를 구현하는 것도 가능하다.
    • 메시지 브로커를 통해 특정 사용자에게만 메시지를 전송하는 기능 등을 가능하게 한다.
  • 스프링은 spring-websocket 모듈을 통해서 STOMP를 제공하고 있다.
    • WebSocketHandler를 직접 구현할 필요 없다. 즉, 메시지 프로토콜과 메세징 형식을 개발할 필요가 없다.
    • @MessagingMappin과 같은 어노테이션을 사용해서, 메시지 발행 시 엔드포인트를 별도로 분리해서 관리할 수 있다.
    • 스프링 부트 서버의 내부 메모리에 존재하는 내장 메시지 브로커(In Memory Brocker)를 통해 구독을 관리하고 메시지를 broadcast한다.

 

Message Broker

  • publisher로 부터 전달받은 메시지를 subscriber에게 메시지를 주고 받게 해주는 중간 역할을 담당한다.
  • 메시지가 적재되는 공간을 Message Queue(메시지큐)라고 하고, 메시지의 그룹을 Topic(토픽)이라고 한다.
SUBSCRIBE
destination:/sub/chat/room/3
SEND
content-type:application/json
destination:/pub/chat/message

{"roomId":3, "type":"TALK", "sender":"jim", "message":"hello!"}
  • 클라이언트는 메시지를 전송하기 위한 COMMAND로 SENDSUBSCRIBE을 사용한다.
    • 메시지의 내용과 수신 대상을 설명하는 destination 헤더와 함께 메시지에 대한 전송이나 구독을 할 수 있다.

 

Spring WebSocket STOMP

STOMP를 이용한 채팅 구현

WebSocketConfig


  
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// registry.addEndpoint("/ws-stomp").setAllowedOrigins("*");
registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*")
.withSockJS();
}
}
  • @EnableWebSocketMessageBroker: 메시지 브로커가 지원하는 Websocket 메시지 처리를 활성화한다.
  • configureMessageBroker(): 메모리 기반의 Simple Message Brokder를 활설화한다.
    • 메시지 브로커는 /sub으로 지작하는 주소의 Subscriber들에게 메시지를 전달하는 역할을 한다.
      • enableSimpleBroker(): 스프링에서 제공해주는 내장 브로커를 사용하는 함수
    • 클라이언트가 메시지 구독 요청을 보낼 대 붙여야하는 prefix를 /sub으로 시작하도록 지정한다.
    • 클라이언트가 서버로 메세지를 보낼 때 붙여야하는 prefix를 /pub으로 지정한다.
  • registerStompEndpoints(): Websocket 설정과 마찬가지로 HandShake와 통신을 담당할 endpoint를 지정한다.
    • 클라이언트에서 서버로 WebSocket 연결을 하고싶을 때 ws://localhost8080/ws-stomp로 요청을 보내면 된다.
    • 소켓을 지원하지 않는 브라우저가 있을 수 있다. withSockJS()을 사용하면 소켓을 지원하지 않는 브라우저일 경우 sock JS를 사용하도록 설정한다.

 

ChatRoom


  
@Getter
public class ChatRoom {
private String roomId;
private String name;
@Builder
public ChatRoom(String roomId, String name) {
this.roomId = roomId;
this.name = name;
}
}
  • pub/sub 방식을 이용하면 구독자가 알아서 관리되기 때문에 웹소켓의 세션 관리가 필요없어진다. 
  • 또한, 발송의 구현도 알아서 해결되기 때문에 클라이언트에게 메시지를 발송하는 부분을 구현할 필요가 없어진다.

 

ChatController ⇢ Publisher


  
@Controller
@RequiredArgsConstructor
public class ChatController {
private final SimpMessageSendingOperations messagingTemplate;
@MessageMapping("/chat/message")
public void message(ChatMessage message) {
if(message.getType().equals(MessageType.ENTER)) {
message.setEnterMessage();
}
messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
// chatService.saveMessage(message);
}
}
  • @MessageMapping을 이용해서 웹 소켓으로 들어오는 메시지 발행을 처리한다. (publisher 역할)
    • 클라이언트에서 prefix를 붙여서 /pub/chat/message로 발행 요청을 하면 Controller가 해당 메시지를 받아서 처리한다.
  • 메시지가 발행되면 /sub/chat/room/{roomId}로 메시지를 전송한다.
    • 클라이언트가 해당 주소를 구독하고 있다가 메시지를 전달받으면 화면에 출력된다.
  • 해당 컨트롤러가 WebSocketHandler가 담당하는 역할을 대신하고 있기 때문에 Hadler는 더 이상 필요하지 않다.

 

Subscriber 구현


  
// 연결
function connect() {
var socket = new WebSocket('/ws-stomp');
stompClient = Stomp.over(socket);
stompClient.connect({}, function () {
setConnected(true);
stompClient.subscribe('/sub/rooms/5', function (greeting) {
console.log(greeting.body);
});
});
}
function sendMessage() {
stompClient.send("/pub/chat/message", {}, JSON.stringify({
'message': $("#name").val(),
'sender': 'jim',
'roomId': 5,
'type': 'TALK'
}));
}
  • 구독자는 서버에서 따로 구현할 필요가 없다.
  • 웹뷰에서 stomp 라이브러리를 이용해 subscriber 주소를 바라보고 있는 코드만 작성하면 된다.

 

외부 메시지 브로커가 필요한 이유

  • 아무 설정 없이 Spring 환경에서 STOMP 프로토콜을 사용한다면 메시지 브로커는 기본적으로 In Memory Brocker를 사용한다.
  • In Memory Brocker는 몇 가지 단점이 존재한다.
    • 세션을 수용할 수 있는 크기가 제한되어 있다.
    • 서버가 다운되어서 메시지 전송을 못했다면, 큐는 인메모리 기반으로 동작하기 때문에 메시지를 유실할 수 있다.
    • 따로 모니터링하는 것이 불편하다.
  • 이를 해결하기 위해서 외부 브로커(External Broker)를 사용할 수 있다.
    • RabbitMQ, ActiveMQ 등에서 STOMP 프로토콜의 Message Broker 기능을 제공해준다.
    • 외에도 Redis, Kafka(이벤트 브로커) 등이 존재한다.

 

이 다음에 바로 외부 브로커 각각에 대해 좀 더 알아보고, 이를 바탕으로 채팅 서버를 더 개선시키고자 한다.

 


참고