본문 바로가기
웹/Spring

[Web Socket / Spring] 실시간 채팅 기능 구현

by Gnaseel 2020. 1. 12.
728x90
반응형

http통신의 특징과 한계

http통신은 HyperText Transfer Plotocol의 약자로서 오늘날 광범위하고 일반적으로 사용되는 통신 기법이다.
http의 기본 원리는

 

  1. client가 server에게 자신이 받고싶은 정보를 request에 담아 전송한다.
  2. server는 client의 request에 따라서 알맞은 response로 응답한다.
  3. client는 server에게 받은 response의 데이터를 사용한다.
    이다.
    즉, client가 자신이 어떤 데이터를 받고싶은지 server에 요청을 해야, server가 그 요청에 맞는 데이터를 제공해 주는 방식이다.

그래서 http통신의 가장 큰 특징 중 Stateless, Connectionless라는 특징이 있다.
client가 server에 request를 보내면, 서버는 클라이언트에게 response를 하고, 그 이후 연결이 끊어져 서버와 클라이언트는 독립된 상태를 유지하는 것이다.

웹 소켓을 사용해야 하는 이유

하지만 실시간 채팅 기능을 구현하기 위해서는 http통신의 기존 방법으로는 한계가 있다.
우선 실시간 채팅이 이루어지려면 어떤 절차를 밟아야 하는지 살펴보자.

 

  1. client1이 서버에 메세지를 전송하고,
  2. server는 그 메세지를 client2에게 전송하고,
  3. client2는 자신에게 온 메세지를 확인한다.

3단계로 이루어진 아주 간단한 과정이지만 여기서 하나의 문제가 있다.
기본적으로 http통신이란 client가 요청을 해야 server가 응답하는 방식이고,
server가 client2에게 메세지가 도착했다는 것을 알리려 해도, client2에게서 request가 없었기 때문에
server가 일방적으로 response를 할 수 없는 것이다.

client2의 입장에서 생각해 봐도 자신의 독립된 환경에서 메세지가 도착했는지, 안했는지 판단할 수 없기때문에
server에 자신에게 온 메세지의 데이터를 전송해달라고 request를 보낼 수가 없다.

물론 기존 방식으로 해결하고자 하면, 임시방편으로나마 해결할 수 있다.

 

  1. ajax를 사용한 비동기적 통신을 통해 주기적으로 한 페이지 안에서 server한테 자신에게 보낼 정보가 있는지 request한다.
  2. 페이지가 이동될 때마다 자신에게 온 정보가 있는지에 대한 질문을 request에 포함시킨다.

하지만 이 두가지 방법 다 문제가 있는데
1번은 ajax를 주기적으로 사용해야하고, 계속된 요청으로 서버에 무리가 가며, 실시간도 아니다.
2번은 자신이 페이지를 이동 할 때만 묻기 때문에, 채팅이라고 하기에는 문제가 있다.
(실제로 1번은 polling이란 방법으로 웹 소켓이 나오기 이전 http통신의 한계를 극복하는데 잠시 쓰였던 역사가 있다.)

이 단점을 보완하기 위해 등장한 것이 웹 소켓이다.
웹 소켓은 IETF에 의해 RFC 6455로 표준화된 엄연한 표준 기술이다.
웹 소켓은 http와 다르게 전 이중 통신을 지원하기 때문에, clinet의 요청이 없어도 server에서 먼저 client에게 정보를 전송한다.
그렇기에 우리는 실시간 채팅 기능을 구현하기 위해서 웹 소켓을 사용해야 하는 것이다.

실제 코드

1. 의존성 추가

(pom.xml)

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-websocket</artifactId>
        <version>${org.springframework-version}</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson.version}</version>
    </dependency>

우선 의존성을 추가해야한다.
spring-websocket은 spring에서 웹 소켓 기능을 사용할 수 있게 해주고, jackson은 json 관련 라이브러리인데, 웹 소켓에서 json형식으로 데이터를 주고받기 때문에 추가해야한다.

 

2. bean객체 등록 및 xsi스키마 수정

(servlet-context.xml)

 

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
	xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	(바뀐line)xmlns:websocket="http://www.springframework.org/schema/websocket"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
		(바뀐line)http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">

	***생략***
    <websocket:handlers>
		<websocket:mapping handler="echoHandler" path="/echo" />
		<websocket:sockjs />
	</websocket:handlers>
    
	<beans:bean id="echoHandler" class="com.sp.ex.EchoHandler"></beans:bean>

추가된 부분만 작성했다.
여기서 중요한 것은 handler="echoHandler"와 id = "echoHandler"부분을 일치시켜 맵핑해 주는 것이고,
class="com.sp.ex.EchoHandler" 부분의 class는 실제적으로 어떤 클래스에서 웹소켓을 컨트롤할 것인지에 대한 명세이기 때문에
각자 프로젝트에 맞춰서 기입해야 한다는 것이다. 복붙하되 class 경로는 개인의 프로젝트에 따라 알맞게 수정해주자.
tmi일수도 있겠지만, 내가 사용한 경로는 com.sp.ex 라는 패키지 안의 EchoHandler.java 라는 파일에서 컨트롤 하겠다는 뜻이다.

 

