이번에는 Websocket의 프로세스를 좀더 고도화 하고
메시징에 좀 더 최적화된 방식을 구현하기 위해 Stomp를 적용해 보겠습니다.
Stomp란?
stomp는 메시징 전송을 효율적으로 하기 위해 나온 프로토콜이며 기본적으로 pub/sub 구조로 되어있습니다.
메시지를 발송하고 처리하는 부분이 명확하여 개발하는 입장에서 명확하게 인지하고 개발 할 수 있는 이점이 있습니다.
또한 통신 메시지의 헤더에 값을 세팅할 수 있어 헤더값을 기반으로 통신 시 인증처리를 구현하는 것도 가능합니다.
pub는 publisher(집배원)으로 볼 수 있으며 sub는 subscriber(구독자)로써 여러명이 될 수 있습니다.
- 채팅방 만들기 : pub/sub를 위한 topic 생성
- 채팅방 입장 : topic 구독
- 채팅방에서 메시지 보내고 받기 : 해당 topic에 메시지 발송(pub) 또는 메시지 받기(sub)
자 이제 Stomp를 이용하여 채팅 프로그램을 구현해 보자
구별을 위해 패키지를 변경하였습니다 디렉토리의 구조는 다음과 같습니다.
build.gradle
dependencies에 라이브러리를 추가합니다.
추가할 라이브러리는
- sockjs : 낮은 브라우저에서도 websocket 지원
- stomp : stomp방식을 사용하기 위해
- thymeleaf : 프론트단 개발을 위해
- vue : 프론트단 개발을 위해
- bootstrap : 프론트단 개발을 위해
- axios : 프론트단 개발을 위해
WebSockConfig 수정
stomp를 사용하기 위한 broker를 선언
메시지를 발행하는 요청의 prefix는 /pub로 시작하도록 하고
메시지를 구독하는 요청의 prefix는 /sub로 시작하도록 설정
그리고 Stomp websocket의 연결 endpoint는 /ws-stomp로 설정합니다.
ChatRoom DTO 수정
pub/sub 방식을 이용하면 구독자 관리가 알아서 되므로 웹소켓 세션 관리가 필요 없어집니다.
또한 발송의 구현도 알아서 해결 되므로 일일이 클라이언트에게 메시지 발송하는 구현이 필요없어집니다.
따라서 ChatRoom DTO는 다음과 같이 간소화됩니다.
ChatRoomRepository 생성
채팅방을 생성하고 정보를 조회하는 Repository를 생성합니다.
여기서는 간단하게 Map으로 구현하지만
서비스에서는 DB나 다른 저장매체에 구현해야 합니다.
그리고 ChatService는 ChatRoomRepository가 대체하므로 삭제합니다.
ChatController 수정 (publicher 구현)
기존의 WebSockChatHandler가 했던 역할을 대체하므로 WebsocketChatHandler는 삭제합니다.
ChatRoomController 생성
채팅화면 View 구성을 위해 필요한 Controller를 작성합니다.
프론트단 개발
/resources/templates/chat 하위에
room.html
roomdetail.html 두개의 파일을 생성합니다
/resources/templates/chat/room.html
채팅방을 개설,입장하는 페이지
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="/webjars/bootstrap/4.3.1/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container" id="app" v-cloak>
<div class="row">
<div class="col-md-12">
<h3>채팅방 리스트</h3>
</div>
</div>
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">방제목</label>
</div>
<input type="text" class="form-control" v-model="room_name" v-on:keyup.enter="createRoom">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="createRoom">채팅방 개설</button>
</div>
</div>
<ul class="list-group">
<li class="list-group-item list-group-item-action" v-for="item in chatrooms" v-bind:key="item.roomId" v-on:click="enterRoom(item.roomId)">
{{item.name}}
</li>
</ul>
</div>
<script src="/webjars/vue/2.5.16/dist/vue.min.js"></script>
<script src="/webjars/axios/0.17.1/dist/axios.min.js"></script>
<script >
var vm = new Vue({
el: '#app',
data: {
room_name : '',
chatrooms: [
]
},
created() {
this.findAllRoom();
},
methods: {
findAllRoom: function() {
axios.get('/chat/rooms').then(response => { this.chatrooms = response.data; });
},
createRoom: function() {
if("" === this.room_name) {
alert("방 제목을 입력해 주십시요.");
return;
} else {
var params = new URLSearchParams();
params.append("name",this.room_name);
axios.post('/chat/room', params)
.then(
response => {
alert(response.data.name+"방 개설에 성공하였습니다.")
this.room_name = '';
this.findAllRoom();
}
)
.catch( response => { alert("채팅방 개설에 실패하였습니다."); } );
}
},
enterRoom: function(roomId) {
var sender = prompt('대화명을 입력해 주세요.');
if(sender != "") {
localStorage.setItem('wschat.sender',sender);
localStorage.setItem('wschat.roomId',roomId);
location.href="/chat/room/enter/"+roomId;
}
}
}
});
</script>
</body>
</html>
/resources/templates/chat/roomdetail.html
채팅방 개별 세부 페이지(유저끼리 채팅 가능)
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Websocket ChatRoom</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="/webjars/bootstrap/4.3.1/dist/css/bootstrap.min.css">
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div class="container" id="app" v-cloak>
<div>
<h2>{{room.name}}</h2>
</div>
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">내용</label>
</div>
<input type="text" class="form-control" v-model="message" v-on:keypress.enter="sendMessage">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="sendMessage">보내기</button>
</div>
</div>
<ul class="list-group">
<li class="list-group-item" v-for="message in messages">
{{message.sender}} - {{message.message}}
</li>
</ul>
<div></div>
</div>
<!-- JavaScript -->
<script src="/webjars/vue/2.5.16/dist/vue.min.js"></script>
<script src="/webjars/axios/0.17.1/dist/axios.min.js"></script>
<script src="/webjars/sockjs-client/1.1.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3-1/stomp.min.js"></script>
<script>
//alert(document.title);
// websocket & stomp initialize
var sock = new SockJS("/ws-stomp");
var ws = Stomp.over(sock);
var reconnect = 0;
// vue.js
var vm = new Vue({
el: '#app',
data: {
roomId: '',
room: {},
sender: '',
message: '',
messages: []
},
created() {
this.roomId = localStorage.getItem('wschat.roomId');
this.sender = localStorage.getItem('wschat.sender');
this.findRoom();
},
methods: {
findRoom: function() {
axios.get('/chat/room/'+this.roomId).then(response => { this.room = response.data; });
},
sendMessage: function() {
ws.send("/pub/chat/message", {}, JSON.stringify({type:'TALK', roomId:this.roomId, sender:this.sender, message:this.message}));
this.message = '';
},
recvMessage: function(recv) {
this.messages.unshift({"type":recv.type,"sender":recv.type=='ENTER'?'[알림]':recv.sender,"message":recv.message})
}
}
});
function connect() {
// pub/sub event
ws.connect({}, function(frame) {
ws.subscribe("/sub/chat/room/"+vm.$data.roomId, function(message) {
var recv = JSON.parse(message.body);
vm.recvMessage(recv);
});
ws.send("/pub/chat/message", {}, JSON.stringify({type:'ENTER', roomId:vm.$data.roomId, sender:vm.$data.sender}));
}, function(error) {
if(reconnect++ <= 5) {
setTimeout(function() {
console.log("connection reconnect");
sock = new SockJS("/ws-stomp");
ws = Stomp.over(sock);
connect();
},10*1000);
}
});
}
connect();
</script>
</body>
</html>
테스트
다음 시간에는 Redis를 통해 다중서버에서 채팅이 가능하도록 고도화 해보겠습니다.
감사합니다.
출처 : https://www.daddyprogrammer.org/post/4077/spring-websocket-chatting/#google_vignette
'스프링부트' 카테고리의 다른 글
스프링 클라우드 데이터 플로우 (SCDF)를 이용한 로컬 서버 구축 및 어플리케이션 통합 - 실습 가이드와 네이버 개발자 리소스 (1) (0) | 2023.06.09 |
---|---|
스프링 클라우드 데이터 플로우 (SCDF)를 활용한 실시간 대용량 데이터 처리 프로젝트 소개와 경험 공유 (0) | 2023.06.08 |
스프링의 의존성 주입과 객체간의 결합도 이해: 객체 생성과 관리를 이루는 기본 개념 및 실제 적용 (0) | 2023.02.20 |
Java 자바 스프링 부트 채팅 애플리케이션 코드 해석: 핵심 개념 및 활용 방법 이해하기 (3) (0) | 2023.02.20 |
Java 스프링부트 채팅 프로그램 확장하기: 여러 채팅방 구현, 컨트롤러 설정 및 테스트 방법 (2) (0) | 2023.02.20 |
댓글