스프링 웹 소켓 알아보기(4)
이번 포스팅부터는 STOMP에 대하여 알아보자
앞선 포스팅에서 알아보았듯, WebSocket은 HTTP와 달리 메시지의 구조나 의미(semantics)에 대한 표준화된 규약이 없다.
따라서 WebSocket을 사용할 때, 클라이언트와 서버가 주고받을 메시지의 형식과 내용을 직접 정의해야 한다.
하지만 이렇게 모든 메시지의 규칙을 직접 구현하는 것은 복잡하고 번거롭다.
이를 해결하기 위해 등장한 것이 바로 STOMP(Simple/Streaming Text Oriented Messaging Protocol)다.
STOMP는 WebSocket 위에서 동작하며, 메시지의 형식과 의미를 표준화하여 메시징을 간단하고 효율적으로 처리할 수 있도록 도와준다.
STOMP 프레임 구조
STOMP는 HTTP를 모델로 한 프레임 기반 프로토콜이다.
COMMAND
header1:value1
header2:value2
Body^@
- COMMAND : 실행할 명령(SEND, SUBSCRIBE 등)
- 헤더 : 메시지의 속성과 목적지를 지정
- Body : 메시지 내용
클라이언트는 SEND 또는 SUBSCRIBE 명령을 사용하여 메시지를 보내거나 구독할 수 있다. 이 메시지는 브로커를 통해 다른 연결된 클라이언트에게 메시지를 보내거나, 서버에 메시지를 보내 작업을 수행하도록 요청할 수 있다.
이게 간단한 pub/sub 메커니즘이다.
Spring에서의 STOMP
- 스프링의 STOMP 지원을 활성화하면 스프링 애플리케이션이 STOMP 브로커 역할을 한다.
- 메시지를
@Controller의 메시지 핸들링 메서드로 라우팅하거나, 간단한 메모리 브로커를 통해 구독자에게 브로드캐스트한다. - RabbitMQ, ActiveMQ 같은 외부 STOMP 브로커와 통합 가능
- Spring은 브로커와의 TCP 연결을 유지하며, 메시지를 중계하거나 WebSocket 클라이언트와 교환
STOMP 예시
공식문서에 있는 예시를 통해 이해해보자.
아래는 주식시세를 주기적으로 가져오는 서버에 클라이언트가 구독하는 예시이다.
1. 구독 요청(클라이언트 -> 서버)
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
^@
- 클라이언트는
/topic/price.stock.*경로의 메시지를 수신하도록 요청
2. 메시지 전송 (클라이언트 -> 서버)
SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@
- 클라이언트가 서버에 주식 구매 요청을 보냄
- 서버는
@MessageMapping메서드를 통해 이를 처리
3. 메시지 브로드캐스트(서버 -> 클라이언트)
MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM
{"ticker":"MMM","price":129.45}^@
- 서버가
/topic/price.stock.MMM을 구독한 클라이언트에게 주식 가격 메시지를 전달
STOMP의 destination
STOMP에서는 목적지의 의미가 명확하게 정해져 있지 않으며 서버에서 정의한다.
- 일반적인 패턴
/topic/..: Publish-Subscribe(1:N)/queue/..: Point-to-Point(1:1)
제한사항
- 서버는 구독 요청이 없는 경우 메시지를 전송할 수 없다.
- 서버에서 보낸 메시지는 항상 특정 클라이언트 구독과 연관되어야 한다.
Spring에서 STOMP 활성화
스프링에서는 WebSocket 위에서 STOMP를 사용하는 기능을 제공한다.
STOMP는 spring-messaging 및 spring-websocket 모듈에서 제공한다.
사용하는 공식문서 예제를 보자.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 클라이언트가 WebSocket 핸드셰이크를 요청할 URL
registry.addEndpoint("/portfolio");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// "/app"으로 시작하는 메시지는 @Controller의 @MessageMapping 메서드로 라우팅
config.setApplicationDestinationPrefixes("/app");
// /topic 또는 /queue로 시작하는 메시지는 내장 브로커로 전송
config.enableSimpleBroker("/topic", "/queue");
}
}
- Spring에서 STOMP 지원을 활성화하려면
@EnableWebSocketMessageBroker를 사용하면 된다. 주석을 확인하면 이해하기 쉬울 것이다.만약 외부 브로커를 사용할 경우 브로커가 지원하는 STOMP 목적지와 접두사를 확인해야한다.
서버측에서의 메시지 흐름
STOMP 엔드포인트를 노출하면 스프링 애플리케이션은 연결된 클라이언트에 대한 STOMP 브로커가 된다.
이 과정에서 스프링은 메시지 전송 및 라우팅을 처리하기 위해 다양한 구성요소를 활용한다.
Message- 메시지의 헤더와 페이로드를 포함한 단순한 메시지 표현
MessageHandler- 메시지를 처리하는 핸들러
MessageChannel- 메시지를 송신할 수 있는 채널로, 생산자와 소비자를 느슨하게 결합
SubscribableChannel- MessageChannel의 구독 가능 버전
- MessageHandler가 구독자로 추가될 수 있음
아래는 먼저 스프링 내장 브로커를 사용할 경우의 공식문서 다이어그램이다.
이때 사용되는 채널흐름은 다음과 같다.
clientInboundChannel: 클라이언트의 메시지 처리clientOutboundChannel: 서버 -> 클라이언트로 메시지전송brokerchannel: 서버 -> 브로커로 메시지전달
외부 브로커를 사용할 경우 다이어그램은 아래와 같다.