3. EchoHandler 클래스 작성

 

package com.sp.ex;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@RequestMapping("/echo")
public class EchoHandler extends TextWebSocketHandler{
    //세션 리스트
    private List<WebSocketSession> sessionList = new ArrayList<WebSocketSession>();

    private static Logger logger = LoggerFactory.getLogger(EchoHandler.class);

    //클라이언트가 연결 되었을 때 실행
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessionList.add(session);
        logger.info("{} 연결됨", session.getId()); 
    }

    //클라이언트가 웹소켓 서버로 메시지를 전송했을 때 실행
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        logger.info("{}로 부터 {} 받음", session.getId(), message.getPayload());
        //모든 유저에게 메세지 출력
        for(WebSocketSession sess : sessionList){
            sess.sendMessage(new TextMessage(message.getPayload()));
        }

    //클라이언트 연결을 끊었을 때 실행
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessionList.remove(session);
        logger.info("{} 연결 끊김.", session.getId());
    }
}

}

중요한점은 TextWebSocketHandler 클래스를 상속받아 사용한다는 것이다.
클라이언트가 접속할 때, 메세지를 보낼 때, 접속을 끊었을 때 각각 override된 메소드가 동작한다.

 

3. 뷰 페이지 작성

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.js"></script>
<script type="text/javascript"
	src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.5/sockjs.min.js"></script>

</head>
<body>
	<input type="text" id="message" />
	<input type="button" id="sendBtn" value="submit"/>
	<div id="messageArea"></div>
</body>
<script type="text/javascript">
	$("#sendBtn").click(function() {
		sendMessage();
		$('#message').val('')
	});

	let sock = new SockJS("http://localhost:8220/ex/echo/");
	sock.onmessage = onMessage;
	sock.onclose = onClose;
	// 메시지 전송
	function sendMessage() {
		sock.send($("#message").val());
	}
	// 서버로부터 메시지를 받았을 때
	function onMessage(msg) {
		var data = msg.data;
		$("#messageArea").append(data + "<br/>");
	}
	// 서버와 연결을 끊었을 때
	function onClose(evt) {
		$("#messageArea").append("연결 끊김");

	}
</script>
</html>

웹소켓을 공부하는 사람이면 기본적인 js문법이나 jquery의 사용법은 이해하고 있다고 가정하고 설명을 생략하겠다.

우선 중요한 부분은

<script type="text/javascript"
	src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.5/sockjs.min.js"></script>

부분으로 sockjs 라이브러리를 추가해주는 코드이다.

이 부분이 있어야 script부분에서 sockjs 관련 함수를 사용할 수 있다.

let sock = new SockJS("http://localhost:8220/ex/echo/");
	sock.onmessage = onMessage;
	sock.onclose = onClose;

그리고 SockJS객체를 생성하고, 그 객체가 메세지를 받고, 연결이 끊길 때 각각 어떤 함수를 호출할건지 세팅해주는 과정이다. constructor의 매개변수에는 자신의 url과 EchoHandler를 맵핑한 주소를 적어주면 된다.

 

 

 

 

5. 실제 동작

하나는 이클립스 내부에서, 하나는 외부 크롬에서 메세지를 작성했고,
각각의 메세지가 상대방의 클라이언트에 전송이 되는 것을 볼 수 있다.

6. 응용

웹 소켓의 기능을 활용한다면 실시간 채팅 이외에도

게시판 프로젝트에서 댓글 알람, 또는 사이트 내부 메신저에도 활용할 수 있다.

적극적으로 자신의 프로젝트에 활용해보자.

반응형

' > Spring' 카테고리의 다른 글

[Spring ]WebSocketSession 에서 HttpSession 값 사용하는 방법  (4) 2020.01.12
Spring과 Oracle 연동하기  (0) 2019.11.25

