Spring Boot, Java Web Socket을 활용한 웹 채팅 프로그램 만들기
이번에는 간단하게 Web Socket을 이용하여 채팅 프로그램을 만드는 방법을 알아보도록 하겠습니다. 버전 정보는 다음과 같습니다.
spring boot : 3.2.5
java : 17
web socket : 3.2.0
UI, CSS: bootstrap chatting template
의존성 추가
gradle
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket', version: '3.2.0'
maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>3.2.0</version>
</dependency>
먼저 WebSocketConfig class를 작성 해 줍니다.
package com.config;
import com.globalHandler.SocketHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new SocketHandler(), "/socket")
.setAllowedOrigins("*");
}
}
@EnableWebSocket 어노테이션의 경우 spring 애플리케이션에서 WebSocket을 활성화 합니다.
오버라이딩 된 registerWebSocketHandlers 메소드는 SocketHandler() 인스턴스를 레지스트리에 추가합니다. URL, "/socket"로 요청이 들어 왔을 때 엔드포인트에 대한 WebSocket 요청은 SocketHandler() class에 처리됩니다. .setAllowedOrigins("*"); 모든 출처의 URL 요청을 허용합니다.
위에 Mapping 시킨 class, SocketHandler class를 생성하여, TextWebSocketHandler class를 상속 받습니다.
public class SocketHandler extends TextWebSocketHandler
전역 변수 선언
// socket 사용자
List<WebSocketSession> sessionList = new ArrayList<WebSocketSession>();
// 채팅방 활성화
List<Map<String, Object>> roomList = new ArrayList<Map<String, Object>>();
socket 이용자와 채팅방 활성화를 구별하기 위해 전역 변수를 선언합니다.
그 후 handleTextMessage 메소드를 오버라이딩 하여 필요한 부분을 구현 해 줍니다. handleTextMessage 메소드를 구현하면 실제 화면에서 데이터를 request, response하여 데이터를 주고 받을 수 있습니다. 데이터는 TextMessage class에서message.getPayload()를 이용하여 Socket통신으로 받은 데이터를 속출할 수 있습니다. 이러한 데이터를 ObjectMapper class를 이용하여 DTO class에 초기화 하여 사용하였습니다.(spring 특정 이상 버전에서는 ObjectMapper 의존성을 추가하지 않아도 사용 가능함.)
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 방 생성 및 방 입장
createChatRoom(session, message);
// 소켓 데이터 (세션 -> 방) 체크
String sessionId = session.getId();
String uid = "";
for(int i = 0; i < roomList.size(); i++){
if(sessionId.equals((String)roomList.get(i).get("chatSession"))){
uid = (String)roomList.get(i).get("chatRoomId");
}
}
// 데이터 셋
ObjectMapper objectMapper = new ObjectMapper();
ParamsDTO paramsDTO;
// 방에 들어가지 않고 메시지를 보냈다면
if("".equals(uid) || uid == null){
WebSocketSession webSocketSession = session;
paramsDTO = objectMapper.readValue(message.getPayload(), ParamsDTO.class);
paramsDTO.setStatus(404);
String json = new ObjectMapper().writeValueAsString(paramsDTO);
webSocketSession.sendMessage(new TextMessage(json));
}
// 채팅방 체크
if(uid != null && uid != ""){
// 소켓 메세지 보내기
for(int i = 0; i < roomList.size(); i++){
if(uid.equals((String)roomList.get(i).get("chatRoomId"))){
paramsDTO = objectMapper.readValue(message.getPayload(), ParamsDTO.class);
// 메세지를 보낸 주체가 자신인지 상대방인지
if(sessionId.equals((String)roomList.get(i).get("chatSession"))){
paramsDTO.setChatSession((String)roomList.get(i).get("chatSession"));
}
// 상태
paramsDTO.setStatus(200);
String json = new ObjectMapper().writeValueAsString(paramsDTO);
if((json != null && !"".equals(json)) && !"null".equals(json)){
// 상태
WebSocketSession webSocketSession = (WebSocketSession)roomList.get(i).get("session");
webSocketSession.sendMessage(new TextMessage(json));
}
}
}
}
}
채팅방의 roomId와 socket의 sessionId를 활용하여 밸리데이션 체크와 실제 대화를 주고받을 수 있는 조건을 구현하였습니다. List roomList는 채팅방 사용의 필수 정보들이 담겨져 있습니다.
방 생성과 방 입장의 경우로 나누어 구현하였습니다. 방을 생성하거나 방을 입장하는 메소드는 createChatRoom(session, message); 입니다. 코드는 다음과 같습니다.
public void createChatRoom(WebSocketSession session, TextMessage message) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
ParamsDTO paramsDTO = objectMapper.readValue(message.getPayload(), ParamsDTO.class);
int connectionType = paramsDTO.getConnectionType();
// 밸리데이션 체크
if(connectionType == 0){
return;
}
// 중복 체크
if(roomList.size() > 0){
String chk = session.getId();
for(int i = 0; i < roomList.size(); i++){
if(chk.equals((String)roomList.get(i).get("chatSession"))){
return;
}
}
}
// 방 생성
if(connectionType == 1){
Map<String, Object> map = new HashMap<String, Object>();
map.put("chatSession", session.getId());
map.put("session", session);
map.put("chatNickName", paramsDTO.getChatNickName());
map.put("conversationCHK", 0);
UUID uuid = UUID.randomUUID();
map.put("chatRoomId", uuid.toString());
roomList.add(map);
sessionList.add(session);
}
// 대화 시작
if(connectionType == 2){
Map<String, Object> map = new HashMap<String, Object>();
// 랜덤 로직
for(Map<String, Object> roomUser : roomList){
int conversationCHK = (int)roomUser.get("conversationCHK");
if(conversationCHK != 1){
map.put("chatSession", session.getId());
map.put("session", session);
map.put("chatNickName", paramsDTO.getChatNickName());
map.put("conversationCHK", 1);
map.put("chatRoomId", roomUser.get("chatRoomId"));
sessionList.add(session);
}
}
roomList.add(map);
}
}
방 생성일 경우 uuid를 신규 생성하고, 방 입장의 경우 순차적으로 방에 입장합니다. 만약 랜덤 채팅이나, 게시판 목록을 활용하여 1:1 채팅방 및 1:N 채팅방을 구현한다면, 대화 시작 부분의 추가 로직을 구현 해 주면 됩니다.
필요한 자동 설정을 추가할 수 있습니다.
// 통신 연결 요청
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("통신 연결 요청!!");
//sessionList.add(session);
}
// 통신 연결 해제
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("통신 연결 해제!!");
sessionList.remove(session);
}
위 코드는 통신 연결될 때 및 통신이 종료될 때 항상 호출하여 추가 로직을 구현할 수 있습니다.
화면 front-end 처리는 다음과 같습니다.(프로토콜이 HTTP면 ws, HTTPS면 wss로 하면 됩니다.)
const socketUrl = 'wss://192.168.0.2:8080/socket';
let webSocket = new WebSocket(socketUrl);
let mySessionId = "";
let myRoomId = "";
먼저 socketUrl을 설정 해 줍니다. 필자의 경우 webSocket URL과 접속한 session, 접속한 roomId를 판별하기 위해 두 변수를 추가적으로 선언하였습니다.
그리고 방을 생성하거나, 채팅 메세지를 보냈을 때 처리 함수를 구현 해 줍니다.
// 방 생성 및 입장
function fn_chatStart(param){
if($("#nickName").val() == null || $("#nickName").val() == ""){
alert("닉네임을 입력 해 주세요!");
return;
}
let connectionType = 2; // 대화
if(param != null && param != ""){
connectionType = 1; // 방생성
}
let data = {
chatNickName : $("#nickName").val(),
connectionType: connectionType
};
webSocket.send(JSON.stringify(data));
$("#nickName").attr('readonly', true);
}
// 메세지 보내기
function fn_chatSend(){
let data = {
chatNickName : $("#nickName").val(),
msg: $("#inputText").val(),
};
webSocket.send(JSON.stringify(data));
$("#inputText").val("");
}
여기서 위 코드에서 선언 한 webSocket 변수의 send 함수를 호출 합니다. 매개변수는 Json 데이터로 진행 해 주시면 됩니다.
// 소켓 통신 RES
webSocket.onmessage = function (res) {
let chattingHTML = "";
let data = JSON.parse(res.data);
mySessionId = data.chatSession;
let status = data.status;
if(status == 404){
alert("접속된 방이 없습니다.");
return;
}
// 메세지 존재 유무 체크
if(data.msg != null && data.msg != ""){
// 주체가 누구인지 체크
if(mySessionId != null && mySessionId != ""){ // 메세지 보낸이가 나라면
chattingHTML += '<li class="chat-right">';
chattingHTML += '<div class="chat-hour">';
chattingHTML += timeSet();
chattingHTML += '<span class="fa fa-check-circle"></span>';
chattingHTML += '</div>';
chattingHTML += '<div class="chat-text">';
chattingHTML += data.msg;
chattingHTML += '</div>';
chattingHTML += '<div class="chat-avatar">';
/*chattingHTML += '<img src="" alt="Retail Admin" />';*/
chattingHTML += '<div class="chat-name">';
chattingHTML += data.chatNickName;
chattingHTML += '</div>';
chattingHTML += '</div>';
chattingHTML += '</li>';
$("#chatting").append(chattingHTML);
} else { // 내가 아니라면
chattingHTML += '<li class="chat-left">';
chattingHTML += '<div class="chat-avatar">';
/*chattingHTML += ' <img src="" alt="Retail Admin">';*/
chattingHTML += '<div class="chat-name">';
chattingHTML += data.chatNickName;
chattingHTML += '</div>'
chattingHTML += '</div>';
chattingHTML += '<div class="chat-text" style="font-size: 12px; height: fit-content;">';
chattingHTML += data.msg;
chattingHTML += '</div>';
chattingHTML += '<div class="chat-hour">'
chattingHTML += timeSet();
chattingHTML += '<span class="fa fa-check-circle"></span>';
chattingHTML += '</div>';
chattingHTML += '</li>';
$("#chatting").append(chattingHTML);
}
}
// 버튼 숨기기
$("#startBtn").hide();
$("#createBtn").hide();
};
정상적으로 소켓 통신이 완료되면, 위 코드의 webSocket.onmessage 함수가 호출 됩니다. 해당 결과 값에 따라 원하는 로직을 구현 해 주시면 됩니다.
테스트 결과
1. PC
2. PC