- 외부 브로커를 사용하는 경우 StompBrokerRelay가 되었다.
- StompBrokerRelay는 외부 브로커로 메시지를 전송하거나 클라이언트의 메시지를 브로커로 중계한다.
- TCP 연결을 통해 외부 브로커와 통신하며 구독 및 메시지 브로드캐스트를 관리한다.
이제 메시지 처리 흐름을 살펴보자
- WebSocket 클라이언트에서 메시지 수신
- WebSocket 연결에서 수신된 메시지는 STOMP 프레임으로 디코딩되고, Spring의
Message표현으로 변환된 뒤clientInboundChannel로 전달된다.
- WebSocket 연결에서 수신된 메시지는 STOMP 프레임으로 디코딩되고, Spring의
- 메시지 라우팅
- 위에서
Message표현된destination헤더가/app으로 시작하면 컨트롤러의@MessageMapping메서드로 라우팅된다. /topic이나/queue경로라면 메시지는 메시지 브로커로 직접 라우팅된다.
- 위에서
- 메시지 브로커 동작
- 컨트롤러는 메시지를 처리한 후
brokerChannel을 통해 메시지를 브로커로 전송 - 브로커는 해당 메시지를 구독자에게 브로드캐스트하며, 이는
clientOutboundChannel을 통해 클라이언트에게 전달된다.
- 컨트롤러는 메시지를 처리한 후
예시코드
공식문서의 예제코드를 보며 동작을 파악해보자.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
}
}
@Controller
public class GreetingController {
@MessageMapping("/greeting")
public String handle(String greeting) {
return "[" + getTimestamp() + ": " + greeting;
}
private String getTimestamp() {
return new SimpleDateFormat("MM/dd/yyyy h:mm:ss a").format(new Date());
}
}
- 클라이언트 연결
- 클라이언트는
localhost:8080/portfolio로 WebSocket연결을 생성한다. - 연결이 성립되면, STOMP 프레임(SUBSCRIBE, SEND 등)을 통해 클라이언트와 서버 간 메시지가 전송되기 시작.
- 클라이언트는
- 구독 요청(SUBSCRIBE)
- 클라이언트는 아래와 같이 STOMP 프레임 전송
SUBSCRIBE destination:/topic/greeting - 수신 및 디코딩 되면 메시지는
clientInboundChannel로 전달되고 - 메시지가 메시지 브로커로 라우팅되어 클라이언트의 구독 정보를 저장
- 클라이언트는 아래와 같이 STOMP 프레임 전송
- 메시지 전송(SEND)
- 클라이언트는 아래와 같이 STOMP 프레임 전송
SEND destination:/app/greeting /app접두사를 통해 메시지가 Spring 컨트롤러의@MessageMapping메서드로 라우팅/app접두사는 제거되고,/greeting부분이 GreetingController의@MessageMapping("/greeting")메서드로 매핑
- 클라이언트는 아래와 같이 STOMP 프레임 전송
- 컨트롤러에서 메시지 처리
- GreetingController의
@MessageMapping메서드는 클라이언트의 메시지를 처리하고 결과를 반환 - 처리 후 반환된 값은 Spring
Message로 변환되어brokerChannel을 통해 메시지 브로커로 전송됨
- GreetingController의
- 메시지 전송(MESSAGE)
- 메시지 브로커는 매칭된 모든 구독자에게 MESSAGE 프레임을 생성하여 전송
MESSAGE destination:/topic/greeting payload: "Hello, Client!" - 메시지는
clientOutboundChannel을 통해 WebSocket으로 전송 - 클라이언트는 WebSocket 연결을 통해 메시지 수신
- 메시지 브로커는 매칭된 모든 구독자에게 MESSAGE 프레임을 생성하여 전송
요약
- STOMP(Simple Text Oriented Messaging Protocol)는 WebSocket 위에서 메시지의 형식과 의미를 표준화하여 메시징 처리를 단순화함.
- Spring에서는 @EnableWebSocketMessageBroker를 통해 STOMP 지원을 활성화하며, 메시지를 컨트롤러(@MessageMapping)나 브로커로 라우팅 가능.
- 내장 브로커(Simple Broker)는 /topic(1:N)과 /queue(1:1) 경로를 통해 메시지를 구독자에게 전달하며, 외부 브로커(RabbitMQ, ActiveMQ 등)와도 통합 가능.
- STOMP 메시지는
clientInboundChannel→brokerChannel→clientOutboundChannel의 흐름으로 처리되며, WebSocket 클라이언트 간 실시간 통신을 지원. - Spring STOMP를 사용하면 WebSocket과 HTTP 기반 메시징을 통합하여 유연한 실시간 애플리케이션을 개발할 수 있음.
여기까지 STOMP와 메시지흐름에 대하여 살펴보았다.
웹 소켓 통신에 대한 핵심적인 내용은 거의다 정리한 것 같다. 그러나 공식문서에는
매우많은 섹션이 남아있다.
다음 챕터를 마지막으로 공식문서에서 필요할만한 내용만 부가적으로 정리하여 웹소켓에 대한 포스팅을 마치려고한다.
이후 직접 코드를 짜보면서 이해하는 시간을 가져야겠다.