[Java] WebSocket에서 채팅 이력을 로딩하는 방법


Development note/Java  2021. 6. 15. 14:51

안녕하세요. 명월입니다.


이 글은 Java의 WebSocket에서 채팅 이력을 로딩하는 방법에 대한 글입니다.


이전 글에서 어떤 분이 채팅방을 구현하는데 이전에 채팅했던 내용이 로드하는 방법에 대해 질문한 글이 있어서 작성했습니다.

링크 - [Java] Websocket을 이용해서 유저(사이트 운영자)가 다른 유저와 채팅하는 방법

다른 사양적인 내용은 중요하지 않고 먼저 유저의 name값을 가져와서 채팅 내용을 db에 저장했다가 다시 로드했을 때, 읽어 오고 싶은 예제에 대해서 질문을 하셨습니다.


우리가 자주 사용하는 스마트폰에서 카카오톡등의 채팅 내용은 일단 서버에 어느 정도 저장을 하겠지만, 기본적으로 로컬(스마트폰 내부)에 저장을 하게 됩니다..

물론 PC나 다른 폰에서도 동기화하기 위해서는 카카오톡 서버에도 저장을 하겠네요.


그러나 웹은 기본적으로 브라우저 정책으로 로컬 파일을 조작(저장 및 읽기, 쓰기)가 제한되어 있습니다. 이게 된다면 웹 사이트에 접속하는 것만으로 내부 시스템을 다 조작할 수 있다는 말이기 때문에 보안 취약성에 구멍이 생기겠네요.

그렇다면 브라우저에서 채팅한 내용은 서버에 저장을 해야합니다.


채팅 내용을 저장하는 방법은 여러가지가 있습니다만, 개인적으로는 데이터베이스에 전부 저장하는 것보다는 내용은 파일로 저장하는 것도 나쁘지는 않다고 생각합니다.

여기서 작성한 예제는 채팅 내용을 파일로 저장했습니다만, 질문하신 분처럼 DB에 넣고 싶으면 파일로 저장하고 읽어오는 부분을 대신 DB의 insert, update로 저장하고 select로 읽어오면 될꺼 같습니다.


먼저 서버와 클라이언트(브라우저)와 복합적인 데이터를 주고 받기 위해서는 JSON타입의 데이터를 주고 받는게 편합니다.

그래서 pom.xml에 자바에서 사용하는 JSON 파싱 라이브러리를 연결하겠습니다.

링크 - [Java] Gson을 이용한 Json 다루기

<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.8.7</version>
</dependency>

