
그동안 기존에 만들어져 있던 레거시 WebSocket 코드를 잘 사용해 오다가,
별도로 구현해야 할 일이 있어 알아보니 현재는 코드가 꽤 짧아진 것을 확인할 수 있었습니다.
이에 예전에 pub/sub 혹은 onOpen, onClose 등의 메서드를 직접 구현해서 사용할 때와,
현재 Spring 4.0에 들어오면서 추가된 WebSocket 관련 편의 메서드까지 모두 알아보겠습니다.
publisher와 subscriber의 줄임말입니다.
즉, 데이터를 제공해주는 서버와 그 서버를 구독하고 알림을 받는 클라이언트를 의미합니다.
메시지에 대한 인터페이스라고 생각하시면 됩니다.
예전에는 이 인터페이스까지 직접 구현하여 사용했습니다.
실시간성을 보장하는 WebSocket 외에도 HTTP를 이용하여 실시간성을 보장하는 것처럼
흉내낼 수 있습니다.
HTTP의 실시간성 보장 기법에는 Polling, Long Polling, Streaming이 있습니다.
각각의 방식은 클라이언트와 서버 간의 데이터 전송 방식이 다릅니다.
Polling은 클라이언트가 일정한 주기로 서버에 요청을 보내는 방식입니다.
클라이언트는 정해진 간격(예: 1초마다)으로 서버에 데이터를 요청하고,
서버는 현재 상태나 새로운 데이터를 클라이언트에게 응답하는 방식입니다.
- 장점
구현이 간단하며, 클라이언트가 서버의 상태를 주기적으로 확인할 수 있습니다.
- 단점
서버에 불필요한 요청이 발생할 수 있으며,
데이터가 변경되지 않아도 주기적으로 요청을 보내기 때문에 비효율적일 수 있습니다.
또한, 실시간성이 떨어질 수 있습니다.
Long Polling은 Polling의 개선된 버전으로, 클라이언트가 요청을 보내면
서버가 즉시 응답하지 않고 새로운 데이터가 준비될 때까지 요청을 유지합니다.
데이터가 준비되면 서버는 응답을 보내고, 클라이언트는 응답을 받은 후
다시 새로운 요청을 보냅니다.
- 장점
실시간성이 향상됩니다.
클라이언트는 서버가 새로운 데이터를 제공할 때까지 기다리므로,
데이터 전송이 필요한 경우에만 요청을 보냅니다.
- 단점
서버의 자원을 더 많이 소모할 수 있으며, 클라이언트와 서버 간의 연결을 유지하기 위한
오버헤드가 발생할 수 있습니다.
Streaming은 클라이언트와 서버 간의 지속적인 연결을 유지하면서
실시간으로 데이터를 전송하는 방식입니다.
서버는 클라이언트가 요청한 이후, 데이터가 발생할 때마다 즉시 전송합니다.
WebSocket과 같은 기술이 이 방식에 해당합니다.
- 장점
매우 낮은 지연 시간으로 실시간 데이터 전송이 가능하며,
클라이언트와 서버 간의 지속적인 연결을 통해 효율적으로 데이터를 교환할 수 있습니다.
- 단점
구현이 복잡하고, 연결이 끊어질 경우 재연결을 처리해야 하는 등의
추가적인 관리가 필요합니다. 또한, 모든 브라우저와
서버가 WebSocket을 지원하지 않을 수 있습니다.
이렇게 각 기법은 서로 다른 상황에서 사용되며,
필요에 따라 적절한 방식으로 선택하여 사용할 수 있습니다.
클라이언트와 서버는 연결을 맺고 끊습니다. (비연결성)
3-way, 4-way handshake로 연결을 맺고 끊어야 합니다.
요청과 응답이 하나의 쌍을 이루는 구조로 통신합니다.
원하는 리소스에 대해 서버에 요청을 해야 합니다.
예) 탁구
매 요청 시마다 많은 정보를 만들어 서버에 보냅니다.
응답 시에도 마찬가지입니다.
실시간성을 요하는 서비스(요청과 응답이 많은 서비스)에 부담이 되는 구조입니다.
한 번 연결을 맺으면, 그 연결을 계속 유지합니다.
연결을 맺는 과정에서 발생하는 비용을 줄일 수 있습니다.
연결이 계속 유지되므로, 요청 없이 상대가 보낸 메시지를 계속 듣고 있기만 하면 됩니다.
예) 전화 연결
최초의 handshake 과정에서는 HTTP 프로토콜을 이용하기 때문에 HTTP와
유사한 양의 데이터를 주고받습니다.
하지만 한 번 연결이 수립되면, 간단한 데이터만 오고 갑니다.
(HTTP보다 통신 비용이 저렴합니다)
- SockJS, socket.io 라이브러리를 사용하면, WebSocket을 지원하지 않는 브라우저에서도
유사한 기능을 제공받을 수 있습니다.- 스프링은 SockJS를 제공합니다.
- WebSocket을 지원하는 브라우저는 WebSocket 기술을 사용합니다.
- 지원하지 않는다면 HTTP의 Streaming을 사용하고, 그것도 지원하지 않으면
Polling을 사용합니다.
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 메서드의 인자로 넣어줍니다.
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를 이용하면 웹소켓을 지원하지 않는 환경에서도 유연하게 대응할 수 있습니다.
1. 뷰를 이용한 웹소켓
2. 웹소켓의 활용성
3. 서블릿 웹소켓
4. Mastering WebSocket: A Practical Guide to send, close, reconnect, and More
5. STOMP을-알아보고-구현해보자
6. WebSocket & Spring