STOMP ?
STOMP는 Simple Text Oriented Messaging Protocol의 약자로 TCP 또는 WebSocket 같은 양방향 네트워크 프로토콜 기반으로 동작한다
STOMP는 텍스트 지향 프로토콜이지만 Message Payload에는 Text 또는 Binary 데이터를 포함할 수 있음
COMMAND
header1:value1
header2:value2
Body^@
STOMP Protocol Specification, Version 1.2
클라이언트는 Message 전송을 위해 SEND, SUBSCRIBE, COMMAND를 사용할 수 있음
또, SEND, SUBSCRIBE, COMMAND 요청 Frame에는 메시지가 무엇기고 누가 받아서 처리할지에 대한 Header 정보를 함께 포함함
위 같은 과정을 통해, STOMP는 Publish-Subscribe 매커니즘을 제공, Broker를 통해서 다른 사용자들에게 메시지를 보내거나 서버가 특정 작업을 수행하도록 메시지를 보낼 수 있게 되는 것임
스프링에서 지원하는 STOMP 사용 시, 스프링 WebSocket 애플리케이션은 STOMP Broker로 동작함
스프링 STOMP의 제공 기능
- 메시지를 @Controller의 메시지 핸들링하는 메서드로 라우팅
- Simple InMemory Broker를 이용해서 Subscribe 중인 다른 클라이언트들에게 메시지를 브로드캐스팅 함
- Simple InMemory Broker는 클라이언트의 Subscribe 정보를 자체적으로 메모리에 유지함
- RabbitMQ, ActiveMQ 같은 외부 Messaging System을 STOMP Broker로 사용할 수 있도록 지원함
스프링은 외부 STOMP Broker와 TCP 커넥션을 유지하고, 외부 STOMP Broker는 서버-클라이언트 사이의 매개체로 동작함
- 스프링은 메시지를 외부 브로커에 전달하고, 브로커는 WebSocket으로 연결된 클라이언트에게 메시지를 전달하는 구조
위 같은 구조 덕에 스프링 웹 애플리케이션은 HTTP 기반의 보안 설정과 공통된 검증등을 적용할 수 있게 됨
클라이언트가 특정 경로에 대해 Subscribe한다면, 서버는 원할 때마다 클라이언트에게 메시지를 전송할 수 있음
SUBSCRIBE
id:sub-1
destination:/topic/something.*
^@
클라이언트는 서버에 메시지를 전달할 수 있는데, 서버는 @MessageMapping 된 메서드를 통해서 해당 메시지를 처리할 수 있다.
서버는 Subscribe한 클라이언트들에게 메시지를 브로드캐스팅할 수도 있음
SEND
destination:/queue/something
content-type:application/json
content-length:38
{"key1":"value1","key2":"value2", 38}^@
STOMP 스펙에서는 의도적으로 Destination 정보를 불분명하게 정의하였는데, 이는 STOMP 구현체에서 문자열 구문에 따라 직접 의미를 부여하도록 하기 위해서임.
따라서, Destination 정보는 STOMP 서버 구현체마다 달라질 수 있기 때문에 각 구현체의 스펙을 살펴봐야 함
그러나, 일반적으로 /topic 문자열로 시작하는 구문은 일대다(one-to-many) 관계의 publish-subscribe를 의미하고, /queue 문자열로 시작하는 구문은 일대일(one-to-one) 관계의 메시지 교환을 의미함
STOMP 서버는 MESSAGE COMMAND를 사용해서 모든 Subscriber들에게 메시지를 브로드캐스트할 수 있음
MESSAGE
message-id:d4c0d7f6-1
subscription:sub-1
destination:/topic/something
{"key1":"value1","key2":"value2"}^@
STOMP Broker는 반드시 애플리케이션이 전달한 메시지를 구독한 클라이언트에게 전달해야하며, 서버 메시지의 subscription 헤더는 클라이언트가 SUBSCRIBE한 id 헤더와 일치해야만 함
STOMP 장점
Spring Framework 및 Spring Security는 STOMP 프로토콜을 사용하여, WebSockets만 이용할 때보다 더 풍부한 프로그래밍 모델을 제공할 수 있음
- 메시징 프로토콜을 만들고, 메시지 형식을 커스터마이징할 필요가 없다
- RabbitMQ, ActiveMQ 같은 Message Broker를 이용해서, subscription을 관리하고 메시지를 브로드캐스팅할 수 있음
- WebSocket 기반으로 각 커넥션마다 WebSocketHandler를 구현하는 것보다, @Controller된 객체를 이용해서 조직적으로 관리할 수 있음
- 메시지들은 STOMP의 Destination 헤더를 기반으로, @Controller 객체의 @MethodMapping 메서드로 라우팅됨
- STOMP의 Destination 및 Message Type을 기반으로 메시지를 보호하기 위해, Spring Security를 사용할 수 있음
STOMP 사용
서버
스프링은 WebSocket 또는 SockJS 기반으로 STOMP를 위해 spring-messaging and spring-websocket 모듈을 제공한다
STOMP는 설정을 할 수 있는데, 기본적으로 커넥션을 위한 STOMP Endpoint를 설정해야만 함
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/test").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/simple")
.enableSimpleBroker("/topic", "/queue");
}
}
- /test는 WebSocket 또는 SockJS 클라이언트가 WebSocket Handshake로 커넥션을 경로이다
- /simple 경로로 시작하는 STOMP 메시지의 Destination 헤더는 @Controller 객체의 @MessageMapping 메서드로 라우팅 됨
- 내장된 메시지 브로커를 사용하여 클라이언트에게 subscriptions, broadcasting 기능을 지원함
/topic 또는 /queue로 시작하는 Destination 헤더를 가진 메시지를 브로커로 라우팅함
내장된 Simple Message Broker는 /topic, /queue prefix에 대해 특별한 의미를 갖지 않는다
- /topic, /queue prefix는 단순히 메시지가 pub-sub, poit-to-point 인지 여부를 나타내는 컨벤션일 뿐이며, 외부 브로커를 사용할 경우에는 해당 Destination 헤더 prefix가 달라질 수 있음
클라이언트
브라우저인 클라이언트 측면에서는 SockJS 프로토콜을 사용하기 위해서 sockjs-client 라이브러리를 사용함(최근에는 STOMP 프로토콜의 클라이언트 라이브러리는 webstomp-client를 많이 사용)
예제 - 클라이언트가 SockJS를 기반으로 한 STOMP 프로토콜을 이용한 서버와 통신
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.message {
margin: 5em;
color: olive;
border: solid 2px #D2691E;
-webkit-transition: 0.5s; transition: 0.5s;
width: 50%;
padding: 10px 20px;
box-sizing: border-box;
}
.messages {
margin: 5em;
}
.send-btn {
background-color: #F4FEE2;
padding: 10px 20px;
}
li {
list-style-type: none;
margin: 1em;
text-align: center;
font-size: 2em;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script type="text/javascript" src="node_modules/webstomp-client/dist/webstomp.min.js"></script>
</head>
<body>
<div>
<input type="text" class="message"/>
<button onClick='send()' class="send-btn">보내기</button>
</div>
<div class="messages">
<div id="messages"></div>
</div>
</body>
<script>
let stomp;
document.addEventListener("DOMContentLoaded", function() {
// ["websocket", "xhr-streaming", "xhr-polling"]
const sock = new SockJS('http://localhost:8080/test', null, {transports: ["xhr-polling"]});
stomp = webstomp.over(sock);
stomp.connect({}, function(frame) {
console.log('Connected!!');
stomp.subscribe("/topic/good", function(frame) {
const messages = document.querySelector("#messages");
const message = document.createElement("li");
message.innerText = frame.body;
messages.appendChild(message)
});
});
});
function send() {
const message = document.querySelector(".message");
stomp.send('/simple/good', message.value);
message.value = '';
}
</script>
</html>
SockJS 프로토콜을 사용하고 있지만, WebSocket만 사용하고 싶은 경우에는 브라우저에서 지원하는 WebSocket 객체를 이용할 수 있음
const socket = new WebSocket("http://localhost:8080/test");
const stomp = Stomp.over(socket);
stomp.connect({}, function(frame) {
...
}
- 예제에서는 login과 passcode 헤더가 필요하지 않았음, 클라이언트에서 설정했더라도 서버 측에서 무시됐을 것임
스프링 제공 샘플 코드
Message Flow
STOMP Endpoint를 노출하면, 스프링 애플리케이션은 연결된 클라이언트에 대한 STOMP Broker가 됨
구성 요소
spring-message 모듈은 스프링 프레임워크의 통합된 메시징 애플리케이션을 위한 근본적인 지원을 한다
몇 가지 사용 가능 한 메시징 추상화에 대한 설명
- Message는 headers와 payload를 포함하는 메시지의 representation 임
- MessageHandler는 Message 처리에 대한 계약이다
- MessageChannel은 Producers와 Consumers의 느슨한 연결을 가능하게 하는 메시지 전송에 대한 계약임
- SubscribableChannel은 MessageHandler 구독자(Subscribers)를 위한 MessageChannel임
- Subscribers를 관리하고, 해당 채널에 전송된 메시지를 처리할 Subscribers를 호출함
- ExecutorSubscribableChannel은 Executor를 사용해서 메시지를 전달하는 SubscribableChannel임
- ExecutorSubscribableChannel은 각 구독자(Subscribers)에게 메시지를 보내는 SubscribableChannel임
Java 기반의 설정(@EnableWebSocketMessageBroker)과 XML 네임스페이스 기반의 설정(websocket:message-broker)은 모두 위의 구성 요소를 사용해서 message workflow를 구성함
내장 메시지 브로커를 사용한 경우의 컴포넌트 구성
- clientInboundChannel은 WebSocket 클라이언트로부터 받은 메시지를 전달함
- clientOutboundChannel은 WebSocket 클라이언트에게 메시지를 전달함
- brokerChannel은 서버의 애플리케이션 코드 내에서 브로커에서 메시지를 전달함
외부 브로커를 사용해서 subscriptions 과 broadcasting 메시지를 관리하도록 설정한 구성 요소
- 두 구성 방식의 주요한 차이점은 Broker Relay 사용 여부임
- Broker Relay의 역할
- TCP 기반으로 외부 STOMP Broker에게 메시지를 전달
- 브로커로부터 받은 메시지를 구독한 클라이언트에게 전달
동작 흐름
- WebSocket 커넥션으로부터 메시지를 전달받는다
- STOMP Frame으로 디코드한다
- 스프링에서 제공하는 Message Representation으로 변환한다
- 추가 처리를 위해, clientInboundChannel로 전송한다
- STOMP Message의 Destination 헤더가 /app으로 시작한다면, @MessageMapping 정보와 매핑된 메서드를 호출한다
- 반면에, Destination 헤더가 /topic 또는 /queue로 시작한다면, 메시지 브로커로 바로(직접) 라우팅된다
Message 처리 과정
@Controller 컨트롤러는 클라이언트로부터 받은 STOMP Message를 다룰 수 있을뿐만 아니라, brokerChannel을 통해서 메시지 브로커에게 메시지를 보낼 수도 있음
이후, 메시지 브로커는 매칭된 구독자들(subscribers)에게 clientOutboundChannel을 통해서 메시지를 브로드캐스팅한다
동일한 컨트롤러의 HTTP 요청에 대한 응답 처리 과정에서 같은 작업을 수행할 수 있다
- 클라이언트가 HTTP POST 요청을 보내면 @PostMapping 메서드는 메시지 브로커에게 메시지를 보내 구독자들(subscribers)에게 브로드캐스팅할 수도 있음
예제
@Configuration
@ComponentScan(basePackages = "com.example.demo")
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/test").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/simple")
.enableSimpleBroker("/topic", "/queue");
}
}
@Controller
public class TestController {
@MessageMapping("/good")
public String handle(String message) {
return message + " - good";
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.message {
margin: 5em;
color: olive;
border: solid 2px #D2691E;
-webkit-transition: 0.5s; transition: 0.5s;
width: 50%;
padding: 10px 20px;
box-sizing: border-box;
}
.messages {
margin: 5em;
}
.send-btn {
background-color: #F4FEE2;
padding: 10px 20px;
}
li {
list-style-type: none;
margin: 1em;
text-align: center;
font-size: 2em;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script type="text/javascript" src="node_modules/webstomp-client/dist/webstomp.min.js"></script>
</head>
<body>
<div>
<input type="text" class="message"/>
<button onClick='send()' class="send-btn">보내기</button>
</div>
<div class="messages">
<div id="messages"></div>
</div>
</body>
<script>
let stomp;
document.addEventListener("DOMContentLoaded", function() {
// ["websocket", "xhr-streaming", "xhr-polling"]
const sock = new SockJS('http://localhost:8080/test', null, {transports: ["xhr-polling"]});
stomp = webstomp.over(sock);
stomp.connect({}, function(frame) {
console.log('Connected!!');
stomp.subscribe("/topic/good", function(frame) {
const messages = document.querySelector("#messages");
const message = document.createElement("li");
message.innerText = frame.body;
messages.appendChild(message)
});
});
});
function send() {
const message = document.querySelector(".message");
stomp.send('/simple/good', message.value);
message.value = '';
}
</script>
</html>
- 클라이언트는 http://localhost:8080/test 에 연결하여 커넥션을 수립하고, STOMP 프레임들을 해당 커넥션으로 전송하기 시작함
- 클라이언트는 /topic/good 경로의 Destination 헤더를 가지고 SUBSCRIBE 프레임을 전송한다
- 서버는 프에림을 수신하면 디코딩하여 Message로 변환하고, 메시지를 clientInboundChannel로 전송함
- 해당 clientInboundChannel 채널에서 메시지를 메시지 브로커로 바로 라우팅해주고, 메시지 브로커는 해당 클라이언트의 구독(Subscription) 정보를 저장한다
- 클라이언트는 /test/good 경로의 Destination 헤더를 가지고 메시지를 전송한다
- /test prefix는 해당 메시지가 @MessageMapping 메서드를 가진 컨트롤러로 라우팅될 수 있도록 도움을 줌
- /test 접두사가 벗겨진 후에는 /good 목적지 경로만 남게 되고 TestController의 @MessageMapping 가진 handle( ) 메서드로 라우팅 된다
- @MessageMapping을 가진 handle( ) 메서드가 반환한 값은 스프링의 Message로 변환 됨
- Message의 Payload는 handle( ) 메서드가 반환한 값을 기반으로 하고, 기본적으로 Destination 헤더는 /topic/good로 설정된다
- Destination 헤더는 클라이언트가 보낸 기존 /test/good 경로의 목적지 헤더에서 /test를 /topic으로 변경된 값으로 설정된다
- 이후, 변환된 Message는 brokerChannel로 전송되고 메시지 브로커에 의해서 처리된다
- 마지막으로 메시지 브로커는 매칭된 모든 구독자들(subscribers)을 탐색하고, clientOutboundChannel을 통해서 각 구독자들에게 MESSAGE 프레임을 보낸다
- clientOutboundChannel 채널에서는 스프링의 Message를 STOMP의 Frame으로 인코딩하고, 연결된 WebSocket 커넥션으로 프레임을 전송한다
'항해 99 > Spring' 카테고리의 다른 글
WebSocket - 실제 코드 분석 (1) | 2024.04.03 |
---|---|
WebSocket - STOMP 2 (0) | 2024.03.30 |
WebSocket - SockJS (0) | 2024.03.28 |
WebSocket - 기본 websocket (0) | 2024.03.27 |
Redis (1) | 2024.03.25 |