학교 프로젝트로 웹소켓과 STOMP 기반의 채팅 및 실시간 웹 서비스를 개발하고, 하면서 여러 문제를 겪고 해결했던 경험을 정리해보고자 합니다.
해당 포스트는 Spring Boot와 React(TypeScript)를 통해 예제를 구현할 것이기 때문에 원활한 이해를 위해서는 둘에 대한 사전 지식을 필요로 합니다. 하지만 가능한 한 작은 사이즈로 필요한 부분만 구현했고, 전체 코드를 포함하고 있으므로 모르는 사람도 따라할 수 있을 것입니다.
완성된 예제는 아래 깃헙 레포지토리에서 확인 가능합니다.
https://github.com/gyehyun-bak/spring-react-websocket
🙇♂️ 아직 배우는 입장에서 제작된 포스트이므로 잘못된 정보나 부족한 부분, 개선 사항, 궁금한 점 등 얼마든지 남겨주세요!
✏️ 웹소켓(WebSocket)은 실시간 양방향 통신을 위한 어플리케이션 계층 통신 프로토콜입니다.
만약 해당 포스트에 오시기 전에 웹소켓에 대해 검색해보셨다면 아마 HTTP Polling과 Long-Polling에 대한 많은 블로그 글과 만나고 오셨을 것입니다. 실제로 WebSocket 프로토콜의 탄생 배경은 이러한 기존 HTTP를 이용한 실시간 서비스, 즉, 데이터 변경이 빠르고, 그걸 유저에게 전달하며, 유저로부터 지속적인 요청을 수신해야 하는 서비스를 구현하기 위함입니다. 또한 이러한 구현을 위해서 기존에 HTTP를 이용하는 방식에서 발생하는 오버헤드를 해결하기 위해서입니다.
출처: https://ably.com/topic/websockets-vs-http
HTTP는 어플리케이션 계층 통신 프로토콜입니다. HTTP는 TCP와 같은 reliable한(신뢰성 있는) Transport(전송) 계층 프로토콜 위에서 동작하도록 구현되어 있습니다. ”요청”에 대한 “응답”을 받는 것을 목적으로 하는 request/response 프로토콜로, 전송 계층 연결이 확보되었다면 요청을 주고, 응답을 받고 나면 연결을 끊습니다. 하나의 HTTP 요청을 위해 TCP 연결을 먼저 하고, HTTP 요청을 보내고, 응답을 받고 나면 TCP 연결을 해제하는 것입니다. 네트워크 계층 구조에 대해 익숙하지 않은 분들을 위해 비유하자면, 전화로 어디에 주문을 한다고 가정해보겠습니다. 여기서 전화 연결이 TCP, 전화 연결 후 “ㅇㅇ 하나 주세요.” 하는 주문이 HTTP입니다.
HTTP Polling은 앞서 말한 것처럼 주기적으로 HTTP 요청을 보내서 이벤트를 확인하는 방식입니다. 앞선 예를 통해 설명하자면, 피자 가게에 전화를 해서 피자를 한 판 주문한 다음 몇 초마다 전화를 다시 걸어서 피자가 다 됐는지 물어보고 대답을 들은 다음 전화를 끊는 것을 반복하는 것입니다.
출처: https://ably.com/topic/websockets-vs-http
HTTP Long Polling은 기존 폴링 방식보다 이벤트를 좀 더 길게 기다리는 방식입니다. 응답 데이터가 바로 준비되지 않는 경우 폴링보다 서버 부담이 감소하며 이벤트에 대한 응답 속도가 증가하지만, 이벤트 발생 시간 간격이 좁다면 일반 폴링과 별 차이가 없습니다.
출처: https://ably.com/topic/websockets-vs-http
HTTP Streaming(Server Sent Event, SSE의 한 구현)는 요청에 대한 응답을 완료하지 않는 방식입니다. 폴링과 롱폴링이 커넥션이 자꾸 닫혀서 발생하는 오버헤드를 해결하기 위해 등장하였습니다. 문제는 한 번에 한쪽에서만 보낼 수 있다는 것입니다.
위 셋 다 기존 HTTP 서비스 위에 도입이 간단하고, 따로 웹소켓 로직을 서비스에 추가하여 복잡도를 추가하지 않아도 되며, 어디서나 잘 동작하므로 경우에 따라서는 좋은 선택입니다. 특히 데이터 업데이트가 규칙적이거나, 실시간의 정도가 그리 중요하지 않거나, 서비스 사용 중 계속해서 연결을 유지할 필요가 없거나, 어느 한 쪽의 요청은 자주 일어나지 않는 서비스의 경우 좋다고 생각합니다.
채팅 혹은 웹게임과 같은 실시간 서비스의 문제점은 서버에서 클라이언트로 전달하는 이벤트가 언제 발생할지 모르며, 항상 유저와 서버가 양방향 통신이 가능해야 한다는 점입니다. 이러한 구현을 위해 하염없이 폴링을 하고 있는 등의 방식은 상당히 비효율적일 것입니다.
💡 The wire protocol has a high overhead, with each client-to-server message having an HTTP header.
WebSocket 프로토콜 문서에서는 웹소켓 등장 배경으로써 wire protocol(HTTP와 같은 어플리케이션 레벨 프로토콜)이 오버헤드가 크다고 설명하고 있습니다. 실제로 HTTP는 가변 길이 헤더를 가지며 작으면 200byte 에서 크면 수십 KB이상(Tomcat 서버는 기본 최대 48KB)으로도 커집니다. 웹소켓 헤더의 크기는 최대 16byte입니다. 이는 10~1000배 가까운 차이입니다.
웹소켓(WebSocket)은 기존의 HTTP 인프라를 그대로 이용해서 실시간 양방향 통신을 구현할 수 있도록 하기 위해 설계된 프로토콜입니다. 웹소켓은 한 번의 HTTP 연결을 통해 생성된 TCP 연결을 끊지 않고 계속 이용하겠다는 방식입니다. 즉, 전화를 걸고 끊지 않고 계속 들고 있는 방식입니다.
HTTP를 이용한 핸드쉐이크 과정을 통해 웹소켓 프로토콜로 변경하고, 처음 요청에 연결한 TCP 연결을 누군가 한 쪽이 끊을 때까지 계속해서 사용하는 방식입니다.
이를 통해 한 번의 HTTP 헤더 전송 이후에는 크기가 상대적으로 작은 웹소켓 헤더와 프레임 단위 메시지를 통해 통신하게 되므로 실시간 서비스에서는 앞선 방식들보다 훨씬 효율적인 통신이 가능합니다.
✏️ STOMP(Simple Text Orientated Messaging Protocol)이란 메시지브로커와 연결하기 위해 만들어져, 웹소켓 등에 적용할 수 있는 서브프로토콜(sub-protocol)입니다.
웹소켓은 텍스트 혹은 바이너리 기반 메시지라는 사실 외에 메시지 페이로드(내용물)에 대한 정해진 구조가 없습니다. 그렇기 때문에 보다 일관적이고 확장적인 서비스 개발을 위해서는 그 형식을 정해야할 것입니다. STOMP는 텍스트 혹은 바이너리 메시지 페이로드의 형식을 정의합니다. 특히 최초에 메시지브로커에 연결할 목적으로 설계되었기 때문에, pub/sub의 형태를 가지고 있으며, 메시지브로커 미들웨어와 호환성이 좋습니다.
COMMAND
header1:value1
header2:value2
Body^@
위는 STOMP 메시지의 형식입니다. 메시지의 종류를 나타내는 COMMAND
와 필수 header
값들이 있고, 메시지 내용을 포함할 수 있는 BODY
와 내용의 끝을 나타내는 ^@
(Null 기호)가 포함되어 있습니다.
SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@
destination
토픽으로 메시지를 전송하는 STOMP 메시지의 예시입니다. 자바스크립스 상에서 디버깅 옵션을 켜면, 이러한 메시지 내부를 볼 수 있습니다. 위 예시에서처럼 STOMP 메시지 바디에 다시 JSON 객체를 PlainText로 포함하여 전송하고 이를 서버측에서 매핑하여 JSON 객체를 주고받을 수 있습니다.
출처: https://docs.spring.io/spring-framework/reference/web/websocket/stomp/message-flow.html
당장 이 그림을 보시면 감이 안 오실 수도 있습니다만, 실제 코드를 작성하시고 나면 아마 와닿으실 겁니다.
앞서 STOMP가 Pub/Sub 구조를 가지고 있다고 언급했습니다. 외부 메시지브로커를 이용하지 않으면 스프링에 내장된 SimpleBroker를 합니다. STOMP는 프로토콜이며, SimpleBroker는 해당 프로토콜에 따라 Pub/Sub 구조의 메시지브로커 기능을 제공하는 서비스입니다.
Pub/Sub, Publish/Subscribe는 이름 그대로 구독과 발행으로 메시지를 전달하는 방식입니다. 유튜브 채널 혹은 이메일 서비스를 구독하듯이 특정 토픽(Topic)에 클라이언트가 구독을 하게 됩니다. 그러면 서버 혹은 클라이언트가 해당 토픽으로 메시지를 푸시하는 경우, 토픽을 구독하는 모든 클라이언트에게 메시지를 브로드캐스팅(전달)하게 됩니다.
STOMP와 스프링을 함께 사용하면, 어느 클라이언트가 어느 메시지를 받아야 하는지, 한 번에 전달하기 위해서는 어떻게 해야하는지 등을 따로 구현하지 않고 편리하게 처리할 수 있습니다. 특히나, 일반 웹소켓만 사용하는 경우 다양한 종류의 클라이언트 측 요청을 WebSocketHandler에서 모두 처리해야 합니다. 또한 그 종류 자체도 Enum 등으로 따로 관리를 해야할 수 있습니다. 웹소켓으로 처리할 서비스가 많아지면 이는 확장에 아주 불편해지기 쉽습니다. STOMP를 사용하는 경우 스프링이 제공하는 SimpAnnotationMethod
을 이용해 기존의 일반 HTTP 요청처럼 비즈니스 로직별(엔드포인트별)로 Controller를 작성해 메시지 처리를 관리할 수 있습니다.
클라이언트 측은 "/topic"
엔드포인트를 이용하는 경우 서버를 거치지 않고 바로 메시지를 해당 토픽에 브로드캐스팅하게 되며, "/app"
엔드포인트에 메시지를 Publish하는 경우 @MessageMapping
어노테이션이 붙은 컨트롤러 메서드를 이용해 메시지를 서버측에서 받아 처리할 수 있게 됩니다.
@Controller
인스턴스로 구성할 수 있으며, 특정 연결에 대해 단일 WebSocketHandler
를 사용해 원시 WebSocket 메시지를 처리하는 대신 STOMP 대상 헤더를 기반으로 메시지를 라우팅할 수 있습니다.💡 Pub/Sub 구조와 메시지브로커에 대해서는 당장 깊이 있게 다루지 못하지만 추가적으로 공부해보신다면 위 동작을 이해하는데 많은 도움이 될 것입니다.
✏️ SockJS는 WebSocket API를 모방하도록 설계된 오픈 소스 실시간 메시징 라이브러리입니다.
SockJS는 클라이언트와 서버 측 모두 제공되는 라이브러리로, 브라우저가 WebSocket 프로토콜을 지원하지 않을 때, 자동으로 롱 폴링이나, HTTP 스트리밍으로 전환되어 양방향 실시간 통신이 가능하도록 해줍니다. 비슷한 라이브러리로 Socket.IO가 있습니다.
STOMP를 통해 통신하도록 해주는 클라이언트측 라이브러리인 StompJS 공식문서를 보면 “SockJS를 적용하는 예제가 인터넷 상 많이 나와 있지만 SockJS가 필요할 일은 실제로(따로 필요하다는 확신이 있는 게 아닌 이상) 거의 없다.”라고 나와있습니다. 저도 공식 문서를 믿고 최초에 SockJS를 사용하지 않고 프로젝트(2024년 기준)를 진행했습니다. 그러나 아주 가끔씩 모바일 웹브라우저 등에서 알 수 없는 연결 문제를 겪은 경험이 있습니다. 조사한 결과 웹소켓 지원이 제대로 안 되서 문제가 생기는 경우가 아직도 종종 있다는 글을 볼 수 있었습니다(확실히 해당 문제인지는 확인하지 못했습니다;;).
뿐만 아니라, SockJS는 프로토콜 대체 말고도 연결 상태 체크(Server-Sent Heartbeats) 등의 추가 기능을 지원합니다. 이는 확실하게 확인을 한 부분인데, nginx나 AWS Load Balancer 등을 통해 프록시 기능을 사용하는 경우, 로드벨런서 차원에서 지속되지 않는 연결을 자동으로 끊어버립니다. 이를 방지하기 위해, 기본 STOMP만 사용하는 경우 자체적으로 일정 시간마다 핑퐁을 주고 받는다거나 하는 기능을 추가로 구현해주어야 하지만, SockJS는 이러한 연결 상태 체크 기능이 기본적으로 포함(기본 25초) 되어 있습니다.
Since Spring’s SockJS Service supports server-sent heartbeats (every 25 seconds by default), that means a client disconnect is usually detected within that time period (or earlier, if messages are sent more frequently).
https://stomp-js.github.io/guide/stompjs/rx-stomp/using-stomp-with-sockjs.html
SockJS는 개발자로 하여금 이러한 부분들에 신경 쓰지 않도록 해주는 장점을 가지고 있기 때문에 해당 예제에서는 SockJS를 사용하도록 하겠습니다.
다음은 클라이언트와 서버 사이 웹소켓을 통한 메시지 통신의 흐름을 표현한 그림입니다.
가장 단순하게 웹소켓을 연결하고, STOMP를 통해 단일 서버 엔드포인트("/app/chat")로 메시지를 보내고, 해당 메시지를 서버가 SimpleBroker로 퍼블리시("/topic/chat")하고, 해당 토픽을 구독 중이던 클라이언트가 브로드캐스팅 된 메시지를 받는 형식입니다.
아래 링크들을 참조하여 vite을 이용해 React(TypeScript) 프로젝트를 생성하고 필요한 의존성을 설치해줍니다.
https://tailwindcss.com/docs/guides/vite
https://www.npmjs.com/package/sockjs-client
// vite.config.ts
export default defineConfig({
plugins: [react()],
define: {
global: "window",
},
});
global is not defined
에러가 나는 경우 위 코드를 추가합니다.https://www.npmjs.com/package/@types/sockjs-client
https://www.npmjs.com/package/@stomp/stompjs
아래 링크 혹은 IDE를 통해
를 의존성으로 추가하여 새로운 프로젝트를 생성합니다.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}
스프링부트에서 메시지브로커를 활성화시키기 위해 WebSocketMessageBrokerConfigurer
를 구현한 Config 클래스를 작성하고 위 메서드 두 개를 구현합니다.
registerStompEndpoint
에 들어가는 엔드포인트는 서버와 웹소켓 연결을 위한 엔드포인트 입니다. 클라이언트 측에서 SockJS 객체를 만들때, 해당 엔드포인트를 사용합니다.
configureMessageBroker
에 enableSimpleBroker
와 setApplicationDestinationPrefixes
가 있습니다.
enableSimpleBroker
에 인자값으로 들어가는 엔드포인트는 스프링 내장 메시지 브로커인 SimpleBroker가 해당 토픽에 메시지를 브로드캐스팅하도록 하기 위한 엔드포인트입니다. 즉, "/topic/..."으로 푸시되는 메시지는 해당 엔드포인트를 구독하고 있는 클라이언트 모두에게 해당 메시지가 전달됩니다.
setApplicationDestinationPrefixes
는 스프링 서버가 메시지를 먼저 받는 주소입니다. 예를 들어 클라이언트가 "/app/chat"으로 메시지를 보낸다면, 해당 메시지는 브로커로 가기 전에 저희가 작성한 ChatController의 @MessageMapping("/chat")
어노테이션이 붙어있는 메서드로 먼저 들어옵니다. 해당 메시지 처리를 서버가 결정할 수 있습니다.
@Controller
@RequiredArgsConstructor
public class ChatController {
private final MessageService messageService;
@MessageMapping("/chat")
@SendTo("/topic/chat")
public MessageResponseDto sendChatMessage(MessageRequestDto requestDto) {
return messageService.processMessage(requestDto);
}
}
앞서 예로 든 ChatController입니다. 스프링웹소켓은 다양한 어노테이션을 제공합니다. 그 중 @MessageMapping
과 @SendTo
가 있습니다.
@MessageMapping
어노테이션이 붙은 메서드는 해당 엔드포인트로 푸시된 메시지를 받을 수 있습니다. 여기서 WebSocketConfig에서 작성한 prefix인 "/app"
은 생략되었습니다.
@SendTo
어노테이션은 해당하는 토픽 엔드포인트로 메서드의 리턴값을 메시지로서 푸시합니다. 푸시된 메시지는 SimpleBroker를 거쳐 해당 토픽을 구독한 사용자에게 브로드캐스팅됩니다.
STOMP를 사용하면 이와 같은 어노테이션을 통해 기존 HTTP와 같이 웹소켓의 동작을 비즈니스 로직 별 엔드포인트와 컨트롤러 클래스로 구별할 수 있습니다. 스프링 서버가 클라이언트와 메시지 브로커 사이 필터링 역할을 하고 있다고 생각하시면 좋을 것 같습니다.
public interface MessageService {
MessageResponseDto processMessage(MessageRequestDto requestDto);
}
@Service
public class MessageServiceImpl implements MessageService {
public MessageResponseDto processMessage(MessageRequestDto requestDto) {
return new MessageResponseDto(requestDto.getContent());
}
}
클라이언트에게 받은 메시지를 처리하는 Service 클래스입니다. 현재는 채팅 메시지의 요청과 응답 형태가 같기 때문에 단순히 변환만 해주고 있지만, 이후 변화가 생긴다면 이렇게 서비스 측에서 관리해주는 것이 편리합니다.
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class MessageRequestDto {
private String content;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class MessageResponseDto {
private String content;
}
import { useEffect, useRef, useState } from "react";
import { Client } from "@stomp/stompjs";
import SockJS from "sockjs-client";
// 메시지 Dto
interface MessageRequestDto {
content: string;
}
interface MessageResonseDto {
content: string;
}
// 서버 웹소켓 엔드포인트트
const SOCKET_URL = "http://localhost:8080/ws";
function App() {
const [message, setMessage] = useState("");
const [messages, setMessages] = useState<MessageResonseDto[]>([]);
const stompClientRef = useRef<Client | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
const socket = new SockJS(SOCKET_URL);
const stompClient = new Client({
webSocketFactory: () => socket as any,
debug: (msg: string) => console.log("[STOMP]:", msg),
onConnect: () => {
console.log("[STOMP] 연결 성공: ", stompClient);
// 채팅 토픽 구독
const callback = (message: any) => {
if (message.body) {
console.log("[STOMP] 메시지 수신: ", message.body);
const newMessage: MessageResonseDto = JSON.parse(message.body);
setMessages((prevMessages) => [...prevMessages, newMessage]);
}
};
stompClient.subscribe("/topic/chat", callback);
},
onStompError: (e) => {
console.error("[STOMP] 연결 실패: ", e);
stompClient.deactivate();
},
onDisconnect: () => console.log("STOMP 연결 해제"),
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
stompClient.activate();
stompClientRef.current = stompClient;
return () => {
stompClient.deactivate();
};
}, []);
const sendMessage = () => {
if (
!message.trim() ||
!stompClientRef.current ||
!stompClientRef.current.connected
)
return;
const messageDto: MessageRequestDto = {
content: message,
};
stompClientRef.current.publish({
destination: "/app/chat",
body: JSON.stringify(messageDto),
});
setMessage("");
inputRef.current?.focus();
};
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
sendMessage();
}
};
return (
<div className="flex justify-center w-screen h-screen">
<div className="flex flex-col max-w-screen-sm w-full h-full bg-neutral-50">
{/* Header */}
<div className="p-4 font-bold text-xl bg-neutral-200 flex justify-center">
Simple Chat Example
</div>
{/* Body */}
<div className="flex-1 overflow-auto p-4">
<div className="flex flex-col gap-1">
{messages.map((message, index) => (
<div
key={index}
className="p-2 my-1 rounded-lg w-fit bg-white shadow-md"
>
{message.content}
</div>
))}
</div>
</div>
{/* Input */}
<div className="p-4 bg-neutral-200 flex items-center w-full">
<input
ref={inputRef}
type="text"
className="flex-1 p-3 rounded-lg mr-2"
placeholder="메시지를 입력하세요..."
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyPress}
/>
<button
className="p-3 bg-neutral-900 text-white rounded-lg"
onClick={sendMessage}
>
전송
</button>
</div>
</div>
</div>
);
}
export default App;
단순하게 표현하기 위해 App.tsx 하나의 파일로 코드를 구현했습니다. 위에서 눈여겨 보실 부분은 아래와 같습니다.
// 서버 웹소켓 엔드포인트트
const SOCKET_URL = "http://localhost:8080/ws";
WebSocketConfig에서 작성한 웹소켓 연결을 위한 엔드포인트입니다.
useEffect(() => {
const socket = new SockJS(SOCKET_URL);
const stompClient = new Client({
webSocketFactory: () => socket as any,
debug: (msg: string) => console.log("[STOMP]:", msg),
onConnect: () => {
console.log("[STOMP] 연결 성공: ", stompClient);
// 채팅 토픽 구독
const callback = (message: any) => {
if (message.body) {
console.log("[STOMP] 메시지 수신: ", message.body);
const newMessage: MessageResonseDto = JSON.parse(message.body);
setMessages((prevMessages) => [...prevMessages, newMessage]);
}
};
stompClient.subscribe("/topic/chat", callback);
},
onStompError: (e) => {
console.error("[STOMP] 연결 실패: ", e);
stompClient.deactivate();
},
onDisconnect: () => console.log("STOMP 연결 해제"),
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
stompClient.activate();
stompClientRef.current = stompClient;
return () => {
stompClient.deactivate();
};
}, []);
useEffect
를 통해 웹사이트 접속 시, SockJS 클라이언트를 연결하고 또한 Stomp 클라이언트를 통해 STOMP 서브프로토콜을 사용한 연결을 진행하고 있습니다.
stompjs
는 STOMP를 사용하는 클라이언트측에 다양한 기능을 제공합니다.
debug
옵션을 통해 오고가는 메시지 형태를 볼 수 있습니다.onConnect
옵션을 통해 STOMP 연결이 성공하고 나면 실행할 callback 함수를 지정할 수 있습니다. 여기서는 단일 메시지 토픽인 "/topic/chat"을 구독합니다. 구독할 때, 해당 토픽으로 브로드캐스팅 되는 메시지를 어떻게 처리할지 callback 함수를 마찬가지로 지정해줍니다. 여기서는 단순히 useState
를 사용하는 messages
에 새로운 메시지를 추가합니다.onStompError
로 연결 오류에 대한 경우를 처리할 수 있습니다.onDisconnect
로 연결 해제되는 경우를 처리할 수 있습니다.reconnectDelay
를 설정하면 연결 오류 시 재연결 시도 간격을 설정할 수 있습니다. 해당 옵션과 나머지 옵션 모두 현재 모두 공식 문서에서 제공하는 기본값입니다.stompClient
객체를 useRef
를 이용해 저장합니다. 이를 통해 메시지를 퍼블리시할 것입니다.
const sendMessage = () => {
if (
!message.trim() ||
!stompClientRef.current ||
!stompClientRef.current.connected
)
return;
const messageDto: MessageRequestDto = {
content: message,
};
stompClientRef.current.publish({
destination: "/app/chat",
body: JSON.stringify(messageDto),
});
setMessage("");
inputRef.current?.focus();
};
메시지를 서버로 보내는 함수입니다. 이전에 언급했다시피 서버로 메시지를 우선 전달할 것이기 때문에 destination은 "/app/chat"
으로 되어있는 것을 볼 수 있습니다. 만약 여기서 "/topic/chat"
으로 수정하시면 해당 메시지는 서버의 컨트롤러를 거치지 않고 바로 메시지 브로커에 의해 브로드캐스팅 됩니다.
다른 추가적인 기능 없이 웹사이트에 접속한 클라이언트끼리 순수하게 메시지만 주고 받을 수 있는 서비스가 완성되었습니다.
이 상태로는 누가 메시지를 보냈는지 알 수 없어 불편할 것입니다. 그러므로 이 다음에는 WebSocket EventListener
와 HandshakeInterceptor
, 커스텀 헤더 등을 이용하여 사용자 이름을 추가하고, 연결된 세션 정보를 인메모리 컬렉션으로 관리하도록 구현해보겠습니다.