본문 바로가기
Projects/SNS프로젝트

[채팅]오픈채팅 구현 과정(이슈&해결)

by 젊은오리 2022. 12. 23.
728x90

지금 진행하고 있는 프로젝트가 SNS기능을 담당하는만큼, 채팅도 가능하게 하면 어떨까 생각했다.

그래서 생각한 것이 오픈채팅인데, 우선은 스프링부트로 채팅서버를 구현하려면 웹소켓에 대한 이해가 필요했다.

 

웹소켓에 대한 이해

웹소켓은 실시간 양방향 데이터 통신이 필요한 경우에 이를 가능케하는 프로토콜이다. 기존의 단방향 HTTP프로토콜과 호환되어서 양방향 통신(full duplex)을 제공하며, 일반 소켓통신과 달리 HTTP 80번 포트를 사용하기에 통상 웹소켓(Websocket)이라고 불린다. 

모든 HTTP 통신에서 클라이언트는 서버에 요청을 보내고, 서버는 그에 맞는 응답을 하며 브라우저와 연결을 끊는다. 이러한 HTTP만으로 원하는 정보를 송수신할 수 있었지만, 클라이언트가 먼저 요청하지 않으면 서버에서 클라이언트에 데이터를 보내주지 않으므로 구현하고자 하는 채팅의 경우에 HTTP는 적합하지 않다.

웹소켓은 클라이언트가 접속요청을 하고 서버가 응답한 후에 연결을 끊는 것이 아닌 connection을 유지하기 때문에 클라이언트의 요청 없이도 데이터를 전송할 수 있게 한다. 웹소켓 통신은 HTTP프로토콜에서 handshaking을 한 후에 HTTP로 동작하지만, HTTP와는 다른 방식으로 통신하게 된다.

 

구현과정

  • 사용자A가 1번 채팅방에 들어오게 되면 ws://localhost:8080/chatroom/1/mychat/A 의 url을 갖는 Websocket객체가 생성되어 서버와 연결을 시도한다.
  • 서버와 연결이 정상적으로 된다면, ChatService의 onOpen메서드가 호출되어 서버에서 Map<1(방번호),사용자 세션>의 형태로 세션을 저장한다.
  • 사용자가 메세지 전송을 하게 되면 ChatService의 onMessage메서드가 호출되어 채팅방에 있는 모든 사용자에게 메세지를 전송한다. (이때, 같은 방에 있는 사람만 채팅이 보여야 하므로 앞서 Map<>의 형태로 방마다 세션을 저장했고, 사용자A가 2번 채팅방에도 들어갈 수 있으므로, 방 입장시마다 별개의 세션이 필요하기 때문에 세션객체를 사용자마다 여러개 가질 수 있도록 했다.) 
  • 사용자가 채팅방을 나간다면 세션을 remove한다.

방 목록

 

채팅방

 

 

view와 css를 생략한 코드는 다음과 같다.

chat.js

//user 정보 불러오기
var principalId = $("#principalId").val();
var principalName = $("#principalName").val();

//채팅방 정보 불러오기
var roomId = $("#roomId").val();

var data = {};
var ws ;

var btnSend = document.getElementById('btnSend');
var talk = document.getElementById('talk');
var msg = document.getElementById('msg');

let url = "ws://" + location.host + "/chatroom/" + roomId + "/mychat/" + principalId;
ws = new WebSocket(url);

ws.onmessage = function(msg){
    var data = JSON.parse(msg.data);
    var css;

    if(data.username == principalName){
        css = 'class=me';
    }else{
        css = 'class=other';
    }

    var item = `<section ${css}>
                    <span class="data-username">${data.username}</span> 
                    <span class="data-date">${data.date}</span><br/>
                    <span>${data.message}</span>
                </section>`;

    talk.innerHTML += item;
    talk.scrollTop=talk.scrollHeight;//스크롤바 하단으로 이동
}

msg.onkeyup = function(ev){
    if(ev.keyCode == 13){
        submit();
    }
}

btnSend.onclick = function(){
    submit();
}

function submit(){
    if(msg.value.trim() == ''){
        alert("한글자 이상을 입력하세요.");
    }else{
        data.username = principalName;
        data.message = msg.value;
        data.date = new Date().toLocaleTimeString();
        var temp = JSON.stringify(data);
        ws.send(temp);
    }
    msg.value ='';
}

 

ChatService

package com.cos.mysns.Service;


import com.cos.mysns.config.webSocket.ServerEndpointConfigurator;
import com.cos.mysns.domain.chatRoom.ChatRoom;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.*;

@Slf4j
@RequiredArgsConstructor
@Service
@ServerEndpoint(value="/chatroom/{roomId}/mychat/{userId}",configurator = ServerEndpointConfigurator.class)
public class ChatService {
    private static Set<Session> sessionSet = Collections.synchronizedSet(new HashSet<Session>());
    private static Map<Integer, ArrayList<Session>> sessionMap = Collections.synchronizedMap(new HashMap<Integer, ArrayList<Session>>());
    private final ChatRoomService chatRoomService;

    @OnOpen
    public void onOpen(Session s, @PathParam("roomId") int roomId, @PathParam("userId") Long userId ) {

        // 방 처음 들어왔으면 해당 방 세션리스트생성
        if(!sessionMap.containsKey(roomId)){
            sessionMap.put(roomId, new ArrayList<>());
        }

        //방에 아무도 없다면 해당 방 세션리스트생성 & 추가, 있다면 추가만
        if(sessionMap.get(roomId).size()==0){
            sessionMap.put(roomId, new ArrayList<>());
            sessionMap.get(roomId).add(s);
        }else{
            sessionMap.get(roomId).add(s);
        }

        if(!sessionSet.contains(s)) {
            sessionSet.add(s);
            log.info("세션 열림 : " + s);
        }else {
            log.info("이미 연결된 세션");
        }

        System.out.println("*******방마다 세션 객체 불러오기*******");
        for(Integer key: sessionMap.keySet()){
            log.info(key+"번방 : ");
            for(int j=0; j<sessionMap.get(key).size();j++){
                log.info(String.valueOf(sessionMap.get(key).get(j)));
            }
        }
    }

    @OnMessage
    public void onMessage(String msg, Session s) throws Exception{
        //사용자가 어느 방에 있는지 찾기
        Integer findkey = -1;
        for(int key: sessionMap.keySet()){
            for(int j=0; j<sessionMap.get(key).size();j++){
                if(sessionMap.get(key).get(j).equals(s)){
                    findkey = key;
                }
            }
        }
        // 방에 있는 세션 모두 tmpSessionSet에 저장
        Set<Session> tmpSessionSet = Collections.synchronizedSet(new HashSet<Session>());
        for(int j=0;j<sessionMap.get(findkey).size();j++){
            tmpSessionSet.add(sessionMap.get(findkey).get(j));
        }

        //같은 방에 있는 사람에게만 보낸다.
        for(Session session : tmpSessionSet) {
            log.info("전송 메세지 : " + msg);
            session.getBasicRemote().sendText(msg);
        }
        log.info("----------------------------");
    }

    @OnClose
    public void onClose(Session s) {
        log.info("세션 닫힘 : " + s);
        sessionSet.remove(s);

        Integer findkey = -1;
        for(int key: sessionMap.keySet()){
            for(int j=0; j<sessionMap.get(key).size();j++){
                if(sessionMap.get(key).get(j).equals(s)){
                    findkey = key;
                }
            }
        }
        sessionMap.get(findkey).remove(s);

        //방인원 -1
        ChatRoom chatRoom = chatRoomService.decreaseExist(findkey);

    }
}

 

 

728x90

댓글