그리고 브라우저에서 사용될 html과 javascript 소스입니다. 참고로 편의를 위해 Jquery 라이브러리를 사용했습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Web Socket Example</title>
<style>
// 에러 메시지 css
.error-message {
  color: red;
}
</style>
</head>
<body>
  <!-- 로그인 실패할 때 표시되는 에러 메시지 -->
  <p>
    <span class="error-message"></span>
  </p>
  <!-- 아이디를 넣고 login 버튼을 누르면 name이 저장된다. -->
  <p>
    login : <input type="text" id="username" name="username" class="login-package">
    <button id="loginBtn" class="login-package">login</button>
  </p>
  <!-- 채팅시에 사용될 채팅 textbox과 버튼(초기는 disabled) -->
  <p>
    <input id="textMessage" type="text" disabled="disabled" class="chat-package">
    <button id="sendBtn" disabled="disabled" class="chat-package">Send</button>
  </p>
  <!-- 채팅 내용이 적혀질 채팅 내용 박스 -->
  <p>
    <textarea id="messageTextArea" rows="10" cols="50" disabled="disabled"></textarea>
  </p>
  <!-- Jquery 라이브러리 링크 -->
  <script src="//code.jquery.com/jquery-3.6.0.min.js"></script>
  <script type="text/javascript">
    // Javascript 캡슐화
    (function(_) {
    })((function() {
      // WebSocket 변수
      let webSocket = null;
      // WebSocket 접속 함수
      function connect() {
        // 웹 소켓 객체 생성
        webSocket = new WebSocket("ws://localhost:8080/ChatExample/chat");
        // 웹 소켓이 open되면 실행되는 이벤트
        webSocket.onopen = function(message) {
          // 채팅 textbox와 버튼의 disabled 해제
          $(".chat-package").attr("disabled", false);
          // 접속 초기 데이터 작성
          let key = {
            id : $("#username").val(),
            state : 0 // state는 0
          };
          // 접속 메시지 보내기
          webSocket.send(JSON.stringify(key));
        };
        webSocket.onclose = function(message) {
        };
        webSocket.onerror = function(message) {
        };
        // 서버로 부터 메시지가 오면 실행되는 이벤트
        webSocket.onmessage = function(message) {
          // 채팅 내용 박스 객체 취득
          let messageTextArea = document.getElementById("messageTextArea");
          // 데이터 작성
          messageTextArea.value += message.data;
        };
      }
      // 메시지 보내기 함수
      function sendMessage() {
        // 텍스트 박스 객체 취득
        let message = document.getElementById("textMessage");
        // 메시지 전송 객체 만들기
        let key = {
          id : $("#username").val(),
          state : 1, // state는 1
          value : message.value // 메시지 내용
        }
        // 메시지 보내기
        webSocket.send(JSON.stringify(key));
        // 텍스트 박스 초기화
        message.value = "";
      }
      // 로그인 버튼 이벤트
      $("#loginBtn").on("click", function() {
        // 아이디 텍스트 박스에 내용이 없으면
        if ($.trim($("#username").val()) === '') {
          // 에러 표시
          $(".error-message").html("Please input the name textbox.");
          // 함수 종료
          return false;
        }
        // 에러 메시지 삭제
        $(".error-message").html("");
        // 로그인 텍스트 박스와 버튼 disabled
        $(".login-package").attr("disabled", "disabled");
        // 접속
        connect();
      });
      // 채팅 버튼 누르면 메시지 보내기
      $("#sendBtn").on("click", sendMessage);
      // 텍스트 박스에 키가 눌리면 enter함수 실행
      $("#textMessage").on("keydown", enter);
      // 텍스트 박스에서 엔터키가 오면 메시지 전송
      function enter() {
        if (event.keyCode === 13) {
          sendMessage();
          return false;
        }
        return true;
      }
      return {};
    })());
  </script>
