Spring에서 WebSocket 실시간 통신 모든 것 구현하기

궁금하면 500원·2024년 5월 25일

미생의 스프링

목록 보기
3/48

WebSocket

그동안 기존에 만들어져 있던 레거시 WebSocket 코드를 잘 사용해 오다가,
별도로 구현해야 할 일이 있어 알아보니 현재는 코드가 꽤 짧아진 것을 확인할 수 있었습니다.

이에 예전에 pub/sub 혹은 onOpen, onClose 등의 메서드를 직접 구현해서 사용할 때와,
현재 Spring 4.0에 들어오면서 추가된 WebSocket 관련 편의 메서드까지 모두 알아보겠습니다.

단어

- pub/sub

publisher와 subscriber의 줄임말입니다.
즉, 데이터를 제공해주는 서버와 그 서버를 구독하고 알림을 받는 클라이언트를 의미합니다.

- STOMP

메시지에 대한 인터페이스라고 생각하시면 됩니다.
예전에는 이 인터페이스까지 직접 구현하여 사용했습니다.

WebSocket 외 다른 기술들

실시간성을 보장하는 WebSocket 외에도 HTTP를 이용하여 실시간성을 보장하는 것처럼
흉내낼 수 있습니다.

HTTP의 실시간성 보장 기법에는 Polling, Long Polling, Streaming이 있습니다.

각각의 방식은 클라이언트와 서버 간의 데이터 전송 방식이 다릅니다.

- Polling

Polling은 클라이언트가 일정한 주기로 서버에 요청을 보내는 방식입니다.

클라이언트는 정해진 간격(예: 1초마다)으로 서버에 데이터를 요청하고,
서버는 현재 상태나 새로운 데이터를 클라이언트에게 응답하는 방식입니다.

- 장점

구현이 간단하며, 클라이언트가 서버의 상태를 주기적으로 확인할 수 있습니다.

- 단점

서버에 불필요한 요청이 발생할 수 있으며,
데이터가 변경되지 않아도 주기적으로 요청을 보내기 때문에 비효율적일 수 있습니다.

또한, 실시간성이 떨어질 수 있습니다.

- Long Polling

Long Polling은 Polling의 개선된 버전으로, 클라이언트가 요청을 보내면
서버가 즉시 응답하지 않고 새로운 데이터가 준비될 때까지 요청을 유지합니다.

데이터가 준비되면 서버는 응답을 보내고, 클라이언트는 응답을 받은 후
다시 새로운 요청을 보냅니다.

- 장점
실시간성이 향상됩니다.
클라이언트는 서버가 새로운 데이터를 제공할 때까지 기다리므로,
데이터 전송이 필요한 경우에만 요청을 보냅니다.

- 단점
서버의 자원을 더 많이 소모할 수 있으며, 클라이언트와 서버 간의 연결을 유지하기 위한
오버헤드가 발생할 수 있습니다.

Streaming

Streaming은 클라이언트와 서버 간의 지속적인 연결을 유지하면서
실시간으로 데이터를 전송하는 방식입니다.

서버는 클라이언트가 요청한 이후, 데이터가 발생할 때마다 즉시 전송합니다.
WebSocket과 같은 기술이 이 방식에 해당합니다.

- 장점
매우 낮은 지연 시간으로 실시간 데이터 전송이 가능하며,
클라이언트와 서버 간의 지속적인 연결을 통해 효율적으로 데이터를 교환할 수 있습니다.

- 단점
구현이 복잡하고, 연결이 끊어질 경우 재연결을 처리해야 하는 등의
추가적인 관리가 필요합니다. 또한, 모든 브라우저와
서버가 WebSocket을 지원하지 않을 수 있습니다.

이렇게 각 기법은 서로 다른 상황에서 사용되며,
필요에 따라 적절한 방식으로 선택하여 사용할 수 있습니다.

HTTP vs WebSocket

HTTP

  • 클라이언트와 서버는 연결을 맺고 끊습니다. (비연결성)
    3-way, 4-way handshake로 연결을 맺고 끊어야 합니다.

  • 요청과 응답이 하나의 쌍을 이루는 구조로 통신합니다.

  • 원하는 리소스에 대해 서버에 요청을 해야 합니다.
    예) 탁구

  • 매 요청 시마다 많은 정보를 만들어 서버에 보냅니다.
    응답 시에도 마찬가지입니다.

  • 실시간성을 요하는 서비스(요청과 응답이 많은 서비스)에 부담이 되는 구조입니다.

