🥞 BE
home
💬

WebSocket으로 간단한 채팅 만들어보기

Date
2024/11/13
Category
Web
Tag
Basic
Web Socket
Detail
웹소켓을 공부하며, 이를 Spring Boot 환경에 적용시켜보고자, 간단한 채팅 서비스를 만들어 보려고 한다.
우선 의존성 추가는, spring web과 websocket을 추가해주었다.

WebSocketConfig

@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) { // enableSimpleBroker -> 특정 경로로 시작하는 메시지를 클라이언트에게 바로 전달할 수 있게 함. messageBrokerRegistry.enableSimpleBroker("/topic"); // setApplicationDestinationPrefixes -> 클라이언트가 전송하는 메시지를 해당 경로로 전송하게 하고, 처리할 핸들러에 연결. messageBrokerRegistry.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { // "/ws"로 WebSocket 엔드포인트를 설정하고, SockJS를 사용 stompEndpointRegistry.addEndpoint("/ws").withSockJS(); } }
Java
복사
WebSocket을 활성화하고, 엔드포인트를 설정하는 클래스를 만들어준다.
@EnableWebSocketMessageBroker 로 메시지 브로커를 활성화한다.
이후, MessageBrokerRegistry를 활용하는 메시지 브로커 설정과, StompEndpointRegistry를 활용해 SockJS 엔드포인트 설정을 하는 메서드를 만들어준다.
"/topic" 으로 시작하는 메시지를 클라이언트에게 바로 전달할 수 있게 하고, 클라이언트가 전송한 메시지를 "/app" 으로 시작하는 경로로 전송하여 처리할 핸들러에 연결한다.
웹소켓 연결은 "/ws" 엔드포인트를 통해 stomp로 수행한다.
이제 데이터 전달을 위한 모델관련 코드를 작성해보자.
롬복 Data 어노테이션을 써서, 채팅 메시지를 표현할 ChatMessage 클래스를 생성한다.
import lombok.Data; @Data public class ChatMessage { private MessageType type; private String content; private String sender; public enum MessageType { CHAT, JOIN, LEAVE } }
Java
복사
이제 클라이언트에서 어떤 엔드포인트로 메시지를 구독하고 브로드캐스팅할지에 대한 Controller를 정의한다.
@Controller public class ChatController { @MessageMapping("/chat.sendMessage") @SendTo("/topic/public") public ChatMessage sendMessage(ChatMessage chatMessage) { return chatMessage; } @MessageMapping("/chat.addUser") @SendTo("/topic/public") public ChatMessage addUser(ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { // 웹소켓 세션에 사용자 이름을 저장 headerAccessor.getSessionAttributes().put("username", chatMessage.getSender()); return chatMessage; } }
Java
복사
위 코드를 통해 클라이언트가 /app/chat.sendMessage로 보낸 메시지를 처리하고, /topic/public 구독자들에게 메시지를 전송한다.
addUser쪽은 세션 헤더에 Sender로 username넣어주는 방식으로 처리한다.
이정도면 서버 측에서의 웹소켓 설정은 모두 끝났다. 사실 웹소켓은 서버보다는 클라이언트쪽에서 엔드포인트 매핑해주는 부분이 더 어렵고 중요한 것 같다.
이를 위한 클라이언트쪽 스크립트를 작성.
<!-- src/main/resources/templates/chat.html --> <!DOCTYPE html> <html> <head> <title>WebSocket Chat</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> </head> <body> <div class="container"> <div id="chat"> <div class="row"> <div class="col-md-12"> <div id="conversation" class="panel panel-default" style="height: 400px; overflow-y: scroll;"> <div class="panel-body" id="messageArea"></div> </div> </div> </div> <div class="row"> <div class="col-md-12"> <input type="text" id="name" placeholder="이름을 입력하세요" class="form-control" /> <br /> <input type="text" id="message" placeholder="메시지를 입력하세요" class="form-control" /> <br /> <button id="send" class="btn btn-primary">보내기</button> </div> </div> </div> </div> <!-- STOMP and SockJS 라이브러리 로드 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.1/sockjs.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> <script> var stompClient = null; function connect() { var socket = new SockJS('/ws'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/public', function (message) { showMessage(JSON.parse(message.body)); }); // 사용자 이름 설정 var name = document.getElementById('name').value.trim(); if (name !== "") { stompClient.send("/app/chat.addUser", {}, JSON.stringify({ sender: name, type: 'JOIN' })); } }); } function sendMessage() { var messageInput = document.getElementById('message'); var nameInput = document.getElementById('name'); var messageContent = messageInput.value.trim(); var sender = nameInput.value.trim(); if (messageContent && sender) { var chatMessage = { sender: sender, content: messageContent, type: 'CHAT' }; stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage)); messageInput.value = ""; } } function showMessage(message) { var messageArea = document.getElementById('messageArea'); var messageElement = document.createElement('p'); if (message.type === 'JOIN') { messageElement.innerHTML = '<i>' + message.sender + '님이 참여하셨습니다.</i>'; } else if (message.type === 'LEAVE') { messageElement.innerHTML = '<i>' + message.sender + '님이 퇴장하셨습니다.</i>'; } else { messageElement.innerHTML = '<b>' + message.sender + ':</b> ' + message.content; } messageArea.appendChild(messageElement); messageArea.scrollTop = messageArea.scrollHeight; } document.getElementById('send').addEventListener('click', function (e) { e.preventDefault(); sendMessage(); }); // 엔터 키로 메시지 전송 document.getElementById('message').addEventListener('keypress', function (e) { if (e.key === 'Enter') { sendMessage(); } }); // 페이지 로드 시 연결 window.onload = connect; </script> </body> </html>
JavaScript
복사
function 이름을 보면 보이겠지만, 소켓 관련 부분은 connect(), sendMessage(), showMessage() 로 구성되어 있다.
connect로 소켓 열어주면서 JOIN 타입의 메시지를 addUser 이벤트를 구독하며 전송해준다. sendMessage로 CHAT 타입의 메시지와 함께 데이터를 전송하고, showMessage는 참여, 퇴장여부, 메시지를 텍스트로 보여주는 기능을 수행한다.
@Controller public class PageController { @GetMapping("/chat") public String chat() { return "chat"; } }
JavaScript
복사
chat.html 반환하도록 페이지 컨트롤러 만들어주고, localhost:8080/chat 접속해서 실행 테스트

정리

STOMP 프로토콜을 기반으로 엔드포인트에 따라 메시지를 구독, 구독한 다수의 채널에 대해 메시지를 브로드캐스트 하는게 웹소켓의 기본 통신 방식이다. 이 채널이라는게 끊기지 않고 계속 지속되며 실시간 통신을 보장해주는 것.
결국 http 통신과 api로 데이터 주고받는건 똑같다. 단지 통신의 방법만 바뀌었을 뿐.
스프링에서 웹소켓 관련 인터페이스와 어노테이션을 많이 지원해서 확실히 쉽게 개발이 가능한 것 같다.