본문 바로가기
BOOKS/Spring Boot

Spring Boot에서 WebSocket 만들기

by IT여행자 2022. 1. 2.
728x90

안녕하세요 IT여행자입니다.

 

이번 수첩은 spring boot에서 websocket을 사용한 채팅을 구현해 보려고 합니다. spring boot에서 mvc를 구현하기 위한 초기 설정 단계가 좀 복잡해 보여 전체적으로 복잡하고 어려워 보이지만, 핵심적인 내용은 WebSocket을 사용하기 위한 @ServerEndpoint 어노테이션이 붙은 컨트롤 클래스와 WebSocket에 관한 환경 설정 파일이 주된 내용이라 볼 수 있습니다. 

 

spring boot의 mvc 패턴의 설정 과정이 그리 어렵지 않게 느껴지는 독자라면 너무도 쉽게 알 수 있는 내용이며, mvc를 구현해 보지 않은 독자라도 차근차근 따라가면서 작업하면 그리 어렵지 않게 완성할 수 있으며 이를 응용할 수 있을 것입니다.

 

개발 환경

본 문서를 작성할 때 사용한 개발 환경과 반드시 일치하지 않아도 되지만 버전 간 트러블 없이 코드를 완성하고 실행하려면 개발 환경과 최대한 일치시켜 프로그램을 작성하는 것을 권장드립니다. 그리고, 외부 라이브러리는 최대한 적게 사용하였습니다.

 

개발언어 및 툴 설명
java 11 java 8 버전이상이면 됨.
eclipse EE 2021-03 이상.

 

프로젝트 생성

이클립스에 spring suite 4 플러그인을 설치하지 않았다면 이클립스의 marketpalce에 가서 설치해 두고 시작해야 합니다. 

 

이클립스 메뉴 > New > Other > Spring Starter Project

빨간색 동그라미 표시 부분을 유념하면서 프로젝트를 생성해 주시기 바랍니다.

노란색 원의 내용은 임의로 작업하셔도 됩니다. 다만, Package 지정은 프로그램 코드에 영향을 직접 주는 요소 이므로 유념해서 지정해 주시기 바랍니다.

 

프로젝트를 생성할 때 노 란원에 들어 있는 Spring Boot DevTools와 Spring Web Dependency만을 선택하였지만 추가적으로 필요한 Dependency들을 추가해도 상관없습니다.

dependency 추가

프로젝트를 생성할 때 지원하지 않는 websocket 부분과, jsp 페이지를 위한 tomcat-embed-jsper를 추가하도록 하겠습니다.

 

<dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
	<version>2.6.2</version>
</dependency>

전체 pom.xml은 아래와 같습니다.

 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>kr.jobtc</groupId>
	<artifactId>Spring-boot-web-socket</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>SpringBootWebSocket</name>
	<description>spring boot web socket</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
			<version>2.6.2</version>
		</dependency>
		
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

MVC 설정

MVC 패턴을 사용하기 위한 간단한 설정만을 application.properties에 지정합니다.

 

path : src/main/resources/application.properties

# 서버 포트
server.port=9999 