WebSocket

  • 한 번 연결을 맺으면, 그 연결을 계속 유지합니다.
    연결을 맺는 과정에서 발생하는 비용을 줄일 수 있습니다.

  • 연결이 계속 유지되므로, 요청 없이 상대가 보낸 메시지를 계속 듣고 있기만 하면 됩니다.
    예) 전화 연결

  • 최초의 handshake 과정에서는 HTTP 프로토콜을 이용하기 때문에 HTTP와
    유사한 양의 데이터를 주고받습니다.
    하지만 한 번 연결이 수립되면, 간단한 데이터만 오고 갑니다.
    (HTTP보다 통신 비용이 저렴합니다)

  • SockJS, socket.io 라이브러리를 사용하면, WebSocket을 지원하지 않는 브라우저에서도
    유사한 기능을 제공받을 수 있습니다.
  • 스프링은 SockJS를 제공합니다.
    • WebSocket을 지원하는 브라우저는 WebSocket 기술을 사용합니다.
    • 지원하지 않는다면 HTTP의 Streaming을 사용하고, 그것도 지원하지 않으면
      Polling을 사용합니다.

WebSocket Configuration 클래스 작성할때...

package com.websocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new SocketTextHandler(), "/user")
                .setAllowedOrigins("*")
                .withSockJS();
    }
}

WebSocket에 대한 Configuration 클래스를 만들고,
WebSocketConfigurer 인터페이스를 구현하며, @EnableWebSocket 어노테이션을 추가합니다.

스프링에서 WebSocket을 사용하기 위해서는 클라이언트가 보내는 통신을 처리할
핸들러가 필요합니다.

직접 구현한 WebSocket 핸들러(SocketTextHandler)를 WebSocket이 연결될 때
Handshake할 주소(/user)와 함께 addHandler 메서드의 인자로 넣어줍니다.

Socket Handler 작성

package com.websocket;

import org.json.JSONObject;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class SocketTextHandler extends TextWebSocketHandler {

    private final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        JSONObject jsonObject = new JSONObject(payload);
        for (WebSocketSession s : sessions) {
            s.sendMessage(new TextMessage("Hi " + jsonObject.getString("user") + "!"));
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessions.remove(session);
    }
}

간단한 WebSocket 핸들러입니다.

WebSocket 프로토콜은 기본적으로 Text, Binary 타입을 지원합니다.

필요에 따라 TextWebSocketHandler, BinaryWebSocketHandler를 상속하여
구현할 수 있습니다.

WebSocketSession 파라미터는 WebSocket이 연결될 때
생기는 연결 정보를 담고 있는 객체입니다.

핸들러에서는 WebSocket 통신에 대한 처리를 위해,
WebSocket 세션들을 컬렉션에 담아 관리하는 경우가 많습니다.

  • WebSocket 연결이 맺어지는 경우(afterConnectionEstablished): sessions.add(session);

  • 연결이 끊어지면(afterConnectionClosed): sessions.remove(session);

WebSocket 세션을 통해, 연결된 모든 클라이언트에게 메시지를 보낼 수 있습니다.

웹소켓 핸들러 다이어그램

좀더 추가 설명 한다면..

현재 구현된 WebSocket 핸들러는 기본적인 기능만 제공하므로,
예외 처리와 더 복잡한 메시지 라우팅, 인증 등을 추가하는 것이 좋습니다.

아래는 예외 처리를 추가한 코드 예제입니다.

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    try {
        String payload = message.getPayload();
        JSONObject jsonObject = new JSONObject(payload);
        for (WebSocketSession s : sessions) {
            s.sendMessage(new TextMessage("Hi " + jsonObject.getString("user") + "!"));
        }
    } catch (Exception e) {
        session.sendMessage(new TextMessage("Error processing message: " + e.getMessage()));
    }
}

위 코드는 메시지 처리 중 예외가 발생했을 때,
클라이언트에게 에러 메시지를 보내는 방식으로 보완하였습니다.

이렇게 하면 클라이언트가 발생한 문제를 인지하고 적절히 대응할 수 있습니다.

추가적으로, 메시지 라우팅이나 인증 로직을 추가해 보다 안전하고
효율적인 WebSocket 서비스를 구현할 수 있습니다.

결론

Spring에서 WebSocket을 사용하여 실시간 통신을 구현하면,
HTTP 기반의 Polling, Long Polling, Streaming과는 차별화된
빠르고 효율적인 통신을 제공할 수 있습니다.

특히 SockJS를 이용하면 웹소켓을 지원하지 않는 환경에서도 유연하게 대응할 수 있습니다.

references

1. 뷰를 이용한 웹소켓
2. 웹소켓의 활용성
3. 서블릿 웹소켓
4. Mastering WebSocket: A Practical Guide to send, close, reconnect, and More
5. STOMP을-알아보고-구현해보자
6. WebSocket & Spring

번외: 폴링 채팅 , 롱폴링 채팅

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글