본문 바로가기
스프링부트

Stomp를 활용한 실시간 채팅 프로그램 구현: 웹소켓 최적화 및 효율적인 메시징 전송 (4)

by 플라퉁 2023. 2. 24.
728x90
반응형

 

 

 

 

이번에는 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

 

Spring websocket chatting server(1) - basic websocket server

Spring에서 제공하는 Websocket을 이용하여 간단한 채팅 서버를 구현해 보도록 하겠습니다. 일반적인 http통신을 하는 서버들과 달리 채팅 서버는 socket통신을 하는 서버가 필요합니다. 통상적으로 htt

www.daddyprogrammer.org

 

 

 

 

 

728x90
반응형

댓글