📃 요약
Websocket chatting 샘플입니다.
Stomp( Simple Text Oriented Messaging Protocol ) 이용해서 진행해 보도록 하겠습니다.
기본 메세징 방식은 publisher( 발행자 ) – subscribe( 구독자 ) 방식입니다.
메세지 브로커( 중개인 )가 메세지를 수신하면 구독자에게 전달해 주는 방식입니다.
간단한 예제를 위해 채팅방 개설 같은 기능은 제외하고 회원 ID / 채팅메세지 만으로 샘플을 구성합니다.
Spring 공식홈페이지에서 제공하는 Websocket Stomp 예제는 아래 URL 을 참고하십시요
( https://spring.io/guides/gs/messaging-stomp-websocket )
프론트는 Vue 를 사용하고, 벡엔드는 spring boot 를 사용합니다.
요소 기술 :
– 프론트엔드 : Vue
- 프론트 패키지 : stompjs, sockjs-client, sockjs, webstomp-client
– 벡엔드 : Spring Boot
- 벡엔드 패키지 : websocket 패키지
결과 화면 :
- 채팅 화면
프로젝트 탐색기 : Vue
프로젝트 탐색기 : String Boot
Backend : URL 정보
컨트롤러 함수 어노테이션 | URL | 설명 |
---|---|---|
@MessageMapping | /pub | 메세지를 전달 받는 주소 ( frontend -> backend ) |
@SendTo | /sub | 메세지를 전송하는 주소 ( backend -> frontend ) |
Stomp 간단 메세징 절차
- Stomp 메세징 frame 구조
- Command
- header:value
- Body
- 메세징을 전송하는 규격이 미리 정의되어 있어 편하게 활용할 수 있습니다. 개발자가 메세징 구조를 설계할 필요가 없습니다.
- /pub.으로 메세지를 전송하면 메세지브로커가( 중개인 ) /sub 으로 구독자들에게 메세지를 모두 전송해 줍니다.
- 각 주소를 고유하게 설계해서 채널 또는 채팅방으로 활용할 수도 있습니다.
📃 기술 구현
스펙 :
Spring build.gradle : dependencies 블럭 내 추가
dependencies { implementation 'org.springframework.boot:spring-boot-starter-websocket' ... 생략 }
WebSocketConfig 설정 클래스
-WebSocketConfig.java
package org.example.simplechatting.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; /** * @author : kangtaegyung * @fileName : WebSocketConfig * @since : 24. 6. 1. * description : * 1) sub 구독한 수신자에게 메세지 broadcasting */ @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Value("${simpleDms.app.front-url}") private String frontUrl; // 메시지를 구독할 주소를 정의 @Override public void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) { messageBrokerRegistry.enableSimpleBroker("/sub"); // 1) } // 웹 소켓 연결설정 @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/") .setAllowedOrigins(frontUrl) // vue 주소 .withSockJS(); } }
- 1) sub 구독한 수신자에게 메세지 broadcasting
메세지 브로커 컨트롤러 : /pub 메세지를 전달받아 간단하게 다시 /sub 구독자에게 전달합니다.
-ChatController.java
package org.example.simplechatting.controller; import org.example.simplechatting.dto.ChatMemberDto; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.web.bind.annotation.RestController; /** * @author : kangtaegyung * @fileName : ChatController * @since : 24. 6. 1. * description : * @MessageMapping("/pub") : /pub 를 메시지를 받을 URL * @SendTo("/sub") : /sub 로 메시지를 전달할 URL * => 의미 : /pub 로 메시지를 받고, /sub 로 메시지를 보내줍니다. */ @RestController public class ChatController { @MessageMapping("/pub") @SendTo("/sub") public ChatMemberDto startChatting(ChatMemberDto chatMember) { String userId = chatMember.getUserId(); String chatMessage = chatMember.getChatMessage(); ChatMemberDto chatMember2 = new ChatMemberDto(userId, chatMessage); return chatMember2; } }
– ChatMemberDto.java
package org.example.simplechatting.dto; import lombok.*; /** * @author : kangtaegyung * @fileName : ChatMemberDto * @since : 24. 5. 27. * description : 채팅 회원 DTO */ @Getter @Setter @ToString @NoArgsConstructor @AllArgsConstructor public class ChatMemberDto { private String userId; private String chatMessage; }
– CommonExceptionAdvice
공통 Exception 클래스 : 컨트롤러에서 어떤 예외가 발생하더라도 이 클래스의 함수가 실행됨
package org.example.simplechatting.exceptions; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * @author : kangtaegyung * @fileName : ExControllerAdvice * @since : 24. 5. 27. * description : 공통된 예외 처리 함수 * @ResponseStatus(HttpStatus.상태코드) : 상태코드가 발생하면 * @ExceptionHandler(Exception.class) : Exception 예외 클래스에 대해 처리한다. */ @Slf4j @RestControllerAdvice public class CommonExceptionAdvice { // 컨트롤러에서 어떤 에러가 발생하더라도 이 함수가 실행됨 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public ResponseEntity<?> internalServerErrorException(Exception e) { log.debug("벡엔드 에러: " + e.getMessage()); return new ResponseEntity<>("벡엔드 에러: " + e.getMessage() , HttpStatus.INTERNAL_SERVER_ERROR); } }
Vue 페이지 : 간단한 채팅 화면입니다.
– Vue 패키지 추가 :
"dependencies": { "sockjs": "^0.3.24", "sockjs-client": "^1.6.1", "stompjs": "^2.3.3", "webstomp-client": "^1.2.6" ...생략 },
1) 공통 js
– utils/constraints.js : axios 기본 설정 파일
const SERVER_URL = "http://localhost:8000"; const constraints = { SERVER_URL, } export default constraints;
– router/index.js
import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', component: () => import('../views/HomeView.vue') } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router
Vue 페이지
결과 화면 :
– HeaderCom.vue
<template> <div> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container-fluid"> <a class="navbar-brand" href="#">simple-coding</a> <!-- 단축메뉴(햄버거 메뉴) --> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" > <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <!-- 1st : Home --> <li class="nav-item"> <a class="nav-link active" aria-current="page" href="#">Home</a> </li> </ul> </div> </div> </nav> </div> </template> <script> export default {}; </script> <style></style>
App.vue
<template> <div> <!-- 머리말 --> <HeaderCom /> <!-- 본문 --> <div class="container"> <router-view /> </div> </div> </template> <script> import HeaderCom from "@/components/HeaderCom.vue" export default { components: { HeaderCom } } </script> <style lang="scss"></style>
HomeView.vue
<template> <div class="mt-2"> <div class="col-3"> <label for="userId" class="col-form-label"> userId </label> </div> <div class="col-3"> <input type="text" required class="form-control" placeholder="userId" v-model="chatMember.userId" /> </div> <div class="col-3 mt-5"> <label for="chatMessage" class="col-form-label"> chatMessage </label> </div> <div class="input-group mb-3"> <input type="text" required class="form-control" placeholder="chatMessage" v-model="chatMember.chatMessage" @keyup.enter="sendMessage" /> <button class="btn btn-secondary" @click="sendMessage">전송</button> </div> <div class="phone-wrap mt-5"> <div> <img :src="require('@/assets/img/iphone.png')" alt="아이폰" /> </div> <div class="text-wrap"> <div v-for="(data, index) in messageArray" :key="index"> <h3>유저이름: {{ data.userId }}</h3> <h3>내용: {{ data.chatMessage }}</h3> </div> </div> </div> </div> </template> <script> import Stomp from "webstomp-client"; import SockJS from "sockjs-client"; import constraints from "@/utils/constrains"; export default { data() { return { messageArray: [], // 전송한 내용을 모두 저장할 배열 chatMember: { userId: "", chatMessage: "", }, connected: false, stompClient: null, }; }, methods: { connectSocket() { let socket = new SockJS(constraints.SERVER_URL); // 벡엔드 주소로 접속 this.stompClient = Stomp.over(socket); this.stompClient.connect( {}, () => this.receiveMessage(), (e) => this.errorSocket(e) ); }, receiveMessage() { this.connected = true; this.stompClient.subscribe("/sub", (response) => { // sub 주소로 메세지 받기 this.messageArray.push(JSON.parse(response.body)); }); }, // 접속시 에러 출력 errorSocket(e) { console.log("소켓 연결 실패", e); this.connected = false; }, sendMessage() { if ( this.stompClient && this.connected && this.chatMember.chatMessage !== "" ) { this.stompClient.send("/pub", JSON.stringify(this.chatMember), {}); // pub 주소로 메세지 전송 this.chatMember.chatMessage = ""; } }, }, mounted() { this.connectSocket(); }, }; </script> <style scoped> .phone-wrap { width: 20vw; position: relative; } .phone-wrap img { width: 100%; } .text-wrap { position: absolute; top: 51%; left: 51%; width: 80%; height: 71%; transform: translate(-50%, -50%); background-color: black; font-size: 20px; color: wheat; font-family: Arial, Helvetica, sans-serif; padding: 10px; overflow: scroll; } </style>
📃 결론
Websocket stomp 이용해서 채팅을 진행하는 예제를 살펴보았습니다.
화면에 메세지를 타이핑하고 엔터키 또는 전송 버튼을 클릭하면 /pub 으로 메세지가 전송됩니다.
연달아서 /sub 으로 구독하고 있는 사용자에게 메세지가 전송됩니다.
메세지를 전송받고 전송하는 컨트롤러는 1개만 사용했습니다.
Websocket 과 Stomp 를 이용한 채팅에 관심 있으시다면 Source 는 아래에서 찾을 수 있습니다.
감사합니다.