웹 소켓은 기본 스펙이 아니기 때문에 해당 기능을 이용하려면 의존성을 필수적으로 추가해야 한다.
Maven 빌드의 경우
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
Gradle 빌드의 경우
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}
WebSocketConfig 클래스를 설정하여 WebSocket의 설정을 정의해야 한다. 여기에는 @EnableWebSocketMessageBroker 어노테이션을 사용하여 메시지 브로커를 활성화하고, configureMessageBroker() 메소드를 사용하여 메시지 브로커의 프리픽스를 설정한다.
package com.practice.SampleChat.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
/*
WebSocketConfigurer 인터페이스를 구현하고 registerWebSocketHandlers() 메서드를 오버라이드하여
WebSocketHandler를 등록함으로써, 클라이언트는 /chat 엔드포인트로 접속하여 웹소켓 통신을 수행할 수 있다.
*/
@Configuration
@EnableWebSocketMessageBroker // WebSocket 서버를 활성화하는데 사용되는 어노테이션
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 클라이언트에서 메시지 송신을 구독하는 주소의 프리픽스를 설정
config.enableSimpleBroker("/topic");
// 클라이언트에서 메시지를 보낼 주소의 프리픽스를 설정
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// WebSocket의 연결을 관리하는 엔드포인트를 등록
// SockJS는 WebSocket을 지원하지 않는 브라우저에 대한 폴백 옵션을 활성화하는데 사용
registry.addEndpoint("/ws").withSockJS();
}
}
다음으로 클라이언트의 메시지를 처리하고 응답을 보낼 메시지 핸들러를 구현해야 한다. 이는 보통 @Controller 어노테이션을 사용한 클래스 안에 @MessageMapping 어노테이션을 사용한다.
이러한 설정은 웹 소켓의 동작 방식을 제어하고, 어떤 경로로 메시지를 보내고 받을지, 메시지는 어떻게 처리되고 응답을 어떻게 보낼지 등을 정의하기 위해 필요하다. 웹 소켓은 지속적인 연결을 유지하기 때문에, 이러한 설정 없이는 서버와 클라이언트 간의 메시지 교환을 제대로 제어할 수 없다.
package com.practice.SampleChat.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
@Controller
public class GreetingController {
@MessageMapping("/hello") // 클라이언트에서 /app/hello로 보내는 메시지를 매핑
@SendTo("/topic/chat") // /topic/greetings를 구독하는 클라이언트에게 메시지를 전송
public String chat(String message) throws Exception {
// Jackson의 ObjectMapper를 사용하여 JSON 문자열을 파싱
ObjectMapper mapper = new ObjectMapper();
Map<String, String> map = mapper.readValue(message, Map.class);
// "name" 키에 해당하는 값을 가져옴
String name = map.get("name");
// 현재 시간을 가져와 "년-월-일 시:분:초" 형식으로 변환
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String now = LocalDateTime.now().format(formatter);
// 현재 시간과 입력받은 이름을 포함하여 반환
String returnMessage = "{\\"content\\": \\"" + now + " " + name + "\\"}";
// 메시지를 콘솔에 출력
System.out.println(returnMessage);
return returnMessage;
}
@GetMapping("/chat")
public String chatView() {
return "chat";
}
}
<!DOCTYPE html>
<html xmlns:th="<http://www.thymeleaf.org>">
<head>
<title>WebSocket Demo</title>
<!-- SockJS와 STOMP JavaScript 라이브러리 호출. SockJS는 WebSocket의 폴백 옵션 제공, STOMP는 메시징 프로토콜 -->
<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 type="text/javascript">
// WebSocket 연결 및 STOMP 클라이언트를 관리하는 전역 변수를 선언
var stompClient = null;
// WebSocket 연결 상태에 따라 UI를 업데이트하는 함수
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
document.getElementById('response').innerHTML = '';
}
// WebSocket 연결을 수립하는 함수
function connect() {
// SockJS 객체를 생성하고 WebSocket 연결을 초기화
var socket = new SockJS('/ws');
// STOMP 클라이언트를 생성하고 연결
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
// 서버로부터 메시지를 수신하는 대상을 구독
stompClient.subscribe('/topic/chat', function(greeting){
showGreeting(JSON.parse(greeting.body).content);
});
});
}
// WebSocket 연결을 종료하는 함수
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
// 서버에 메시지를 보내는 함수
function sendName() {
var name = document.getElementById('name').value;
stompClient.send("/app/hello", {}, JSON.stringify({'name': name}));
}
// 받은 메시지를 화면에 표시하는 함수
function showGreeting(message) {
var response = document.getElementById('response');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(message));
response.appendChild(p);
}
</script>
</head>
<body>
<div>
<!-- 'connect' 함수를 호출하여 WebSocket 연결을 수립하는 버튼 -->
<button id="connect" onclick="connect();">ws 프로토콜 연결</button>
<!-- 'disconnect' 함수를 호출하여 WebSocket 연결을 종료 -->
<button id="disconnect" disabled="true" onclick="disconnect();">접속종료</button>
</div>
<div id="conversationDiv">
<!-- 사용자로부터 입력을 받음. '전송' 버튼을 클릭하면 'sendName' 함수를 호출하여 입력값을 서버에 보냄. -->
<label for="name">아무거나 입력</label><input type="text" id="name" />
<button id="send" onclick="sendName();">전송</button>
<!-- 서버로부터 받은 메시지를 표시하는 영역 -->
<p id="response"></p>
</div>
</body>
</html>

좌측 창은 크롬 브라우저, 가운데는 엣지 브라우저, 우측은 크롬 브라우저의 개발자 콘솔이다. 서로 다른 브라우저에서 동일한 엔드포인트로 접속하고 동일한 엔드포인트를 구독하는 connection 함수를 실행하고 나면 양방향 연결이 수립된다.
연결이 수립된 후, 어느 쪽에서 메시지를 입력하더라도 같은 엔드포인트를 구독하는 브라우저에서는 비동기적으로 실시간 메시징이 가능한 것을 확인할 수 있다.