현재 진행중인 프로젝트에서 websocket을 이용한 채팅 시스템을 개발해야하는데 해당 부분에 대해서 공부하며 정리해보고자한다.
해당 유튜브 영상을 많이 참고했다.
https://www.youtube.com/watch?v=rvss-_t6gzg
HTTP
웹소켓
STOMP 프레임 구조(위 유튜브 영상에서 발췌)
웹소켓은 페이로드(메시지) 길이, 오퍼레이션 코드(프레임 유형), 마스킹 키, 페이로드 데이터 등의 기본 프레임을 사용하여 데이터를 교환하지만 메시지 형식이나 데이터 구조에 대한 규정은 제공하지 않는다.
반면에 STOMP를 이용 시 데이터가 이해하기 쉽게 구조화 되어 나온다는 장점이 있다.
STOMP 통신 흐름
발신자는 "/app"을 통해 메시지를 송신 시에 서버에서 메시지를 가공/처리를 한 뒤 브로커를 통해 "/topic"을 구독하고 있는 구독자에게 전달된다.
"/topic"을 통해 메시지를 송신 시에는 메시지를 가공/처리를 하지 않고 "/topic"을 구독하는 구독자에게 전달된다.
위 개념을 통해 스프링으로 간단한 방명록 서비스를 실습하였다.
해당 실습은 아래의 블로그를 발췌하였다.
https://growth-coder.tistory.com/157
<build.gradle>
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.10'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.websocket'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '11'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:webjars-locator-core'
implementation 'org.webjars:sockjs-client:1.0.2'
implementation 'org.webjars:stomp-websocket:2.3.3'
implementation 'org.webjars:bootstrap:3.3.7'
implementation 'org.webjars:jquery:3.1.1-1'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
html, js 파일은 서버의 동작을 확인하기 위함으로 src/main/resources/static 파일 아래에 코드를 입력하였다.
<index.html>
<!DOCTYPE html>
<html>
<head>
<title>Hello WebSocket</title>
<link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/main.css" rel="stylesheet">
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/app.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
<div class="row">
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="connect">WebSocket connection:</label>
<button id="connect" class="btn btn-default" type="submit">Connect</button>
<button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
</button>
</div>
</form>
</div>
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="name">What is your name?</label>
<input type="text" id="name" class="form-control" placeholder="Your name here...">
</div>
<button id="send" class="btn btn-default" type="submit">Send</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="conversation" class="table table-striped">
<thead>
<tr>
<th>Greetings</th>
</tr>
</thead>
<tbody id="greetings">
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
<app.js>
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#greetings").html("");
}
function connect() {
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}
function showGreeting(message) {
$("#greetings").append("<tr><td>" + message + "</td></tr>");
}
$(function () {
$("form").on('submit', function (e) {
e.preventDefault();
});
$( "#connect" ).click(function() { connect(); });
$( "#disconnect" ).click(function() { disconnect(); });
$( "#send" ).click(function() { sendName(); });
});
<HelloMessage.java>
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class HelloMessage {
private String name;
}
클라이언트로부터 받은 메시지를 나타내는 모델 클래스이다.
name은 사용자의 이름을 저장하는데 사용된다.
<Greeting.java>
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Greeting {
private String content;
}
메시지의 내용을 저장하는 모델이다.
메시지의 내용(content)를 저장하는 역할을 하며 클라이언트와 서버 간에 데이터 교환 시에 사용된다.
<GreetingController.java>
@Controller
public class GreetController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception{
Thread.sleep(1000);
return new Greeting("Hello, "+
HtmlUtils.htmlEscape(message.getName())+"!");
//HtmlUtils.htmlEscape(String input)
}
}
메시지를 받고 처리하는 컨트롤러이다.
@MessageMapping("/hello")는 클라이언트가 '/app/hello' 경로로 메시지를 보낼 때 해당 메소드(greeting)를 호출하도록 설정하였다.
이 경로는 'WebSocketConfig'에서 설정한 애플리케이션 destination prefix '/app'과 결합되어 사용된다.
@SendTo("/topic/greetings")는 'greeting' 메소드가 반환하는 객체('Greeting')를 '/topic/greetings' 경로를 구독하는 모든 클라이언트에게 전송하도록 지정한다.
이를 통해 STOMP 통신 흐름 처리에서 설명한 서버에서의 가공/처리 이후의 구독자에게 메시지를 송신이 가능하다.
<WebSocketConfig.java>
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config){
//발행자가 토픽 경로로 메시지를 주면 구독자에게 전달
config.enableSimpleBroker("/topic");
//발행자가 앱 경로로 메시지를 주면 가공을 해서 구독자에게 전달
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry){
//커넥션을 맺는 경로 설정
//만약 websocket을 사용할 수 없는 브라우저라면 다른 방식 사용하도록 설정
registry.addEndpoint("/ws").withSockJS();
}
}
'WebSocketConfig' 클래스는 WebSocket과 STOMP 메시징을 위한 설정을 담당한다. @Configuration과 @EnableWebSocketMessageBrocker 애노테이션을 통해 스프링의 웹소켓 메시지 브로커를 활성화한다.
'configureMessageBroker'메소드에서는 메시지 브로커가 사용할 경로를 설정한다. '/topic'으로 시작하는 경로는 메시지 브로커가 처리하여 구독자에게 메시지를 전달하며 '/app'으로 시작하는 경로는 애플리케이션 내에서 처리된 후 특정 브로커에게 전달된다.
'registerStompEndpoints' 메소드에서는 STOMP 프로토콜을 사용할 수 있는 WebSocket 엔드포인트를 등록한다. 'withSockJS()'를 사용하여 WebSocket을 지원하지 않는 브라우저에서도 대체 옵션을 사용할 수 있도록 한다.
실행화면