WebSocket 프로토콜은 두 가지 유형의 메시지를 정의하고 있지만 그 메시지의 내용까지는 정의하고 있지 않다.
Simple Text Oriented Messaging Protocol
텍스트 기반의 메시지 프로토콜
STOMP 프로토콜은 WebSocket만을 위해 만들어진 프로토콜이 아니다.
WebSocket
WebSocketHandler 구현이 필요하다.STOMP
WebSocketHandler를 구현할 필요 없이 @Controller, @MessageMapping 애노테이션만 사용하면 된다.@MessageMapping으로 메시지를 발행하면 엔드포인트를 별도로 분리하여 관리할 수 있다.destination 헤더를 기반으로 @Controller 객체의 @MessageMapping 메서드로 라우팅된다.STOMP는 커맨드, 헤더, 바디로 이루어진 Frame 단위를 정의해 두었다.
Text이다.key:value 형태로 header의 정보를 포함한다.COMMAND
header1 : value1
header2 : value2
Body^@
SEND, SUBSCRIBE COMMAND를 사용할 수 있다.destination 헤더는 메시지를 어디에 전송(SEND)할지, 어디에서 메시지를 구독(SUBSCRIBE)할지를 나타낸다.
destination은 의도적으로 정보를 불분명하게 정의했다.
이는 STOMP 구현체에서 문자열 구문에 따라 직접 의미를 부여하도록 하기 위함이다.일반적으로는 다음의 형식을 따른다.
topic/...-> publish-subscribe(1:N)queue/...-> point-to-point(1:1)
COMMAND
SEND, SUBSCRIBE, UNSUBSCRIBEheader
destination 헤더로 메시지를 보내거나(SEND), 구독(SUBSCRIBE)할 수 있음Body
^@
NULL 문자)스프링에서 지원하는 STOMP는 많은 기능을 한다.
예를 들어 Simple In-Memory Broker를 이용해SUBSCRIBE중인 다른 클라이언트에게 메시지를 보낸다.
메시지를 보내는 발신자(Publisher), 메시지를 받으려는 구독자(Subscriber)가 있다.
구독자는 /topic이라는 경로로 구독을 하고 있다.
발신자는 /topic을 destination 헤더로 넣고, 메시지를 메시지 브로커를 통해 구독자에게 송신할 수 있다.
서버에서 데이터 가공이 필요하다면 /app 경로로 메시지를 송신할 수 있다.
/topic이라는 경로에 담아 메시지 브로커에 전달한다./topic을 구독하는 구독자들에게 전달한다.메시지 스펙 변경 MessageDto
@Getter
@Setter
@NoArgsConstructor(access = PROTECTED)
public class MessageDto {
private MessageType type; // 메시지 타입
private String sender; // 보내는 사람
private String channelId; // 채널 ID
private String message; // 메시지
private LocalDateTime time; // 채팅 발송 시간
@Builder
public MessageDto(
MessageType type, String sender, String channelId, String message, LocalDateTime time) {
this.type = type;
this.sender = sender;
this.channelId = channelId;
this.message = message;
this.time = time;
}
}
받는 사람을 DTO에 지정해 주었다.채널에 메시지를 전송할 것이므로 channelId를 DTO에 포함시켰다.WebSocketMessageBrokerConfig 추가
@Configuration
@EnableWebSocketMessageBroker
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 엔드포인트 등록
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// stomp 접속 URL: "/ws-stomp"
registry
.addEndpoint("/ws-stomp")
.setAllowedOrigins("*");
}
/**
* 메시지 브로커 설정
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 메시지 구독 요청 URL -> SUBSCRIBE하는 클라이언트에게 메시지 전달
registry.enableSimpleBroker("/sub");
// 메시지 발행 요청 URL -> 클라이언트에서 SEND 요청 처리
registry.setApplicationDestinationPrefixes("/pub");
}
}
@EnableWebSocketMessageBroker 애노테이션을 달고, WebSocketMessageBrokerConfigurer 인터페이스를 구현한다.
registerStompEndpoints() 메서드는 WebSocket의 handler와 유사하다.
/ws-stomp로 정의했다.configureMessageBroker() 메서드는 STOMP에서 사용하는 메시지 브로커를 설정하는 메서드이다.
/sub/channel/{channelId}로 정의한다./pub/message로 메시지를 보내도록 한다.channelId를 포함해야 한다.WebSocket과는 달리 STOMP는 하나의 연결 주소마다 핸들러 클래스를 따로 구현할 필요 없이 Controller 방식으로 간편하게 사용할 수 있다.
지금까지의 코드를 살펴봤을 때 메시지 브로커, 메시지 큐는 스프링 부트 서버의 내부 메모리에 존재한다.
다시 말하면 WebSocket 서버가 다수일 때에는 정상적으로 동작하지 않는다.
인메모리 기반 시스템은 메시지 유실 가능성이 있다.
서버가 down 뒤에서 메시지 전송을 하지 못했다면, 큐는 인메모리 기반으로 동작하므로 메시지는 유실될 것이다. 하지만 외부 메시지 브로커를 사용하고 있었다면, 서버 재실행 시에도 외부 브로커에 저장 중인 큐에 대기 중인 메시지를 수신할 수 있다.
다만, 무조건 외부 브로커 연동을 하는 것은 좋지 않다. 외부 메시지 브로커를 사용하면 인프라 비용이 증가하므로 여러 상황을 고려해서 시스템 아키텍처를 잘 구성해야 한다.