๐[WEB] STOMP๋?/SpringBoot๋ก ์ฑํ ๊ตฌํ
STOMP
STOMP๋ Simple/Stream Text Oriented Message Protocol์ ์ฝ์๋ก, ๋ฉ์์ง ์ ์ก์ ํจ์จ์ ์ผ๋ก ํ๊ธฐ ์ํด ๋์จ ํ๋กํ ์ฝ์ด๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก pub/sub
๊ตฌ์กฐ๋ก ๋์ด์์ด ๋ฉ์์ง๋ฅผ ๋ฐ์กํ๊ณ , ๋ฉ์์ง๋ฅผ ๋ฐ์ ์ฒ๋ฆฌํ๋ ๋ถ๋ถ์ด ํ์คํ ์ ํด์ ธ์๊ธฐ ๋๋ฌธ์ ๊ฐ๋ฐํ๋ ์
์ฅ์์ ๋ช
ํํ๊ฒ ์ธ์งํ๊ณ ๊ฐ๋ฐํ ์ ์๋ ์ด์ ์ด ์๋ค.
STOMP๋ ์น์์ผ(WebSocket)
์์์ ๋์ํ๋ฉฐ, http์ ๋ง์ฐฌ๊ฐ์ง๋ก frame
์ ์ฌ์ฉํด ์ ์กํ๋ ํ๋กํ ์ฝ์ด๋ค.
ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ์ ์กํ ๋ฉ์์ง์ ์ ํ, ํ์, ๋ด์ฉ๋ค์ ์ ์ํ๋ ๋งค์ปค๋์ฆ์ด๋ค.
๋ํ stomp๋ฅผ ์ด์ฉํ๋ฉด ํต์ ๋ฉ์์ง์ ํค๋์ ๊ฐ์ ์ธํ ํ ์ ์์ด ํค๋ ๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก ํต์ ์ ์ธ์ฆ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํ๋ ๊ฒ๋ ๊ฐ๋ฅํ๋ค๊ณ ํ๋ค.
STOMP๋ฅผ ๊ฐ๋จํ๊ฒ ์ดํดํด๋ณด์
- Simple Text Oriented Messaging Protocol
- ๋ฉ์์ง ๋ธ๋ก์ปค๋ฅผ ํ์ฉํด ์ฝ๊ฒ ๋ฉ์์ง๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์๋ ํ๋กํ ์ฝ
Pub - Sub(๋ฐํ - ๊ตฌ๋ )
: ๋ฐ์ ์๊ฐ ๋ฉ์์ง๋ฅผ ๋ฐํ -> ์์ ์๊ฐ ๊ทธ ๊ฒ์ ์์ ํ๋ ๋ฉ์์ง ํจ๋ฌ๋ค์- ๋ฉ์์ง ๋ธ๋ก์ปค : ๋ฐ์ ์์ ๋ฉ์์ง๋ฅผ ๋ฐ์์์ ์์ ์๋ค์๊ฒ ๋ฉ์์ง๋ฅผ ์ ๋ฌํ๋ ์ด๋ค ๊ฒ
- ์น์์ผ ์์ ์น์ด ํจ๊ป ์ฌ์ฉํ ์ ์๋ ํ์(์๋ธ) ํ๋กํ ์ฝ
Frame?
Frame
์ ๋ช
๋ น(Command)๊ณผ ์ถ๊ฐ์ ์ธ ํค๋(Header)์ ์ถ๊ฐ์ ์ธ ๋ฐ๋(Body)๋ก ๊ตฌ์ฑ๋๋ค.
Frame์ ๋ช ๊ฐ์ ํ
์คํธ ๋ผ์ธ์ผ๋ก ์ง์ ๋ ๊ตฌ์กฐ์ธ๋ฐ, ์ฒซ ๋ฒ์งธ ๋ผ์ธ์ ํ
์คํธ(Command ๋ช
๋ น์ด)๊ณ , ์ดํ key:value ํํ๋ก header ์ ๋ณด๋ฅผ ํฌํจํ๋ค.
header ์ดํ์ ๊ณต๋ฐฑ ์ค์ ํ๋ ๋ ์ถ๊ฐํ๋ ๊ฒ์ผ๋ก header์ ๋์ ์ค์ ํ ์ ์๋ค.
header ์ดํ์๋ Payload(Body)๊ฐ ์กด์ฌํ๊ณ , ์ด๋ ์ ์ก๋๋ ๋ฐ์ดํฐ๋ฅผ ์๋ฏธํ๋ค.
[์์]
COMMAND
header1:value1
header2:value2
Body^@
SpringBoot & STOMP๋ฅผ ์ด์ฉํ ์ฑํ ๊ตฌํ
build.gradle
plugins {
id 'org.springframework.boot' version '2.1.5.RELEASE'
id 'java'
}
apply plugin: 'io.spring.dependency-management'
group = 'com.spring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-freemarker'
implementation 'org.springframework.boot:spring-boot-devtools'
implementation 'org.webjars.bower:bootstrap:4.3.1'
implementation 'org.webjars.bower:vue:2.5.16'
implementation 'org.webjars.bower:axios:0.17.1'
implementation 'org.webjars:sockjs-client:1.1.2'
implementation 'org.webjars:stomp-websocket:2.3.3-1'
implementation 'com.google.code.gson:gson:2.8.0'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
application.yml
spring:
devtools:
livereload:
enabled: true
restart:
enabled: false
freemarker:
cache: false
application.properties
spring.devtools.livereload.enabled=true
spring.devtools.restart.enabled=false
spring.freemarker.cache=false
WebSockConfig ์์
Stomp๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด @EnableWebSocketMessageBroker
๋ฅผ ์ ์ธํ๊ณ , WebSocketMessageBrokerConfigurer
๋ฅผ ์์๋ฐ์ configureMessageBroker
๋ฅผ ๊ตฌํํ๋ค.
๋ฉ์์ง๋ฅผ ๊ตฌ๋
ํ๋ ์์ฒญ์ prefix/sub
๋ก, stomp websocket connection endpoint๋ /ws-stomp
๋ก ์ค์ ํ๋ค.
์ฆ, ๊ฐ๋ฐ ์๋ฒ์ ์ ์ ์ฃผ์๋ ws://localhost:8080/ws-stomp ๊ฐ ๋๋ค.
@Configuration
@EnableWebSocketMessageBroker
public class WebSockConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/sub");
config.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp").setAllowedOrigins("*")
.withSockJS();
}
}
ChatRoom ์์ (DTO)
pub/sub ๋ฐฉ์์ ์ด์ฉํ๋ฉด ๊ตฌ๋
์ ๊ด๋ฆฌ๊ฐ ์์์ ๋๋ฏ๋ก, ์น์์ผ ์ธ์
๊ด๋ฆฌ๊ฐ ํ์ ์์ด์ง๋ค.
๋, ๋ฐ์ก์ ๊ตฌํ๋ ์์์ ํด๊ฒฐ๋๋ฏ๋ก ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง๋ฅผ ๋ฐ์กํ๋ ๊ตฌํ์ด ํ์ ์์ด์ง๋ค.
@Getter
@Setter
public class ChatRoom {
private String roomId;
private String name;
public static ChatRoom create(String name) {
ChatRoom chatRoom = new ChatRoom();
chatRoom.roomId = UUID.randomUUID().toString();
chatRoom.name = name;
return chatRoom;
}
}
ChatRoomRepository ์์ฑ
package com.spring.wschat.repo;
// import ์๋ต....
@Repository
public class ChatRoomRepository {
private Map<String, ChatRoom> chatRoomMap;
@PostConstruct
private void init() {
chatRoomMap = new LinkedHashMap<>();
}
public List<ChatRoom> findAllRoom() {
// ์ฑํ
๋ฐฉ ์์ฑ์์ ์ต๊ทผ ์์ผ๋ก ๋ฐํ
List chatRooms = new ArrayList<>(chatRoomMap.values());
Collections.reverse(chatRooms);
return chatRooms;
}
public ChatRoom findRoomById(String id) {
return chatRoomMap.get(id);
}
public ChatRoom createChatRoom(String name) {
ChatRoom chatRoom = ChatRoom.create(name);
chatRoomMap.put(chatRoom.getRoomId(), chatRoom);
return chatRoom;
}
}
ChatController ์์
@MessageMapping
์ ํตํด ์น์์ผ์ผ๋ก ๋ค์ด์ค๋ ๋ฉ์์ง ๋ฐํ์ ์ฒ๋ฆฌํ๋ค.
ํด๋ผ์ด์ธํธ์์๋ prefix๋ฅผ ๋ถ์ฌ /pub/chat/message๋ก ๋ฐํ์ ์์ฒญํ๋ฉด ์ปจํธ๋กค๋ฌ๊ฐ ํด๋น ๋ฉ์์ง๋ฅผ ๋ฐ์ ์ฒ๋ฆฌํ๋ค.
๋ฉ์์ง๊ฐ ๋ฐํ๋๋ฉด /sub/chat/room/{roomId}๋ก ๋ฉ์์ง๋ฅผ sendํ๋ ๊ฒ์ ๋ณผ ์ ์๋๋ฐ, ํด๋ผ์ด์ธํธ์์๋ ํด๋น ์ฃผ์๋ฅผ (/sub/chat/room/{roomId}) ๊ตฌ๋
ํ๊ณ ์๋ค๊ฐ ๋ฉ์์ง๊ฐ ์ ๋ฌ๋๋ฉด ํ๋ฉด์ ์ถ๋ ฅํ๋ค.
@RequiredArgsConstructor
@Controller
public class ChatController {
private final SimpMessageSendingOperations messagingTemplate;
@MessageMapping("/chat/message")
public void message(ChatMessage message) {
if (ChatMessage.MessageType.JOIN.equals(message.getType()))
message.setMessage(message.getSender() + "๋์ด ์
์ฅํ์
จ์ต๋๋ค.");
messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
}
}
๊ตฌ๋ ์(Subscriber) ๊ตฌํ
์๋ฒ ๋จ์์๋ ๋ฐ๋ก ๊ตฌํํ ํ์๊ฐ ์๋ค.
์น ๋ทฐ์์ stomp ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด ๊ตฌ๋
์ ์ฃผ์๋ฅผ ๋ฐ๋ผ๋ณด๊ณ ์๋ ์ฝ๋๋ฅผ ์์ฑํ๋ฉด ๋๋ค.
ChatRoomController ์์ฑ
@RequiredArgsConstructor
@Controller
@RequestMapping("/chat")
public class ChatRoomController {
private final com.spring.wschat.repo.ChatRoomRepository chatRoomRepository;
// ์ฑํ
๋ฆฌ์คํธ ํ๋ฉด
@GetMapping("/room")
public String rooms(Model model) {
return "/chat/room";
}
// ๋ชจ๋ ์ฑํ
๋ฐฉ ๋ชฉ๋ก ๋ฐํ
@GetMapping("/rooms")
@ResponseBody
public List<ChatRoom> room() {
return chatRoomRepository.findAllRoom();
}
// ์ฑํ
๋ฐฉ ์์ฑ
@PostMapping("/room")
@ResponseBody
public ChatRoom createRoom(@RequestParam String name) {
return chatRoomRepository.createChatRoom(name);
}
// ์ฑํ
๋ฐฉ ์
์ฅ ํ๋ฉด
@GetMapping("/room/enter/{roomId}")
public String roomDetail(Model model, @PathVariable String roomId) {
model.addAttribute("roomId", roomId);
return "/chat/roomdetail";
}
// ํน์ ์ฑํ
๋ฐฉ ์กฐํ
@GetMapping("/room/{roomId}")
@ResponseBody
public ChatRoom roomInfo(@PathVariable String roomId) {
return chatRoomRepository.findRoomById(roomId);
}
}
์ฑํ ํ๋ฉด ์์ฑ
์ฑํ
๋ฐฉ ๋ฆฌ์คํธ, ์ฑํ
๋ฐฉ ์์ธ ํ๋ฉด view๋ฅผ ์์ฑํ๋ค.
/resources/templates/
์ room.ftl, roomdetail.ftl ํ์ผ์ ์์ฑํ๋ค.
room.ftl
<!doctype html>
<html lang="en">
<head>
<title>Websocket Chat</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!-- 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 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)">
</li>
</ul>
</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>
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>
roomdetail.ftl
<!doctype html>
<html lang="en">
<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></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">
- </a>
</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>
์ฐธ๊ณ ์๋ฃ
https://velog.io/@qkrqudcks7/STOMP%EB%9E%80
https://www.daddyprogrammer.org/post/4691/spring-websocket-chatting-server-stomp-server/
WebSocket ๊ณต์๋ฌธ์
[10๋ถ ํ
์ฝํก] โจ ์๋ก ์ แแ
ฐแธแแ
ฉแแ
ฆแบ&แแ
ณแแ
ณแ
แ
ตแผ
๋๊ธ๋จ๊ธฐ๊ธฐ