지난 글(WebSocket)에 이어서 STOMP에 대해 알아보자.
1. STOMP
STOMP(Simple/Streaming Text Oriented Messaging Protocol)란 클라이언트와 서버 간의 메시지 전송을 위한 텍스트 기반 프로토콜이다.
이 프로토콜은 Publish-Subscribe 구조를 기반으로 하며, Spring의 내장 브로커인 SimpleBroker나 외부 메시지 브로커(RabbitMQ, Redis, Kafka 등)를 활용하여 발신자(Publisher)와 수신자(Subscriber) 간의 메시지를 쉽게 주고받을 수 있다.
STOMP는 메시징 프로토콜이며, 발행자(Publisher)가 메시지를 생성하면 브로커가 이를 관리하고 구독자(Subscriber)에게 전달한다.
2. STOMP 구조
이전 글에서 WebSocket을 알아보며, 메세지 형식에 대한 규격이 없어 개발자가 직접 정의해야 하는 문제점들을 언급했다. STOMP를 사용하면 형식을 따로 고민할 필요도, 파싱하기 위한 코드를 구현할 필요도 없다.
STOMP는 프레임(frame)이라고 해서 Command, header, body로 이미 형식을 정의해 두었다.
frame
COMMAND
header1:value1
header2:value2
Body
- Command는 메세지 유형을 나타내며 CONNECT, SEND, SUBSCRIBE 등이 있다.
- Header은 메시지에 대한 부가 정보를 key-value 형태로 제공한다. 대표적으로 'destination' 헤더는 메시지의 목적지를 지정한다.
- Body에 실제 전송할 데이터 내용을 포함하게 된다.
3. STOMP 통신 흐름
STOMP 통신 흐름은 발신자, 서버, 브로커, 구독자 간의 상호작용을 통해 이루어진다.
STOMP 프로토콜에서 /topic
과 /app
접두사는 다음과 같은 의미를 가진다.
- /topic : 이 접두사를 사용하면 메시지가 직접 메시지 브로커로 전달되어 해당 토픽을 구독하고 있는 모든 클라이언트에게 즉시 발행된다.
- /app : 이 접두사는 애플리케이션의
@MessageMapping
메서드로 메시지를 라우팅한다. 이 메서드에서 메시지를 처리한 후, 개발자가 명시적으로SimpMessagingTemplate
을 사용하여/topic
으로 메시지를 전송해야 구독자들에게 발행이 된다.
따라서, /app으로 들어온 메시지는 서버에서 추가 처리(데이터베이스 저장, 메시지 변환, 인증 등)를 할 수 있는 기회를 제공하며, 이 처리 후에 개발자가 명시적으로 /topic으로 메시지를 전송해야 구독자들에게 발행된다.
이러한 구조는 단순히 메시지를 전달하는 경우와 서버에서 추가 로직이 필요한 경우를 유연하게 처리할 수 있게 해준다.
4. Spring STOMP
이전에 WebSocket을 구현해 볼 때 사용했던 WebSocket King Client의 경우 Command, header, body로 이루어진 STOMP의 프레임 타입을 지원하지 않는다. 따라서 STOMP 테스트를 만들어준 블로그를 이용하여 구현해 보자.
4.1 환경설정
- STOMP TESTER
- Spring Boot 2.7.18
- Java 11
WebSocket과 STOMP를 Gradle에 의존성 추가
gradle
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:stomp-websocket:2.3.3'
4.2 Config
java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 클라이언트가 구독할 prefix (예: /topic)
registry.enableSimpleBroker("/topic");
// 클라이언트가 메시지를 보낼 때 사용할 prefix (예: /app)
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*");
}
}
STOMP를 사용하면 더 이상 SocketTextHandler
와 같은 저수준 WebSocket 핸들러를 직접 구현할 필요가 없다.
STOMP는 메시지 라우팅, 구독 관리, 메시지 형식 등을 추상화하여 제공하므로, 개발자는 비즈니스 로직에 더 집중할 수 있다.
대신, @MessageMapping
어노테이션을 사용하여 특정 목적지로 오는 메시지를 처리하는 메서드를 정의하게 된다.
WebSocket 때와 마찬가지로 Tester에서도 .withSockJs() 를 통해 SockJs를 활성화할 수 있지만 오류가 날 수 있으니 제거하고 실습하자.
4.3 Controller
java
@Controller
@RequiredArgsConstructor
public class ChatController {
private final SimpMessagingTemplate messagingTemplate;
@MessageMapping("/chat/enter")
public void enterChatRoom(@RequestBody ChatDto chatDto, SimpMessageHeaderAccessor headerAccessor) {
Map<String, Object> sessionAttributes = headerAccessor.getSessionAttributes();
sessionAttributes.put("id", chatDto.getId());
sessionAttributes.put("roomCode", chatDto.getRoomCode());
String joinMessage = chatDto.getId() + "님이 입장하셨습니다.";
messagingTemplate.convertAndSend("/topic/chat/room/" + chatDto.getRoomCode(), joinMessage);
}
@MessageMapping("/chat/send")
public void sendMessage(@RequestBody ChatDto chatDto) {
String msg = chatDto.getId() + ": " + chatDto.getMessage();
messagingTemplate.convertAndSend("/topic/chat/room/" + chatDto.getRoomCode(), msg);
}
}
@MessageMapping
어노테이션이 붙은 메서드는 WebSocketConfig에서 설정한 applicationDestinationPrefixes ("/app")와 결합된다. 따라서 클라이언트에서 "/app/chat/enter"로 보낸 메시지가 이 메서드로 라우팅 된다.
enterChatRoom
메서드는 사용자의 채팅방 입장을 처리한다. 세션 속성에 사용자 ID와 roomCode를 저장하고 입장 메시지를 생성한다. 그 후, messagingTemplate을 사용하여 "/topic/chat/room/{roomCode}" 목적지로 메시지를 전송하며 해당 채팅방을 구독 중인 모든 클라이언트에게 전달된다.
sendMessage
메서드에서 메세지를 가공하거나 DB에 저장하는 로직을 추가할 수 있다. DB 연결은 생략하고 메세지만 다음과 같이 가공하여 보내보자. String msg = chatDto.getId() + ": " + chatDto.getMessage();
4.4 Dto
java
@Data
public class ChatDto {
private String id;
private String roomCode;
private String message;
}
DB가 없으니 Entity는 생략하고 Dto만 간단히 만들어보자.
4.5 SpringBoot + STOMP TESTER
먼저 STOMP TESTER 에 접속 후 창을 2개 켜서 같은 채팅방에 구독하고, 입장 해보자
DESTINATION PATH: /app/chat/enter
MESSAGE(JSON): {"id": "User1", "roomCode": "ROOM123"}
위와 같이 /topic/chat/room/ROOM123으로 구독한 후
유저의 정보와 채팅방 정보를 담은 메세지를 보내게 되면 채팅방 입장 메세지가 구독한 유저에게 모두 보이게 된다.
이후로는 아래와 같이 JSON 객체에 message 변수를 추가하여 메세지를 보낼 수 있다.
DESTINATION PATH: /app/chat/send
MESSAGE(JSON): {"id": "User1", "roomCode": "ROOM123", "message":"Hello"}
마치며,
WebSocket이 단순히 클라이언트-서버 간 1:1 통신을 제공하는 반면, STOMP를 사용하면서 메시지 브로커를 통한 중앙 집중식 구조를 가질 수 있게 되었다.
이로 인해 메시지의 관리와 분배가 더 체계적으로 이루어질 수 있으며, 개발자는 복잡한 메시징 로직 구현에 집중하기보다는 비즈니스 로직에 더 집중할 수 있다.
'Spring' 카테고리의 다른 글
[Spring] Socket, WebSocket이란? (Spring Boot + WebSocket King Client로 구현해보기) (0) | 2025.02.20 |
---|---|
[Spring] Vue.js + Spring Boot 개발환경구축 (0) | 2025.02.17 |
[Spring] JPA, ORM, Hibernate, JDBC 총정리 (0) | 2025.02.05 |
[Spring] Spring MVC에서 Apache Tiles 사용법 (0) | 2025.01.10 |