[Spring Boot] Web Socket

Hood·2025년 4월 7일
0

Spring Boot

목록 보기
8/14
post-thumbnail

✍ Back-End 지식을 늘리자!

백엔드 개발자를 준비하며 생기는 의문들을 정리한 포스트입니다.


들어가기 전

우리가 흔히 사용하는 HTTP는 요청이 있어야만 응답이 가능한 단방향 통신인데
실시간으로 연결이 유지되어 데이터를 주고 받을 수 있는 양방향 통신이 필요할 수 있습니다.
그래서 이번 포스트는 채팅, 실시간 알람, 주식 등에 사용할 수 있는 Web Socket에 대해 알아보려 합니다.


Web Socket이란?

WebSocket은 HTTP와는 다르게 클라이언트와 서버가 양방향으로 지속적인 통신을 할 수 있는 프로토콜 입니다.
예를 들어 채팅, 실시간 알림, 게임 등 실시간 데이터 송수신이 필요한 서비스에 주요 사용됩니다.
일반적으로 HTTP 요청/응답과 달리, 한번 연결하면 서버와 클라이언트가 계속 데이터를 주고 받을 수 있습니다.

그럼 왜 필요할까?

1. 실시간 양방향 통신

WebSocket은 한 번 연결만 하면 클라이언트와 서버가 양방향으로 자유롭게 데이터를 주고 받을 수 있습니다.
특히 실시간성이 중요한 서비스에는 꼭 사용되어야 합니다.
이를 통해 실시간 데이터 송수신이 필요한 서비스에 유용하게 사용할 수 있습니다.

2. 지속적인 연결 유지

HTTP는 요청-응답 이후 연결이 종료되지만
WebSocket은 지속적인 연결을 유지하며 실시간으로 데이터를 PUSH할 수 있습니다.

3. 낮은 오버헤드

오버헤드는 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간과 메모리를 뜻합니다.
WebSocket은 처음에만 헨드셰이크를 한 뒤 이후에는 헤더 없이 소켓 메시지를 주고받기 떼문에
네트워크 오버헤드가 낮고 빠릅니다.

4. 효율적인 서버 리소스 사용

폴링 방식보다 적은 리소스로 더 많은 클라이언트를 처리할 수 있습니다.

WebSocket vs HTTP vs Polling

항목WebSocketHTTPPolling
통신 방식양방향 (Full Duplex)단방향 (Request → Response)단방향, 주기적 요청
연결 유지O (지속 연결)X (요청마다 연결)X (반복 연결)
실시간성매우 높음낮음중간 (주기에 따라 다름)
서버 → 클라이언트 전송O (Push 가능)X (불가능)X (클라이언트가 먼저 요청)
오버헤드낮음 (초기만 Handshake)높음 (요청마다 헤더 포함)높음 (불필요한 요청 반복)
사용 예채팅, 알림, 게임, 실시간 주식 등로그인, 게시글 조회 등 일반 API간단한 알림/업데이트 polling이 가능할 때

In Kotlin

간단한 채팅 서비스 만들기

1. 의존성 추가

//Spring Boot WebSocket
implementation("org.springframework.boot:spring-boot-starter-websocket")

2. 서비스 구성

//entity
@Entity
class Chat(
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    val roomId: Long? = null,

    @Column(nullable = false, unique = true)
    val topic: String
)


// dto
data class ChatMessage(
    val topic: String,
    val time: String,
    val message: String
)

//controller
@GetMapping("/")
fun index(): String {
    return "index"
}

@PostMapping("/create/topic")
@ResponseBody
fun createChatRoom(@RequestBody topic: String): String {
    val savedChat = chatRepository.save(Chat(topic = topic))
    return "Chat room created with topic: ${'$'}{savedChat.topic}"
}

// 특정 주제 메시지 전달
@MessageMapping("/chat/message")
fun sendMessage(message: ChatMessage) {
    template.convertAndSend("/sub/" + message.topic, message)
}

3. config 설정

