Spring Boot와 WebSocket 을 이용한 실시간 통신 시스템 구축해보기
실시간 통신은 채팅, 알림, 게임. 금융 거래 등 다양한 분야에서 핵심 기술로 사용된다. 예를 들어, 소셜 미디어에서 친구가 게시물을 올릴 때 실시간 알림이 전송되는 경우, WebSocket을 통해 효율적으로 구현할 수 있다. 추후에 어떤 서비스를 구현해 볼지 모르기 때문에 한번 관련된 내용을 정리해두자
기본 개념을 먼저 이해해보자
1. HTTP 프로토콜
HTTP(HyperText Transfer Protocol)는 웹의 기본 통신 프로토콜이다.
- 요청-응답 구조 : 클라이언트가 요청하면 서버가 응답
- 단발성 연결 : 요청 후 연결 종료
- 비상태성 : 각 요청이 독립적
위처럼 HTTP의 한계는 실시간 상호작용에 적합하지 않다는 점이다. 실시간 데이터 전송이나 서버의 지속적인 이벤트 알림은 HTTP만으로 구현하기 어렵다.
2. WebSocket이란?
WebSocket은 HTTP와 달리 양방향, 지속적인 연결을 제공하는 프로토콜이다.
- 지속 연결 : 클라이언트와 서버가 한 번 연결되면 계속해서 데이터를 주고받을 수 있다.
- 양방향 통신 : 서버가 클라이언트에게 자유롭게 메시지를 전송할 수 있다.
- 실시간성 : 낮은 지연시간으로 실시간 데이터를 처리할 수 있다.
HTTP와 WebSocket의 차이점을 표로 만들어보자
구분 | HTTP | WebSocket |
연결 방식 | 요청-응답 후 연결 종료 | 연결 후 지속적으로 열린 상태 유지 |
통신 방향 | 단방향(클라이언트 요청에 의존) | 양방향(서버와 클라이언트 모두 자유롭게 전송) |
프로토콜 오버헤드 | 매 요청마다 헤더 정보 전송 | 초기 핸드쉐이크 후 최소한의 오버헤드 |
실시간성 | 제한적(폴링 방식 필요) | 매우 우수 (즉시 데이터 전달) |
실시간 애플리케이션에서 왜 WebSocket을 선호하는지 알 것 같다. 그러면 WebSocket의 동작 원리에 대해서 알아보자
3. WebSocket의 동작 원리
3.1 핸드쉐이크 과정
WebSocket 통신은 HTTP 핸드쉐이크로 시작한다.
- 클라이언트 요청 : 클라이언트는 HTTP 업그레이드 헤더를 포함하여 서버에 연결 요청을 보낸다.
- 서버 응답 : 서버는 요청을 수락하고, 프로토콜을 WebSocket으로 전환하는 응답을 보낸다.
- 연결 수립 : 이후 연결은 TCP기반의 지속 연결로 전황되어 데이터를 주고받는다.
이 과정을 통해 기존 HTTP 환경에서도 WebSocket을 사용할 수 있는 유연성을 제공한다.
3.2 연결 유지 및 메시지 교환
연결이 성립되면 클라이언트와 서버는 프레임 단위로 메시지를 주고받는다.
- 텍스트 프레임 : 일반 텍스트 메시지 전송
- 바이너리 프레임 : 이미지, 파일 등 이진 데이터 전송
- 컨트롤 프레임 : 연결 종료, 핑/퐁 등 관리 메시지 전송
3.3 연결 종료 메커니즘
연결 종료 시에는 양측에서 종료 요청을 보내고, 지정된 절차에 따라 연결을 정상적으로 마감한다. 이 과정을 예기치 않은 연결 종료를 방지하고, 자원 누수를 최소화한다.
Spring Boot를 활용해서 간단한 서비스를 만들어보자
Spring Boot에서는 기본적으로 WebSocket을 지원하고, 모듈도 제공해준다.
- spring-boot-starter-websocket : WebSocket 관련 의존성 자동 구성
- STOMP(Simple Text Oriented Messaging Protocol) : 메시지 브로커와의 통신 지원
Spring Boot에서 WebSocket을 사용하려면 Gradle 또는 Maven을 통해 의존성을 추가해준다.
*gradle 예시
implementation 'org.springframework.boot:spring-boot-starter-websocket'
간단한 실시간 알림 서비스
- 사용자가 실시간으로 이벤트를 감지
- 서버는 이벤트 발생 시 즉시 모든 관련 클라이언트에 알림 전달
- 연결 상태를 지속적으로 유지하여 지연 없이 메시지 전송
WebSocket 설정 클래스
@Configuration
@EnableWebSocket
class WebSocketConfig : WebSocketConfigurer {
override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
registry.addHandler(NotificationHandler(), "/ws/notifications")
.setAllowedOrigins("*")
}
}
WebSocket 핸들러 클래스
class NotificationHandler : TextWebSocketHandler() {
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
val receivedText = message.payload
val response = TextMessage("알림: $receivedText")
session.sendMessage(response)
}
override fun afterConnectionEstablished(session: WebSocketSession) {
println("클라이언트 연결됨: ${session.id}")
}
override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
println("클라이언트 연결 종료: ${session.id}")
}
}
실행 클래스
@SpringBootApplication
class WebSocketApplication
fun main(args: Array<String>) {
runApplication<WebSocketApplication>(*args)
}
화면
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Kotlin WebSocket 테스트</title>
</head>
<body>
<h2>실시간 알림 테스트</h2>
<input id="input" type="text" placeholder="메시지 입력">
<button onclick="send()">전송</button>
<div id="log"></div>
<script>
const ws = new WebSocket("ws://localhost:8080/ws/notifications");
ws.onmessage = (event) => {
const log = document.getElementById('log');
log.innerHTML += `<p>${event.data}</p>`;
};
function send() {
const input = document.getElementById('input');
ws.send(input.value);
input.value = '';
}
</script>
</body>
</html>