# html, js, css, mapper들의 위치
spring.mvc.static-locations=/resources/**

# servlet의 접두사와 접미사
spring.mvc.view.prefix=/WEB-INF/chatt/
spring.mvc.view.suffix=.jsp

# 서버 자동 재가동
spring.devtools.livereload.enabled=true
spring.freemarker.cache=false

 

 WebSocketChatt 만들기

 

클라이언트가 접속할 때마다 객체가 생성되어 클라이언트의 메시지를 수신받고 접속된 모든 클라이언트에게 메시지를 전송하는 기능을 갖고 있는 클래스입니다.

step 1.

백그라운드로 돌아가는 빈을 등록하기 위해 @Service 어노테이션과, WebSocket의 연결점을 알려주는 @ServerEndoint 어노테이션을 지정한 클래스를 생성합니다.  @ServerEndpoint는 WebSocket을 활성화시키는 매핑 정보를 지정합니다.

 

package kr.jobtc.chatt;

import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Service;

@Service
@ServerEndpoint(value="/chatt")
public class WebSocketChatt {
    … 
}

 

step 2.

위에서 지정한 클래스 WebSocketChatt는 클라이언트가 접속할 때마다 생성되어 클라이언트와 직접 통신하는 클래스입니다. 따라서 새로운 클라이언트가 접속할 때마다 클라이언트의 세션 관련 정보를 정적형으로 저장하여 1:N의 통신이 가능하도록 만들어야 합니다.

 

package kr.jobtc.chatt;

import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Service;

@Service
@ServerEndpoint(value="/chatt")
public class WebSocketChatt {
	private static Set<Session> clients = 
			Collections.synchronizedSet(new HashSet<Session>());

}

 

step 3.

클라이언트의 접속, 메시지 수신, 접속 해제에 따른 이벤트 핸들러를 어노테이션과 함께 메서드를 정의합니다.

 

어노테이션 설 명
@OnOpen 클라이어트가 접속할 때 발생하는 이벤트
@OnClose 클라이언트가 브라우저를 끄거나 다른 경로로 이동할 때
@OnMessage 메시지가 수신되었을 때
package kr.jobtc.chatt;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.springframework.stereotype.Service;

@Service
@ServerEndpoint(value="/chatt")
public class WebSocketChatt {
	private static Set<Session> clients = 
			Collections.synchronizedSet(new HashSet<Session>());

	@OnMessage
	public void onMessage(String msg, Session session) throws Exception{
		
	}
	
	@OnOpen
	public void onOpen(Session s) {

	}
	
	@OnClose
	public void onClose(Session s) {

	}
}

 

step 4.

1) onOpen 메서드
클라이언트가 ServerEndpoint값인 “/chatt “ url로 서버에 접속하게 되면 onOpen 메서드가 실행되며, 클라이언트 정보를 매개변수인 Session 객체를 통해 전달받습니다. 이때 정적 필드인 clients에 해당 session이 존재하지 않으면 clients에 접속된 클라이언트를 추가합니다.

2) onMessage 메서드
클라이언트로부터 메시지가 전달되면 WebSocketChatt 클래스의 onMessage메서드에 의해  clients에 있는 모든 session에 메시지를 전달합니다.

3) onClose 메서드
클라이언트가 url을 바꾸거나 브라우저를 종료하면 자동으로 onClose() 메서드가 실행되며 해당 클라이언트 정보를 clients에서 제거합니다.

 

package kr.jobtc.chatt;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.springframework.stereotype.Service;

@Service
@ServerEndpoint(value="/chatt")
public class WebSocketChatt {
	private static Set<Session> clients = 
			Collections.synchronizedSet(new HashSet<Session>());

	
	@OnOpen
	public void onOpen(Session s) {
		System.out.println("open session : " + s.toString());
		if(!clients.contains(s)) {
			clients.add(s);
			System.out.println("session open : " + s);
		}else {
			System.out.println("이미 연결된 session 임!!!");
		}
	}
	
	
	@OnMessage
	public void onMessage(String msg, Session session) throws Exception{
		System.out.println("receive message : " + msg);
		for(Session s : clients) {
			System.out.println("send data : " + msg);
			s.getBasicRemote().sendText(msg);

		}
		
	}
	
	@OnClose
	public void onClose(Session s) {
		System.out.println("session close : " + s);
		clients.remove(s);
	}
}

 

WebSocketCofig 만들기

일반적으로 스프링에서 빈들은 싱글톤으로 관리되지만,  @ServerEndpoint 어노테이션이 달린 클래스들은 WebSocket이 생성될 때마다 인스턴스가 생성되고 JWA에 의해 관리되기 때문에 스프링의  @Autowired가 설정된 멤버들이 정상적으로 최기화 되지 않습니다. 이때 이를 연결해 주고 초기화해 주는 클래스가 필요합니다.

 

package kr.jobtc;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Component
public class WebSocketConfig {

		@Bean
		public ServerEndpointExporter serverEndpointExporter() {
			return new ServerEndpointExporter();
		}
}

 

Controller에 등록

브라우저 url에 “/mychatt”으로 매팅 정보가 들어오면 ‘WEB-INF/chatt/client.jsp” 페이지를 보여주도록 MVC 패턴의 컨트롤을 작성합니다.

 

package kr.jobtc.chatt;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

@RestController
public class ChattController {
	
	@RequestMapping("/mychatt")
	public ModelAndView chatt() {
		ModelAndView mv = new ModelAndView();
		mv.setViewName("chatting");
		return mv;
	}

}

 

Web Page 및 JavaScript

위에서 만든 ChattController에 의해 불려질 웹페이지와 웹 페이지에서 사용되는 javascript, css를 static 폴더에 작성합니다. 파일을 만들기 전에 3개의 폴더를 따로 만들어 주십시오.

 

webapp/WEB-INF/chatt jsp 페이지 경로
resources/stqtic/js javascript 파일 경로
resource/static/css css 파일 경로

 

path : webapp/WEB-INF/chatt/chatting.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel='stylesheet' type='text/css' href='./css/chatt.css'>
</head>
<body>
	<div id='chatt'>
		<h1>WebSocket Chatting</h1>
		<input type='text' id='mid' value='홍길동'>
		<input type='button' value='로그인' id='btnLogin'>
		<br/>
		<div id='talk'></div>
		<div id='sendZone'>
			<textarea id='msg' value='hi...' ></textarea>
			<input type='button' value='전송' id='btnSend'>
		</div>
	</div>
	<script src='./js/chatt.js'></script>
</body>
</html>

 

[javascript]

 

path : resources/static/js/chatt.js

/**
 * web socket
 */

