우아한테크코스 6기 '반갑개' 팀에서 개발하고 있는 모바일 애플리케이션에서 채팅 기능을 담당하게 되었다. 채팅 기능을 개발하면서 공부한 것들을 정리하고자 한다.
프로젝트 세팅
공식 문서에 있는 가이드를 응용해서 채팅 프로그램 예제를 만들어 보았다. 공식 문서에는 채팅 대신 닉네임을 전송하도록 되어 있지만, 잘 와닿지 않아서 닉네임 + 채팅 내용을 보낼 수 있도록 바꿔 보았다.
초기 프로젝트 설정은 Spring Initializr에서 하면 된다.
(Spring Boot 3.3.2, Java 17 기준 작성)
작동 방식
이 코드는 STOMP 방식으로 작동한다. STOMP의 핵심은 pub / sub 이다.
- 메시지를 전송하는 쪽을 publisher 라고 한다.
- 메시지를 구독하는 쪽을 subscriber 라고 한다. 우리가 아는 '구독'의 개념과 크게 다르지 않다.
- 우리는 스프링으로 "A라는 주소로 publish했을 때 B라는 주소를 subscribe하는 사람에게 메시지를 보내줘!" 와 같은 작업을 할 수 있다.
코드
import는 생략하여 작성하였다. IDE의 자동완성 기능을 이용하자.

먼저 다음과 같이 컨트롤러를 작성하자.
ChatController.java
@Controller
public class ChatController {
@MessageMapping("/send")
@SendTo("/topic/chat")
public MessageResponse sendMessage(@Payload MessageRequest request) {
return new MessageResponse(
HtmlUtils.htmlEscape(request.username()), HtmlUtils.htmlEscape(request.content())
);
}
}
위 코드에서 sendMessage() 메서드는 /publish/send로 전송되는 데이터를 /topic/chat을 구독하고 있는 쪽에 보여준다.
/send라고 썼는데 /publish/send로 보내야 하는 이유에 대해서 궁금할 수 있는데, 바로 밑에 있는 config 파일에서 prefix를 설정했기 때문이다.
이 주소들을 채팅방 ID에 맞게 변경하여 여러 개의 채팅방을 구현할 수도 있다.
참고로 HtmlUtils.htmlEscape()는 XSS 공격을 막기 위한 메서드이다.
WebSocketConfig.java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic"); // 메시지 브로커를 등록
config.setApplicationDestinationPrefixes("/publish"); // publish하는 주소의 prefix 지정
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/connect"); // 웹 소켓 초기 연결하는 주소
}
}
enableSimpleBroker() 메서드는 메모리 기반의 메시지 브로커를 등록한다. prefix를 /topic으로 설정했기 때문에, 모든 subscribe 주소 는 /topic으로 시작해야 한다.
또한 setApplicationDestinationPrefixes() 메서드는 publish 하는 주소의 prefix를 일괄 지정한다.addEndpoint() 메서드로는 초기 웹 소켓 연결에 필요한 주소를 지정할 수 있다.
MessageRequest.java
public record MessageRequest(String username, String content) {
}
MessageResponse.java
public record MessageResponse(String username, String content) {
}
클라이언트 코드
공식 문서의 예제를 일부 변형한 것이다.
app.js - src/main/resources/static에 복붙하기
const stompClient = new StompJs.Client({
brokerURL: 'ws://localhost:8080/connect'
});
stompClient.onConnect = (frame) => {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/chat', (message) => {
showMessage(
JSON.parse(message.body).username + ": " + JSON.parse(message.body).content
);
});
};
stompClient.onWebSocketError = (error) => {
console.error('Error with websocket', error);
};
stompClient.onStompError = (frame) => {
console.error('Broker reported error: ' + frame.headers['message']);
console.error('Additional details: ' + frame.body);
};
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#greetings").html("");
}
function connect() {
stompClient.activate();
}
function disconnect() {
stompClient.deactivate();
setConnected(false);
console.log("Disconnected");
}
function sendMessage() {
stompClient.publish({
destination: "/publish/send",
body: JSON.stringify({'username': $("#username").val(), 'content': $("#content").val()})
});
}
function showMessage(message) {
$("#greetings").append("<tr><td>" + message + "</td></tr>");
}
$(function () {
$("form").on('submit', (e) => e.preventDefault());
$( "#connect" ).click(() => connect());
$( "#disconnect" ).click(() => disconnect());
$( "#send" ).click(() => sendMessage());
});
테스트 해보기!
다른 스프링 프로젝트와 마찬가지로 http://localhost:8080 에 접속하여 채팅을 테스트해볼 수 있다.

다음과 같이 탭을 두 개 (이상) 띄우고 각각의 창에서 Connect를 누른다.
그런 다음 이름과 내용 필드를 채워 Send 버튼을 누르면 양쪽 창에서 정상적으로 채팅 내용이 반영되는 것을 확인할 수 있다.
