Back-End/Spring

Spring Boot, Java Web Socket을 활용한 웹 채팅 프로그램 만들기(HTML5, JavaScript, JQuery)

개발자 DalBy 2024. 5. 14. 16:58
반응형

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

PC 1에서 PC2와 대화
PC 1에서 PC2와 대화

 

2. PC

PC 2에서 PC1과 대화
PC 2에서 PC1과 대화

 

 

 

 



반응형