@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
    //클라 서버 세션 연결
    override fun registerStompEndpoints(registry: StompEndpointRegistry) {
        registry.addEndpoint("/chat").setAllowedOriginPatterns("*").withSockJS()
    }

    //pub과 sub의 데이터 통신 정의
    override fun configureMessageBroker(registry: MessageBrokerRegistry) {
        //클라가 서버로 통신하는 것을 정의
        registry.setApplicationDestinationPrefixes("/pub")
        //서버가 클라에게 데이터를 전달할 때
        registry.enableSimpleBroker("/sub")
    }
}

4. 예제 HTML

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Simple Chat</title>
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.5.1/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; }
        #chat-box { border: 1px solid #ccc; height: 300px; overflow-y: scroll; margin-bottom: 10px; padding: 10px; }
    </style>
</head>
<body>
<h2>실시간 채팅</h2>

<label>닉네임: <input type="text" id="from" /></label><br/>
<label>채팅방(topic): <input type="text" id="topic" value="chat" /></label><br/><br/>

<div id="chat-box"></div>

<input type="text" id="message" placeholder="메시지를 입력하세요" />
<button onclick="sendMessage()">보내기</button>

<script>
    const socket = new SockJS("/chat");
    const stompClient = Stomp.over(socket);

    let topic = "chat";

    stompClient.connect({}, function () {
        document.getElementById("topic").addEventListener("change", function () {
            topic = this.value;
        });

        stompClient.subscribe("/sub/chat", function (msg) {
            const message = JSON.parse(msg.body);
            const messageBox = document.createElement("div");
            messageBox.textContent = `${message.from}: ${message.message}`;
            document.getElementById("chat-box").appendChild(messageBox);
        });
    });

    function sendMessage() {
        const from = document.getElementById("from").value.trim();
        const text = document.getElementById("message").value.trim();
        const topicValue = document.getElementById("topic").value.trim();

        if (!from || !text || !topicValue) {
            alert("닉네임, 메시지, 토픽 모두 입력해주세요!");
            return;
        }

        stompClient.send("/pub/chat/message", {}, JSON.stringify({
            from: from,
            to: "everyone",
            topic: topicValue,
            message: text
        }));

        // 내 메시지도 바로 출력
        const messageBox = document.createElement("div");
        messageBox.textContent = `${from} (나): ${text}`;
        document.getElementById("chat-box").appendChild(messageBox);

        document.getElementById("message").value = "";
    }
</script>
</body>
</html>


Spring Boot에서 WebSocket 구성 요소와 역할

구성 요소어노테이션 / 클래스사용 목적언제 사용하는가?
WebSocket 설정@EnableWebSocketMessageBroker
WebSocketMessageBrokerConfigurer
WebSocket 메시지 브로커 기능 활성화WebSocket 기능을 Spring 애플리케이션에 적용할 때
메시지 송신 경로.setApplicationDestinationPrefixes("/app")클라이언트가 서버에 메시지를 보낼 경로 지정클라이언트가 메시지를 서버에 전송할 때
메시지 구독 경로.enableSimpleBroker("/topic", "/queue")서버가 클라이언트에게 브로드캐스트할 경로 설정서버가 클라이언트에게 실시간 메시지를 보낼 때
엔드포인트 등록registry.addEndpoint("/ws")WebSocket 연결을 위한 URL 엔드포인트 등록클라이언트가 최초로 WebSocket 연결을 시도할 때
메시지 처리@MessageMapping("/chat")클라이언트로부터 받은 메시지를 처리클라이언트가 특정 목적지로 메시지를 전송했을 때
메시지 브로드캐스트@SendTo("/topic/public")처리된 메시지를 구독자에게 전송서버가 여러 클라이언트에게 메시지를 전송할 때
메시지 DTOdata class ChatMessage(...)송수신할 메시지 구조 정의메시지를 직렬화/역직렬화해서 주고받을 때

📌 결론

실시간으로 통신해야 하는 서비스가 필요할 때는 Spring Web Socket을 이용하여 구현할 수 있습니다.

profile
달을 향해 쏴라, 빗나가도 별이 될 테니 👊

0개의 댓글