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
, UNSUBSCRIBE
header
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 뒤에서 메시지 전송을 하지 못했다면, 큐는 인메모리 기반으로 동작하므로 메시지는 유실될 것이다. 하지만 외부 메시지 브로커를 사용하고 있었다면, 서버 재실행 시에도 외부 브로커에 저장 중인 큐에 대기 중인 메시지를 수신할 수 있다.
다만, 무조건 외부 브로커 연동을 하는 것은 좋지 않다. 외부 메시지 브로커를 사용하면 인프라 비용이 증가하므로 여러 상황을 고려해서 시스템 아키텍처를 잘 구성해야 한다.