Back-End/Spring Boot + Kotlin

Spring Boot와 WebSocket 을 이용한 실시간 통신 시스템 구축해보기

김검정 2025. 4. 7. 12:04

실시간 통신은 채팅, 알림, 게임. 금융 거래 등 다양한 분야에서 핵심 기술로 사용된다. 예를 들어, 소셜 미디어에서 친구가 게시물을 올릴 때 실시간 알림이 전송되는 경우, 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 핸드쉐이크로 시작한다.

  1. 클라이언트 요청 : 클라이언트는 HTTP 업그레이드 헤더를 포함하여 서버에 연결 요청을 보낸다.
  2. 서버 응답 : 서버는 요청을 수락하고, 프로토콜을 WebSocket으로 전환하는 응답을 보낸다.
  3. 연결 수립 : 이후 연결은 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>