</body>
</html>
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import com.google.gson.Gson;
// ws://호스트/chat 접속시
@ServerEndpoint("/chat")
public class Chat {
  // 인라인 클래스 메시지 타입 클래스
  class ChatMessage {
    // id
    private String id;
    // state
    private int state;
    // 내용
    private String value;
    // getter, setter
    public String getId() {
      return id;
    }
    public void setId(String id) {
      this.id = id;
    }
    public int getState() {
      return state;
    }
    public void setState(int state) {
      this.state = state;
    }
    public String getValue() {
      return value;
    }
    public void setValue(String value) {
      this.value = value;
    }
  }
  // 세션과 id를 가지고 있는 세션
  class ChatSession {
    // WebSocket 세션
    private Session session;
    // id
    private String id;
    // getter, setter
    public Session getSession() {
      return session;
    }
    public void setSession(Session session) {
      this.session = session;
    }
    public String getId() {
      return id;
    }
    public void setId(String id) {
      this.id = id;
    }
  }
  // 유저 리스트
  private static List<ChatSession> users = new LinkedList<>();
  // JSON을 파싱하는 클래스
  private Gson gson = new Gson();
  // WebSocket session으로 ChatSession을 탐색하는 함수
  private ChatSession getSession(Session userSession) {
    // session 클래스와 ChatSession 클래스의 session 변수를 비교해서 탐색해 온다.
    Optional<ChatSession> data = users.stream().filter(x -> x.getSession() == userSession).findFirst();
    // 데이터가 있으면
    if (data.isPresent()) {
      // 리턴
      return data.get();
    }
    // 없으면 null
    return null;
  }
  // 세션 만들기
  private ChatSession createSession(ChatMessage msg, Session userSession) {
    // 먼저 기존 세션에 있는지 확인
    ChatSession session = getSession(userSession);
    // 세션이 없으면
    if (session == null) {
      // 인스턴스 생성
      session = new ChatSession();
      // 세션 저장
      session.setSession(userSession);
      // user 리스트에 추가
      users.add(session);
    }
    // id 저장
    session.setId(msg.getId());
    // session 리턴
    return session;
  }
  // WebSocket이 접속될 때 호출되는 함수
  @OnOpen
  public void handleOpen(Session userSession) {
  }
  // 브라우저로부터 메시지가 오면 호출되는 함수
  @OnMessage
  public void handleMessage(String message, Session userSession) {
    // 메시지가 JSON 타임으로 오는데 ChatMessage 클래스로 변환
    ChatMessage msg = gson.fromJson(message, ChatMessage.class);
    // State가 0이라면 초기 접속
    if (msg.getState() == 0) {
      // 세션 만들기
      createSession(msg, userSession);
      try {
        // 파일로부터 채팅 내용을 읽어와서 보내기
        userSession.getBasicRemote().sendText(readFile());
      } catch (Throwable e) {
        // 에러가 발생할 경우.
        e.printStackTrace();
      }
    // State가 1이라면 일반 메시지
    } else if (msg.getState() == 1) {
      // 세션 확인 하기
      if (getSession(userSession) != null) {
        // 메시지 보내기
        sendMessage(msg.getId(), msg.getValue());
        // 파일에 저장하기
        saveFile(msg.getId(), msg.getValue());
      }
    }
  }
  // 채팅 내용을 파일로 부터 읽어온다.
  private String readFile() {
    // d드라이브의 chat 폴더의 chat 파일
    File file = new File("d:\\chat\\chat");
    // 파일 있는지 검사
    if (!file.exists()) {
      return "";
    }
    // 파일을 읽어온다.
    try (FileInputStream stream = new FileInputStream(file)) {
      return new String(stream.readAllBytes());
    } catch (Throwable e) {
      e.printStackTrace();
      return "";
    }
  }
  // 파일를 저장하는 함수
  private void saveFile(String id, String message) {
    // 메시지 내용
    String msg = id + "]  " + message + "\n";
    // 파일을 저장한다.
    try (FileOutputStream stream = new FileOutputStream("d:\\chat\\chat", true)) {
      stream.write(msg.getBytes("UTF-8"));
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
  // 메시지 보내기는 함수
  private void sendMessage(String id, String message) {
    // 메시지 내용
    String sendMessage = id + "]  " + message + "\n";
    for (ChatSession user : users) {
      try {
        // 메시지 전송
        user.getSession().getBasicRemote().sendText(sendMessage);
      } catch (Throwable e) {
        e.printStackTrace();
      }
    }
  }
  // WebSocket이 닫기면 호출되는 함수
  @OnClose
  public void handleClose(Session userSession) {
    // session으로 users에서 찾는다.
    Optional<ChatSession> session = users.stream().filter(x -> x.getSession() == userSession).findFirst();
    // 있으면 삭제
    if (session.isPresent()) {
      users.remove(session.get());
    }
  }
}

디버그하고 실행하겠습니다.

실행화면 화면에 login 텍스트 박스와 버튼이 활성화되어 있고 message 박스와 Send 버튼은 비활성화 되어 있습니다.

test를 넣고 로그인합니다.

그리고 aaaaa라는 메시지를 넣으면 채팅창에 aaaaa라는 메시지가 표시됩니다.

다른 브라우저에서 다시 해당 url를 접속합니다.

이번에는 test1로 넣고 로그인합니다.

그러면 test에서 작성한 내용이 표시가 됩니다.

bbbbb라는 메시지를 넣고 보냅니다.

이번엔 다시 test가 있는 브라우저 창을 확인합니다.

서로간에 채팅은 문제없이 되는 것을 확인했습니다.

이번에는 chat 파일의 데이터가 제대로 작성되는지 확인합니다.

일단 사양대로 실행은 됩니다.


질문은 예젠에 채팅 이력을 표시하기를 원하는 사양이었습니다만, 아마 채팅방 개념을 만들면 약간 내용이 달라질 듯 싶습니다.

채팅방을 만들면 채팅방의 키를 작성하고 그 키로 파일을 읽고 표시하는 것으로 채팅방별로 이력을 달리해야하고, 입장하기 전의 내용은 표시않는다라는 내용을 넣으려면 각 메시지마다도 키를 만들어서 표시를 해야합니다. 물론 입장했을 때의 키를 유저가 가지고 있어야 하고 이런저런 사양을 넣으면 약간 복잡해 지겠네요.


여기까지 Java의 WebSocket에서 채팅 이력을 로딩하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.