function getId(id){
	return document.getElementById(id);
}

var data = {};//전송 데이터(JSON)

var ws ;
var mid = getId('mid');
var btnLogin = getId('btnLogin');
var btnSend = getId('btnSend');
var talk = getId('talk');
var msg = getId('msg');

btnLogin.onclick = function(){
	ws = new WebSocket("ws://" + location.host + "/chatt");
	
	ws.onmessage = function(msg){
		var data = JSON.parse(msg.data);
		var css;
		
		if(data.mid == mid.value){
			css = 'class=me';
		}else{
			css = 'class=other';
		}
		
		var item = `<div ${css} >
		                <span><b>${data.mid}</b></span> [ ${data.date} ]<br/>
                      <span>${data.msg}</span>
						</div>`;
					
		talk.innerHTML += item;
		talk.scrollTop=talk.scrollHeight;//스크롤바 하단으로 이동
	}
}

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

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

function send(){
	if(msg.value.trim() != ''){
		data.mid = getId('mid').value;
		data.msg = msg.value;
		data.date = new Date().toLocaleString();
		var temp = JSON.stringify(data);
		ws.send(temp);
	}
	msg.value ='';
	
}

 

[css]

 

path : resources/static/css/chatt.css

@charset "UTF-8";

*{
	box-sizing: border-box;
}

#chatt{
	width: 800px;
	margin: 20px auto;
}

#chatt #talk{
	width: 800px;
	height: 400px;
	overflow: scroll;
	border : 1px solid #aaa;
}
#chatt #msg{
	width: 740px;
	height:100px;
	display: inline-block;
}

#chatt #sendZone > *{
	vertical-align: top;
	
}
#chatt #btnSend{
	width: 54px;
	height: 100px;
}

#chatt #talk div{
	width: 70%;
	display: inline-block;
	padding: 6px;
	border-radius:10px;
	
}

#chatt .me{
	background-color : #ffc;
	margin : 1px 0px 2px 30%;	
}

#chatt .other{
	background-color : #eee;
	margin : 2px;
}

 

[실행결과]

 

UI는 최대한 간단하게 처리했습니다. 내가 쓴 내용은 오른쪽 배치에 노란색 바탕, 상대방이 쓴 내용은 회색 바탕에 왼쪽 정렬 되도록만 처리하였습니다.

 

UI를 좀 더 다듬어 보면 카*톡과 같은 느낌이나 다른 일반적인 채팅 프로그램들과 같은 느낌을 구현할 수 있을 것입니다.

 

이상 IT여행자였습니다.