📃 요약
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 는 아래에서 찾을 수 있습니다.
감사합니다.



