본문 바로가기
ETC/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. 응용

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

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

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

반응형

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

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