댓글22

  • 123qwe 2020.04.28 14:35

    글 잘 봤습니다!
    그런데 위에 코드를 똑같이 작성했는데 화면에 들어가면 바로 연결끊김이 떠버리는데 뭐가 잘못된건지 알 수 있을까요?
    답글

    • Gnaseel 2020.04.28 16:13 신고

      화면에 들어갔다와 연결끊김이 떴다는 정보만으로는 어디에 문제가 생겼는지 알 수 없습니다.

      최소한 어느 부분에서, 어떤 오류 로그를 띄우며 문제가 발생하는 것인지 알아야 문제를 유추할 수 있습니다.

    • aa 2020.06.20 23:05

      저도 이렇게 뜨네요 ㅜㅜ..
      채팅화면에 들어가자마자 연결끊김으로 떠요

    • aa 2020.06.20 23:13

      오류 발생하는게 아니고 바로 연결이 끊깁니다..

    • Gnaseel 2020.06.21 00:08 신고

      그 연결끊김이 뜬다는게 어떤 의미인가요?
      웹소켓의 서버부분에서 끊겼다는 콘솔로그가 출력되나요?
      클라이언트의 onClose이 실행되나요?

      그리고 끊김 이전에 웹소켓을 연결할 때 실행되는 함수는 실행 되나요?
      자세히 상황을 적어주시면 한번 살펴보겠습니다.

    • aa 2020.06.25 16:26

      콘솔로그에 출력이 아무것도 안돼요
      웹소켓 연결할때 함수가 실행이 안되는 것 같은데 ㅜㅜ..

    • Gnaseel 2020.06.27 15:14 신고

      그렇다면 모두 지우고 처음부터 다시 진행하는 것을 추천드립니다.

  • 코딩 2020.05.01 13:09

    안녕하세요. 질문 하나드려도 될까요?
    let sock = new SockJS("https://localhost:8220/ex/echo/"); 이부분에서 url 적는 칸에 localhost:포트번호까지 적고 그 후 ex/echo 에서 echo는 handler:mapping 에 path를 적어주는거 같은데 ex는 무엇인가요?? 기존 프로젝트에 추가하는거여서 경로가 조금 달라 어떤걸 적어주어야 할지 모르겠네요ㅠ
    답글

  • 코딩 2020.05.01 14:13

    혹시 경로를 확인하는법이 있을까요? 생략 하고 바로 매핑 이름을 써줘도 에러가 떠서요.. 주소가 잘못된거 같은데 확인 할 방법이 없나요..?
    답글

  • HJ 2020.07.09 00:36

    저는 연결끊김 문제가 localhost로 되어있다보니 해당PC에선 웹소켓 접속이 되지만 포트포워딩시 외부PC에서는 접속이 안되어 바로 연결끊김이 뜨는 문제였는데 스크립트단에
    let sock = new SockJS("https://localhost:8220/ex/echo/")부분을
    let sock = new SockJS("http://해당PC의IP주소:8220/ex/echo/")
    이런식으로 바꿔주니 외부PC에서 접속이 되고 양방향으로 통신이 잘되네요
    답글

  • hs 2020.08.20 09:58

    db에 테이블은 따로 사용안해도 되는건가요? 만약 사용하면 로직좀 알려주세요 ㅠㅠ
    답글

  • 좋은정보 감사합니다. 2020.09.18 22:55

    좋은글 잘읽고 갑니다~

    날로먹으려고 하시는분들이 많네요~
    답글

  • 감사합니다!
    답글

  • 지은 2020.10.13 12:01

    안녕하세요ㅠㅜ 이거 해봣는데.. Welcome to SockJS! 만 뜨고 input 있는 곳은 안들어가지던데.. 어떻게 해야하나요ㅠㅜ
    답글

    • 북신동루니 2022.09.06 00:22

      requestMapping으로 url을 받아올 수 있는 컨트롤러 메서드가 필요한 것 같아요.

      게시글 올려두신 EchoHandler클래스는 웹소켓 컨트롤러네요

  • 진호 2021.04.24 14:20

    잘봤습니다
    일단 연결끓김으로 뜨는 이유 찾다 보니깐 경로를 정상적으로 했다면
    web을 처음 설정해서 스프링에 적용이 덜된 거라서 그랬더라구요
    그래서 project Explorer쪽 보시면
    JAX-WS-WEB -Services 이거 없으면 연결끓김으로 나와요 저는 컴을 껏다 키니 적용 되더라 구요이후 톰켓치 에러 뜨던데
    이 문제는 별도 이긴한데 탐켓치를 리무브 하고 다시 설정하니깐 되더라구요
    답글

  • 익명 2021.05.24 19:55

    비밀댓글입니다
    답글

  • 봉봉 2021.06.29 10:50

    연결 끊김이 있어서 뭔가 계속 찾았는데ㅠㅠㅠ
    let sock = new SockJS("https://localhost:8220/ex/echo/");
    포트번호를 제걸로 안해서 그런거였네요 ㅠㅠㅠ
    댓글에도 힌트가 있었는데 흑흑

    이 예제 너무 도움이 됬어요!! 감사합니다!!!
    답글

  • 카카 2022.03.24 14:21

    보고 5분만에 이해되었습니다. 고마웡요!! 실습도 성공했네요. 패키지 구조만 알고 개발자 도구 콘솔만 잘 봐도 금방 할 수 있었어요. !!
    답글

  • Leenima 2022.04.01 14:42

    혹시 이 jsp 화면을 http://localhost:8080/chat.do 로 들어간다면
    let sock = new SockJS("http://localhost:8080/chat.do/echo/");
    라고 해줘야하나요?
    그렇다면 핸들러에서
    @RequestMapping("/chat.do/echo/")
    로 해줘야하나요? 아니면 그대로 놔두고 써야하나요??
    기초가 부족해서 헷갈리네요 ㅠㅠ
    답글