그동안 만들어져 있는 lagacy Websocket 코드를 잘 쓰다가 별도로 내가 구현해야할 일이 있어 알아보니 현재는 꽤 코드가 짧아진 것을 확인할 수 있었다.
이에 예전에 pub/sub 혹은 onOpen, onClose 등 메서드를 직접 구현해서 쓸 때와 현재 Spring 4.0 에 들어오면서 추가된 Websocket 관련 편의 메서드까지 모두 알아보자.
publisher subscriber 이다.
즉, 데이터를 제공해주는 서버, 해당 서버를 계속 바라보도록 구독을 하고 알림을 받는 클라이언트 이 단어들의 앞 글자만 딴 것이다.
메시지에 대한 인터페이스라고 생각하면 된다.
예전에는 이 인터페이스까지 다 구현해서 사용했다.
실시간성을 보장하는 Websocket 말고는 HTTP를 이용하여 실시간성을 보장하는 듯하게 흉내낼 수 있다.
HTTP의 실시간성 보장 기법에는 Polling
, Long Polling
, Streaming
이 있다.
각각의 방식은 클라이언트와 서버 간의 데이터 전송 방식이 다르다.
Polling
은 클라이언트가 일정한 주기로 서버에 요청을 보내는 방식이다. 클라이언트는 정해진 간격(예: 1초마다)으로 서버에 데이터를 요청하고, 서버는 현재 상태나 새로운 데이터를 클라이언트에게 응답하는 방식이다.
구현이 간단하고, 클라이언트가 서버의 상태를 주기적으로 확인할 수 있다.
서버에 불필요한 req가 발생할 수 있으며, 데이터가 변경되지 않아도 주기적으로 요청을 보내기 때문에 비효율적이다.
또한, 실시간성이 떨어질 수 있다.
Long Polling
은 Polling
의 개선된 버전으로, 클라이언트가 요청을 보내면 서버가 즉시 응답하지 않고, 새로운 데이터가 준비될 때까지 요청을 유지한다. 데이터가 준비되면 서버는 응답을 보내고, 클라이언트는 응답을 받고 다시 새로운 요청을 보낸다.
실시간성 향상. 클라이언트는 서버가 새로운 데이터를 제공할 때까지 기다리므로, 데이터 전송이 필요한 경우에만 요청을 보낸다.
서버의 자원을 더 많이 소모할 수 있으며, 클라이언트와 서버 간의 연결을 유지하기 위한 오버헤드가 발생할 수 있다.
Streaming
은 클라이언트와 서버 간의 지속적인 연결을 유지하면서 실시간으로 데이터를 전송하는 방식이다. 서버는 클라이언트가 요청한 이후, 데이터가 발생할 때마다 즉시 전송한다. WebSocket
과 같은 기술이 이 방식에 해당한다.
매우 낮은 지연 시간으로 실시간 데이터 전송이 가능하며, 클라이언트와 서버 간의 지속적인 연결을 통해 효율적으로 데이터를 교환할 수 있다.
구현이 복잡하고, 연결이 끊어질 경우 재연결을 처리해야 하는 등의 추가적인 관리가 필요하다. 또한, 모든 브라우저와 서버가 WebSocket을 지원하지 않을 수 있다.
이렇게 각 기법은 서로 다른 상황에서 사용되며, 필요에 따라 적절한 방식으로 선택하여 사용할 수 있다.
SockJS, socket.io 라이브러리를 사용하면, 웹소켓을 지원하지 않는 브라우저에서 웹소켓을 사용하는 것과 같은 비슷한 기능을 제공해준다.
스프링은 SockJS를 제공한다.
웹소켓을 지원하는 브라우저는 웹소켓 기술을 사용한다.
지원하지 않는다면 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();
}
}
스프링에서 웹소켓을 사용하기 위해서 클라이언트가 보내는 통신을 처리할 핸들러가 필요하다.
-> 직접 구현한 웹소켓 핸들러 (SocketTextHandler)를 웹소켓이 연결될 때, Handshake할 주소 (/user)와 함께 addHandler 메소드의 인자로 넣어준다.
setAllowedOrigins("*") 으로 Cors 설정을 할 수 있다.
스프링에서 웹소켓을 사용할 때, same-origin만 허용하는 것이 기본정책이다.
withSockJS() 으로 SockJS 라이브러리를 사용하도록 설정할 수 있다.
웹소켓을 지원하지 않는 브라우저 환경에서도 비슷한 경험을 할 수 있는 기능을 제공해준다.
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);
}
}
간단한 웹소켓 핸들러이다.
웹소켓 프로토콜은 기본적으로 Text, Binary 타입을 지원한다.
-> 필요에 따라 TextWebSocketHandler, BinaryWebSocketHandler를 상속하여 구현해주면 된다.
WebSocketSession 파라미터는 웹소켓이 연결될 때 생기는 연결정보를 담고 있는 객체이다.
-> Handler에서는 웹소켓 통신에 대한 처리를 위해, 웹소켓 세션들을 컬랙션에 담아 관리하는 경우가 많다
웹소켓 커넥션이 맺어지는 경우 (afterConnectionEstablished) -> sessions.add(session);
커넥션이 끊어지면 (afterConnectionClosed) -> sessions.remove(session);
웹소켓 세션을 통해, 연결된 모든 클라이언트들에게 메시지를 보낼 수 있다.