SpringBoot + webSocket 으로 간단한 채팅창 만들기
- 웹 소켓을 활용해 실시간 채팅 기능 구현하기
- 프로젝트에서 사용자들이 모임을 이루어 모임 내에서 실시간 채팅을 할 수 있도록 구현
개념 정리
웹 소켓이란?
RFC 6455 명세서에 정의된 프로토콜인 웹소켓(WebSocket)을 사용하면 서버와 브라우저 간 연결을 유지한 상태로 데이터를 교환할 수 있습니다. 이때 데이터는 ‘패킷(packet)’ 형태로 전달되며, 전송은 커넥션 중단과 추가 HTTP 요청 없이 양방향으로 이뤄집니다.
이런 특징 때문에 웹소켓은 온라인 게임이나 주식 트레이딩 시스템같이 데이터 교환이 지속적으로 이뤄져야 하는 서비스에 아주 적합합니다.
Web Browser에서 Request를 보내면 Server는 Response를 준다. HTTP 통신의 기본적인 동작 방식이다. 하지만 Server에서 Client로 특정 동작을 알려야 하는 상황도 있다. 예를 들어 Browser로 Facebook에 접속해 있다가 누군가 친구가 글을 등록하는 경우, 혹은 Web Browser로 메신저를 구현하는 경우다. WebSocket이 있기 전에는 이를 Polling이나 Long polling 등의 방식으로 해결했었다. 하지만 WebSocket의 등장으로 Server-Client 간의 실시간 통신이 가능하게 되면서, 앞으로 Long polling은 역사의 뒤안길로 사라질 것 같다.
WebSocket이란 HTTP 환경에서 전이중 통신(full duplex, 2-way communication)을 지원하기 위한 프로토콜로,
RFC 6455
에 정의되어 있다. HTTP 프로토콜에서 Handshaking을 완료한 후, HTTP로 동작을 하지만, HTTP와는 다른 방식으로 통신을 한다.
3줄 요약해줘...
1. 양뱡향 통신을 위해 사용되는 약속이라고 이해가 된다.
2. 옛것의 방식은 자주 , 많은량의, 지연이 짧은 통신에 있어서는 비효율 적이다.
- 커넥션을 위한 간단한 예시 .js
let socket = new WebSocket("ws://javascript.info");
ws말고 wss://라는 프로토콜도 있는데, 두 프로토콜의 관계는 HTTP와 HTTPS의 관계와 유사합니다.
http 가 아닌 ws!
소켓이 정상적으로 만들어지면 아래 네 개의 이벤트를 사용할 수 있게 됩니다.
- open – 커넥션이 제대로 만들어졌을 때 발생함
- message – 데이터를 수신하였을 때 발생함
- error – 에러가 생겼을 때 발생함
- close – 커넥션이 종료되었을 때 발생함
커넥션이 만들어진 상태에서 무언가를 보내고 싶으면 socket.send(data)를 사용하면 됩니다.
- 웹 소켓 핸드쉐이크
new WebSocket(url)을 호출해 소켓을 생성하면 즉시 연결이 시작됩니다.
커넥션이 유지되는 동안, 브라우저는 (헤더를 사용해) 서버에 '웹소켓을 지원하나요?' 라고 물어봅니다.
이에 서버가 '네’라는 응답을 하면 서버-브라우저간 통신은 HTTP가 아닌 웹소켓 프로토콜을 사용해 진행됩니다.
핸드쉐이크 관련 요청과 응답에 관한 정보에 대해서는 여기! 참고할 수 있도록 하자
이렇게 브라우저와 서버가 연결이 되면 웹 소켓 프로토콜로 데이터를 송수신 한다고 생각하면 된다
이 외에 개념적 부분에 있어서는 아래 웹소켓 참고자료를 보고 이해하면 된다!
왜 웹소켓을 사용할까?
앞에서 명시된것 처럼
소스코드 구현
환경 설정
IDE : IntelliJ 2023
Language : Java 11 + SpringBoot 2.7.6
+ HTML, JS, Gradle
외 gradle 참고!
1. 간단하게 테스트 용도니까 JPA 나 MYSQL 등 추가하지 않고 기본적인 것들만 넣어줬다
build.gradle
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.7'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
///////////////////////////////////////////////////////////////////////
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
///////////////////////////////////////////////////////////////////////
}
tasks.named('test') {
useJUnitPlatform()
}
1. 웹 소켓 사용을 위해 gradle 추가를 해준다
2. 타임리프 템플릿 엔진은 크게 활용하지 않지만 스크립트 활용을 위해 추가
디렉토리 구조
디렉토리구조
1. 간단명료한 디렉토리 구조
ChatHandler.java
ChatHandler.java
@Component
@Log4j2
public class ChatHandler extends TextWebSocketHandler {
private static List<WebSocketSession> list = new ArrayList<>();
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("payload : " + payload);
//페이로드란 전송되는 데이터를 의미한다.
for(WebSocketSession sess: list) {
sess.sendMessage(message);
}
}
/* Client가 접속 시 호출되는 메서드 */
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
list.add(session);
log.info(session + " 클라이언트 접속");
}
/* Client가 접속 해제 시 호출되는 메서드드 */
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info(session + " 클라이언트 접속 해제");
list.remove(session);
}
}
1. 소켓통신은 서버와 클라이언트가 1:N 관계를 맺음
2. 페이로드는 전송되는 데이터를 의미함 ( 데이터 전송 간 다양한 요소들을 함께 보내서 전송효율, 안정성을 높임)
3. 리스트에 웹소켓 세션들을 저장해 메세지를 각 세션에다 뿌려주는 handling 을 구현
WebSocketConfig.java
WebSocketConfig.java
@Configuration
@RequiredArgsConstructor
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final ChatHandler chatHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler, "ws/chat").setAllowedOrigins("*");
}
}
1. @EnableWebSocket 어노테이션을 사용해 웹소켓을 활성화
2. 특정 endpoint 를 정의 한 뒤, 도메인이 다른 서버에서도 접속 가능 하도록 모든 가능성을 열어둔다(보안성 취약함)
ChatController.java
ChatController.java
@Controller
@Log4j2
public class ChatController {
@GetMapping("/chat")
public String chatGET(){
log.info("@ChatController, chat GET()");
return "chater";
}
}
1. /chat 엔드포인트인 컨트롤러이다. 리턴으로 템플릿엔진 chater.html 을 반환하도록 설정한다.
chater.html
chater.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body>
<div>
<div class="container">
<div class="col-6">
<label><b>채팅방</b></label>
</div>
<div>
<div id="msgArea" class="col"></div>
<div class="col-6">
<div class="input-group mb-3">
<input type="text" id="msg" class="form-control" aria-label="Recipient's username" aria-describedby="button-addon2">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-send">전송</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script th:inline="javascript">
$(document).ready(function(){
const username = "userName";
$("#disconn").on("click", (e) => {
disconnect();
})
$("#button-send").on("click", (e) => {
send();
});
const websocket = new WebSocket("ws://localhost:8080/ws/chat");
websocket.onmessage = onMessage;
websocket.onopen = onOpen;
websocket.onclose = onClose;
function send(){
let msg = document.getElementById("msg");
console.log(username + ":" + msg.value);
websocket.send(username + ":" + msg.value);
msg.value = '';
}
//채팅창에서 나갔을 때
function onClose(evt) {
var str = username + ": 님이 방을 나가셨습니다.";
websocket.send(str);
}
//채팅창에 들어왔을 때
function onOpen(evt) {
var str = username + ": 님이 입장하셨습니다.";
websocket.send(str);
}
function onMessage(msg) {
var data = msg.data;
var sessionId = null;
//데이터를 보낸 사람
var message = null;
var arr = data.split(":");
for(var i=0; i<arr.length; i++){
console.log('arr[' + i + ']: ' + arr[i]);
}
var cur_session = username;
//현재 세션에 로그인 한 사람
console.log("cur_session : " + cur_session);
sessionId = arr[0];
message = arr[1];
console.log("sessionID : " + sessionId);
console.log("cur_session : " + cur_session);
//로그인 한 클라이언트와 타 클라이언트를 분류하기 위함
if(sessionId == cur_session){
var str = "<div class='col-6'>";
str += "<div class='alert alert-secondary'>";
str += "<b>" + sessionId + " : " + message + "</b>";
str += "</div></div>";
$("#msgArea").append(str);
}
else{
var str = "<div class='col-6'>";
str += "<div class='alert alert-warning'>";
str += "<b>" + sessionId + " : " + message + "</b>";
str += "</div></div>";
$("#msgArea").append(str);
}
}
})
</script>
</body>
</html>
소스코드 부분적 설명
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
1. 헤더 부분에 Jquery, 화면을 위한 부트스트랩을 받아준다.
<div class="container">
<div class="col-6">
<label><b>채팅방</b></label>
</div>
<div>
<div id="msgArea" class="col"></div>
<div class="col-6">
<div class="input-group mb-3">
<input type="text" id="msg" class="form-control" aria-label="Recipient's username" aria-describedby="button-addon2">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-send">전송</button>
</div>
</div>
</div>
</div>
</div>
2. 채팅방 레이아웃이다. 전송 버튼을 누르면 id="button-send" 인 js 에서 정의한 메소드가 실행됨
$("#button-send").on("click", (e) => {
send();
});
3. 버튼을 눌렸을 경우 클릭 이벤트가 발생하며 send 메소드를 실행한다.
function send(){
let msg = document.getElementById("msg");
console.log(username + ":" + msg.value);
websocket.send(username + ":" + msg.value);
msg.value = '';
}
4. send 메소드는 html의 input 타입의 id가 msg 인 내용의 value 를 가져와서 웹소켓을 통해 메세지를 send 한다.
function onMessage(msg) {
var data = msg.data;
var sessionId = null;
//데이터를 보낸 사람
var message = null;
var arr = data.split(":");
for(var i=0; i<arr.length; i++){
console.log('arr[' + i + ']: ' + arr[i]);
}
var cur_session = username;
//현재 세션에 로그인 한 사람
console.log("cur_session : " + cur_session);
sessionId = arr[0];
message = arr[1];
console.log("sessionID : " + sessionId);
console.log("cur_session : " + cur_session);
//로그인 한 클라이언트와 타 클라이언트를 분류하기 위함
if(sessionId == cur_session){
var str = "<div class='col-6'>";
str += "<div class='alert alert-secondary'>";
str += "<b>" + sessionId + " : " + message + "</b>";
str += "</div></div>";
$("#msgArea").append(str);
}
else{
var str = "<div class='col-6'>";
str += "<div class='alert alert-warning'>";
str += "<b>" + sessionId + " : " + message + "</b>";
str += "</div></div>";
$("#msgArea").append(str);
}
}
5. 메시지를 수신하게 되면 #msgArea 아이디를 가진 영역에 html 을 덧붙여 메세징이 된 것처럼 표현한다.
최종화면
다음과 같이 결과를 확인 할 수 있다.
현재 js 파일에서 const userName 부분이 임의의 값 userName 으로 설정해둬서 다른세션에서도 같은 이름이 적용되는데 나중에 로그인 기능을 구현하게 된다면 로그인에 대한 정보를 타임리프 security 를 활용해서 넘기는 방안도 생각해보면 좋을 거 같다.
참고자료를 꼭 한번 확인하시고, 공식 레퍼런스 문서도 시간 되실때 읽어보시기 바랍니다!
참고
https://ko.javascript.info/websocket
'2023년 > 멋쟁이사자처럼 팀프로젝트' 카테고리의 다른 글
[팀프로젝트] 비동기 방식 통신 + 스프링부트 RestController 활용한 웹 페이지 (0) | 2023.01.26 |
---|---|
[팀프로젝트] 깃허브 워크플로우 (2) | 2023.01.20 |
[팀프로젝트] Springboot 사용하면서 UI 화면은 어떻게 처리하면 좋을까? (1) | 2023.01.19 |
[팀프로젝트] SpringBoot 좋아요 기능 구현 ( UI 는 없음 ) (2) | 2023.01.18 |
[팀프로젝트] SpringSecurity + Jwt 적용 (0) | 2023.01.17 |