STOMP의 사용 이유?
`WebSocket` 프로토콜은 두 가지 유형의 메시지를 정의하고 있지만,
그 메시지의 내용까지는 정의하고 있지 않다.
`STOMP`는 `WebSocket` 위에서 동작하는 프로토콜로써, 클라이언트와 서버가 전송할
- 메시지 유형
- 형식
- 내용
들을 정의하는 매커니즘이다.
STOMP 란?
`STOMP`는 `Simple Text Oriented Messaging Protocol`의 약자로 `TCP` 또는 `WebSocket`과 같은
양방향 네트워크 프로토콜 기반으로 동작한다.
이름에서도 알 수 있듯이, `STOMP`는 텍스트 지향 프로토콜이지만, `Message Payload`에는 `Text` 또는
`Binary` 데이터를 포함할 수도 있다.
`STOMP`는 HTTP 위에서 동작하는 `Frame` 기반의 프로토콜이며, `Frame`은 아래와 같은 형식을 가지고 있다.
COMMAND
header1:value1
header2:value2
Body^@
클라이언트는 `Message`를 전송하기 위해 `SEND`, `SUBSCRIBE`, `COMMAND`를 사용할 수 있다.
또한, `SEND`, `SUBSCRIBE`, `COMMAND` 요청 `Frame`에는 메시지가 무엇이고 누가 받아서
처리할 지에 관한 `Header` 정보를 함께 포함한다.
위와 같은 과정을 통해서, `STOMP`은 `Publish-Subscribe` 매커니즘을 제공한다.
즉, `Broker`를 통해서 다른 사용자들에게 메시지를 보내거나 서버가
특정 작업을 수행하도록 메시지를 보낼 수 있게 되는 것이다.
만약 스프링에서 지원하는 `STOMP`을 사용하게 된다면,
스프링 WebSocket 애플리케이션은 `STOMP Broker`로 동작한다.
스프링에서 지원하는 `STOMP`은 다양한 기능을 제공한다.
구체적으로 메시지를 `@Controller`의 메시지 핸들링하는 메서드로 라우팅하거나,
`Simple In-Memory Broker`를 이용해서 `Subscribe` 중인 다른 클라이언트들에게 메시지를 브로드캐스팅한다.
`Simple In-Memory 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` 프로토콜을 사용하여,
`WebSocket`만 이용할 때 보다 더 풍부한 프로그래밍 모델을 제공할 수 있는데 하나씩 살펴보자.
- 메시징 프로토콜을 만들고, 메시지 형식을 커스터마이징할 필요가 없다.
- 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` 헤더를 가진 메시지를 브로커로 라우팅한다.
- 또한, `/topic` 또는 `/queue`로 시작하는 `Destination` 헤더를 가진 메시지를 브로커로 라우팅한다.
내장된 `Simple Message Broker`는 `/topic`, `/queue` prefix에 대해 특별한 의미를 갖지 않는다.
`/topic`, `/queue` prefix는 단순히 메시지가 pub-sub, point-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>
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`이다.
- 즉, `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 Mesaage`를 다룰 수 있을 뿐만 아니라,
`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`) 정보를 저장한다.
- 이후, 클라이언트는 `/simple/good` 경로의 `Destination` 헤더를 가지고 메시지를 전송한다.
- `/simple` prefix는 해당 메시지가 `@MessageMapping` 메서드를 가진 컨트롤러로 라우팅될 수 있도록 도움을 준다.
- 구체적으로, `/simple` 접두사가 벗겨진 후에는 `/good` 목적지 경로만 남게 되고 `TestConroller`의 `@MessageMapping` 가진 `handle()` 메서드로 라우팅된다.
- `@MessageMapping` 가진 `handle()` 메서드가 반환한 값은 스프링의 `Message`로 변환된다.
`Message`의 `Payload`는 `handle()` 메서드가 반환한 값을 기반으로 하고, 기본적으로 `Destination` 헤더는 `/topic/good`로 설정된다.
`Destination` 헤더는 클라이언트가 보낸 기존 `/simple/good` 경로의 목적지 헤더에서 `/simple`를 `/topic`으로 변경된 값으로 설정된다.
이후, 변환된 `Message`는 `brokerChannel`로 전송되고 `메시지 브로커`에 의해서 처리된다. - 마지막으로 `메시지 브로커`는 매칭된 모든 구독자들(`subscribers`)을 탐색하고,
`clientOutboundChannel`을 통해서 각 구독자들에게 `MESSAGE` 프레임을 보낸다.
구체적으로, `clientOutboundChannel` 채널에서는 스프링의 `Message`를 STOMP의 `Frame`으로 인코딩하고, 연결된 `WebSocket` 커넥션으로 프레임을 전송한다.
Annotated Controllers
애플리케이션은 클라이언트로 부터 받은 메시지를 처리하기 위해 `@Controller` 클래스를 사용할 수 있다.
이러한, 컨트롤러는 `@MessageMapping`, `@SubscribeMapping`, `@ExceptionHandler` 메서드를 선언할 수 있는데, 구체적으로 어떤 역할을 하는지 하나씩 살펴보자.
@MessageMapping
`@MessageMapping` 메서드는 지정한 경로를 기반으로 메시지를 라우팅할 수 있다.
`@MessageMapping`은 메서드뿐 만 아니라 타입 레벨, 즉 클래스에도 설정할 수 있는데 이는 컨트롤러 안에서 공통된 경로를 제공하기 위해서 사용된다.
기본적으로, 매핑은 `Ant-Style` Path 패턴으로 구성하고, Template 변수도 지원한다.
(ex, `/something*`, `/something/{id}`)
Template 변수는 `@DestinationVariable`로 선언한 메서드 인자를 통해서 전달받을 수 있다.
또한, 애플리케이션은 `dot-separated` 기반의 `Destination` 컨벤션으로 바꿀 수 도 있는데, 이는 아래서 다시 언급하겠다.
Method Arguments
그럼, `@DestinationVariable`과 같은 메서드에서 지원하는 인자 목록에 대해서 살펴보자.
다음은 위 Method Argument를 적용한 예제이다.
@Controller
public class TestController {
@MessageMapping("/good/{id}")
public String handle(Message message, MessageHeaders messageHeaders,
MessageHeaderAccessor messageHeaderAccessor, SimpMessageHeaderAccessor simpMessageHeaderAccessor,
StompHeaderAccessor stompHeaderAccessor, @Payload String payload,
@Header("destination") String destination, @Headers Map<String, String> headers,
@DestinationVariable String id) {
System.out.println("---- Message ----");
System.out.println(message);
System.out.println("---- MessageHeaders ----");
System.out.println(messageHeaders);
System.out.println("---- MessageHeaderAccessor ----");
System.out.println(messageHeaderAccessor);
System.out.println("---- SimpMessageHeaderAccessor ----");
System.out.println(simpMessageHeaderAccessor);
System.out.println("---- StompHeaderAccessor ----");
System.out.println(stompHeaderAccessor);
System.out.println("---- @Payload ----");
System.out.println(payload);
System.out.println("---- @Header(\"destination\") ----");
System.out.println(destination);
System.out.println("---- @Headers ----");
System.out.println(headers);
System.out.println("---- @DestinationVariable ----");
System.out.println(id);
return payload;
}
}
Return Values
기본적으로 `@MessageMapping` 메서드가 반환한 값은 일치한 `MessageConverter` 통해서 `Payload`로 직렬화된다.
그리고나서, `Message`에 담겨 `brokerChannel` 보내지고 구독자들(`subscribers`)에게 브로드 캐스팅된다.
이 과정에서, `Message`의 `Destination` 헤더는 클라이언트로 부터 전달받은 `Destination` 헤더 값에서 접두사만 `/topic` 으로 변경된 값으로 설정된다.
만약 `Destination` 헤더를 직접 설정하고 싶다면, `@SendTo` 또는 `@SendToUser`을 사용하면 된다.
`@SendTo`과 `@SendToUser`은 동시에 같은 메서드 또는 클래스에서 사용할 수도 있다.
- `@SendTo`는 특정 또는 다수의 목적지(Destination 헤더)를 설정하는 경우에 사용한다.
- `@SendToUser`는 오직 Input Message와 관련된 사용자에게만 Output Message를 보내도록 설정한다.
A Quick Example of Spring Websockets' @SendToUser Annotation | Baeldung
또한, 만약 `@MessageMapping` 메서드에서 메시지를 비동기적으로 처리하고 싶은 경우에는
`ListenableFuture`, `CompletableFuture` 또는 `CompletionStage` 객체를 반환하면 된다.
`@SendTo`과 `@SendToUser`은 단순히 `SimpMessagingTemplate`을
사용해서 메시지를 보내는 편의에 불과하다는 것을 명심하자.
따라서, 요구 상황에 따라 `@MessageMapping` 메서드에서 `SimpMessagingTemplate`을 직접 사용해야 할 경우도 있다.
`SimpMessagingTemplate`을 이용하면 반환 값없이 메서드 내부에서 처리를 끝마칠 수 있다.
@SubscribeMapping
`@SubscribeMapping`은 `@MessageMapping`와 유사하지만, 오직 `Subscription` 메시지만 매핑한다는 차이점이 있다.
또한 `@SubscribeMapping`은 `@MessageMapping`와 동일한 `Method Arguments`을 제공한다.
하지만, `Return Value`는 기본적으로 `brokerChannel` 통해서 브로커로 전달되는 것이 아니라, `clientOutboundChannel` 통해서 클라이언트에게 직접 보내진다는 차이점이 있다.
만약 `@SendTo` 또는 `@SendToUser`를 통해서 재정의한다면 `Return Value`을 브로커에게 보낼 수도 있다.
그럼, `@SubscribeMapping`은 언제 사용하는 것일까?
브로커는 `/topic`과 `/queue`에 매핑되어 있고, 애플리케이션 컨트롤러는 `/app`에 매핑되어 있다고 가정해보자.
이러한 설정에서, 브로커가 `/topic`, `/queue`에 대한 모든 구독(subscriptions) 정보를 저장하고 있으므로,
애플리케이션은 개입하지 않아도 된다.
하지만, 클라이언트가 `/app` 접두사를 가진 목적지로 구독 요청 보내는 상황을 생각해보자.
`@SubscribeMapping`을 사용한다면, 컨트롤러는 브로커 통과없이 `Return Value`를 구독에 대한 응답으로 보낸다.
즉, `@SubscribeMapping`은 브로커에 구독 정보를 저장하지 않을 뿐더러 구독 정보를 재활용하지도 않는 일회성 용도로 사용된다. 일회성 request-reply 교환인 것이다.
좀 더 단적인 예로, 시작과 동시에 UI 초기 데이터를 채우기 위한 용도로 많이 사용된다.
위와 같은 이유가 아니라면, 브로커와 컨트롤러는 동일한 `Destination` 접두사로 매핑하지 않도록 해야한다.
Inbound 메시지는 병렬적으로 처리되기 때문에, 브로커와 컨트롤러 중에 어느 것이 먼저 처리하는 지 보장하지 않는다.
@MessageExceptionHandler
애플리케이션은 `@MessageMapping` 메서드에서 발생한 `Exception`을 처리하기 위해서
`@MessageExceptionHandler` 메서드를 지원한다.
발생한 예외는 `Method Argument`을 통해 접근할 수 있다.
@Controller
public class TestController {
// ...
@MessageExceptionHandler
public Exception handleException(CustomeException exception) {
// ...
return exception;
}
}
`@MessageExceptionHandler` 메서드는 전형적으로 선언된 컨트롤러 내부의 예외를 처리한다.
만약 좀 더 글로벌하게 예외 처리 메서드를 적용하고 싶다면, `@MessageExceptionHandler` 메서드를 `[@ControllerAdvice](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#mvc-ann-controller-advice)` 컨트롤러에 선언하면 된다.
Message 전송
만약 애플리케이션에서 연결된 클라이언트에게 메시지를 보내야 할 경우에는 어떻게 해야 할까?
애플리케이션 구성 요소는 `BrokerChannel`로 메시지를 보낼 수 있는데,
가장 간단한 방법은 아래와 같이 `SimpMessagingTemplate`을 주입받아서 메시지를 전송하는 것이다.
@Controller
public class TestController {
private SimpMessagingTemplate simpMessagingTemplate;
public TestController(SimpMessagingTemplate simpMessagingTemplate) {
this.simpMessagingTemplate = simpMessagingTemplate;
}
@PostMapping(path = "/greet")
@ResponseBody
public void greet(@RequestBody String greet) {
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
simpMessagingTemplate.convertAndSend("/topic/greet", "[" + now + "]" + greet);
}
}
<!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/greet", 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");
fetch("http://localhost:8080/greet", {
method: "POST",
body: message.value
});
message.value = '';
}
</script>
</html>
'Spring > WebSocket' 카테고리의 다른 글
[Spring WebSocket] SockJS (2) | 2024.11.04 |
---|---|
[Spring WebSocket] WebSocket (0) | 2024.11.04 |
[Spring WebSocket] SSE vs WebSocket (0) | 2024.11.04 |