Spring Boot와 WebSocket을 사용한 간단한 채팅 프로그램의 예시입니다. WebSocketConfig 클래스에서는 메시지 브로커와 STOMP 엔드포인트를 설정하고, ChatController 클래스에서는 메시지를 수신하여 다시 보내는 기능을 구현하고 있습니다.
STOMP (Simple Text Oriented Messaging Protocol)는 메시징 프로토콜 중 하나로, 주로 메시지 브로커와의 통신에 사용됩니다.
구체적인 구현 방법은 개발 환경에 따라 다릅니다. 자세한 내용은 Spring Boot WebSocket 문서를 참조하세요.
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-websocket'
WebSocketConfig
app (application): 메시지를 발행하는 역할을 하며, 특정 목적지(큐 또는 토픽)에 메시지를 전송합니다.
topic: 메시지를 구독하는 역할을 하며, 특정 토픽의 메시지를 수신합니다.
이렇게, 발행자(클라)와 구독자(서버)가 메시지를 주고받으며 비동기식 통신을 효율적으로 처리할 수 있습니다.
STOMP는 텍스트 기반이라 이해하기 쉽고 다양한 언어와 플랫폼에서 사용 가능하다는 장점이 있습니다.
@Configuration
@EnableWebSocketMessageBroker // WebSocket 메시지 브로커를 활성화합니다. -> STOMP 메시지를 사용할 수 있게 해줍니다.
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// STOMP 엔드포인트를 등록
registry.addEndpoint("/example").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 메시지 브로커를 구성합니다.
registry.setApplicationDestinationPrefixes("/app"); // 클라이언트->서버 메시지 프리픽스 (발행)
registry.enableSimpleBroker("/topic"); // 서버->클라이언트 메시지 프리픽스 (구독)
}
}
ChatMessage
@Data
public class ChatMessage {
private String content;
private String sender;
private MessageType type;
public enum MessageType {
CHAT, LEAVE, JOIN
}
}
ChatController
@Slf4j
@Controller
public class ChatController {
// stomp - 토픽 구독방식
@MessageMapping("/chat.register")
@SendTo("/topic/public")
public ChatMessage register(
@Payload ChatMessage chatMessage,
SimpMessageHeaderAccessor headerAccessor
) {
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
log.info("chat.register chatMessage : {}", chatMessage);
return chatMessage;
}
@MessageMapping("/chat.send")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
log.info("chat.send chatMessage : {}", chatMessage);
return chatMessage;
}
}
index.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>Spring Boot WebSocket Chat Application | CalliCoder</title>
<link rel="stylesheet" href="/css/main.css" />
</head>
<body background="spring_logo.png"
style="background-position: center; background-repeat: no-repeat; background-size: cover;">
<noscript>
<h2>Sorry! Your browser doesn't support Javascript</h2>
</noscript>
<div id="username-page">
<div class="username-page-container">
<h1 class="title">Type your username</h1>
<form id="usernameForm" name="usernameForm">
<div class="form-group">
<input type="text" id="name" placeholder="Username"
autocomplete="off" class="form-control" />
</div>
<div class="form-group">
<button type="submit" class="accent username-submit">Start
Chatting</button>
</div>
</form>
</div>
</div>
<div id="chat-page" class="hidden">
<div class="chat-container">
<div class="chat-header">
<h2>JavaTechie Global Chat Box</h2>
</div>
<div class="connecting">Connecting...</div>
<ul id="messageArea">
</ul>
<form id="messageForm" name="messageForm" nameForm="messageForm">
<div class="form-group">
<div class="input-group clearfix">
<input type="text" id="message" placeholder="Type a message..."
autocomplete="off" class="form-control" />
<button type="submit" class="primary">Send</button>
</div>
</div>
</form>
</div>
</div>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="/js/main.js"></script>
</body>
</html>
/js/main.js
'use strict';
var usernamePage = document.querySelector('#username-page');
var chatPage = document.querySelector('#chat-page');
var usernameForm = document.querySelector('#usernameForm');
var messageForm = document.querySelector('#messageForm');
var messageInput = document.querySelector('#message');
var messageArea = document.querySelector('#messageArea');
var connectingElement = document.querySelector('.connecting');
var stompClient = null;
var username = null;
var colors = [
'#2196F3', '#32c787', '#00BCD4', '#ff5652',
'#ffc107', '#ff85af', '#FF9800', '#39bbb0'
];
function connect(event) {
username = document.querySelector('#name').value.trim();
alert("username: "+ username);
if(username) {
usernamePage.classList.add('hidden');
chatPage.classList.remove('hidden');
var socket = new SockJS('/example');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, onError);
}
event.preventDefault();
}
function onConnected() {
// Subscribe to the Public Topic
stompClient.subscribe('/topic/public', onMessageReceived);
// Tell your username to the server
stompClient.send("/app/chat.register",
{},
JSON.stringify({sender: username, type: 'JOIN'})
)
connectingElement.classList.add('hidden');
}
function onError(error) {
connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!';
connectingElement.style.color = 'red';
}
function send(event) {
var messageContent = messageInput.value.trim();
alert("send: "+ messageContent);
if(messageContent && stompClient) {
var chatMessage = {
sender: username,
content: messageInput.value,
type: 'CHAT'
};
stompClient.send("/app/chat.send", {}, JSON.stringify(chatMessage));
messageInput.value = '';
}
event.preventDefault();
}
function onMessageReceived(payload) {
var message = JSON.parse(payload.body);
alert("onMessageReceived: "+ message);
var messageElement = document.createElement('li');
if(message.type === 'JOIN') {
messageElement.classList.add('event-message');
message.content = message.sender + ' joined!';
} else if (message.type === 'LEAVE') {
messageElement.classList.add('event-message');
message.content = message.sender + ' left!';
} else {
messageElement.classList.add('chat-message');
var avatarElement = document.createElement('i');
var avatarText = document.createTextNode(message.sender[0]);
avatarElement.appendChild(avatarText);
avatarElement.style['background-color'] = getAvatarColor(message.sender);
messageElement.appendChild(avatarElement);
var usernameElement = document.createElement('span');
var usernameText = document.createTextNode(message.sender);
usernameElement.appendChild(usernameText);
messageElement.appendChild(usernameElement);
}
var textElement = document.createElement('p');
var messageText = document.createTextNode(message.content);
textElement.appendChild(messageText);
messageElement.appendChild(textElement);
messageArea.appendChild(messageElement);
messageArea.scrollTop = messageArea.scrollHeight;
}
function getAvatarColor(messageSender) {
var hash = 0;
for (var i = 0; i < messageSender.length; i++) {
hash = 31 * hash + messageSender.charCodeAt(i);
}
var index = Math.abs(hash % colors.length);
return colors[index];
}
usernameForm.addEventListener('submit', connect, true)
messageForm.addEventListener('submit', send, true)