WebSocket 등장
인터넷을 사용할 때 가장 많이 사용되는 프로토콜은 HTTP입니다. HTTP는 클라이언트의 요청이 있는 경우에만 서버가 응답할 수 있는 단방향 프로토콜입니다. 즉 서버가 먼저 클라이언트에게 메시지를 보낼 수 없는 프로토콜입니다. 하지만 채팅이나 게임 등 실시간 통신이 필요한 경우가 생겨나면서 HTTP처럼 단방향 프로토콜이 아닌 양방향 프로토콜이 필요해졌습니다.
이를 위해 WebSocket이 등장하였습니다. WebSocket은 양방향 프로토콜로 클라이언트의 요청 없이 자유롭게 서버와 클라이언트 사이의 통신이 가능합니다.
WebSocket 이전 기술
WebSocket은 HTML5부터 등장했기 때문에 이전에는 다른 방법으로 HTTP의 단점을 보완했습니다.
(1) HTTP Polling
새로운 정보가 있는지 확인하기 위해 클라이언트에서 일정 주기마다 서버에 요청을 보내는 방식입니다. 서버에서는 요청을 받으면 바로 응답을 하는데 새로운 변경사항이 있으면 해당 변경사항을 포함해 응답합니다. 이러한 방식은 지속적으로 요청을 보내야 하기 때문에 서버의 부담이 커진다는 단점이 있습니다.
(2) HTTP Long Polling
HTTP Polling을 개선한 방식으로 클라이언트에서 요청을 보내면 서버에서 변경 사항이 있는 경우에만 응답하는 방법입니다. 클라이언트는 응답을 받으면 바로 다시 요청을 보내 변경사항이 있을 때까지 기다립니다.
변경 사항이 불규칙적인 간격으로 일어날 경우 효율적인 방법입니다. 하지만 만약 변경 사항의 빈도가 잦다면 기존의 HTTP Polling과 차이가 없어집니다.
(3) HTTP Streaming
HTTP Streaming은 HTTP Long Polling처럼 클라이언트가 요청을 보내고 서버에서 변경 사항이 있는 경우에만 응답을 합니다. 차이점으로는 Long Polling은 응답을 보낸 뒤 커넥션이 끊어지는 반면, Streaming은 커넥션이 유지됩니다. Long Polling 방식에 비해 서버의 부담을 줄일 수 있지만 여러 건의 변경 사항이 있는 경우 처리가 어려워집니다.
WebSocket 핸드쉐이크
WebSocket을 통해 양방향으로 통신을 하려면 먼저 클라이언트와 서버가 연결되어야 하는데, 이 과정을 핸드쉐이크라고 하며 이때 HTTP를 이용합니다. 클라이언트가 HTTP 요청을 보내고 서버가 요청에 대한 HTTP 응답을 보내면 연결이 완료됩니다. 핸드쉐이크가 끝나면 HTTP가 아닌 WebSocket 프로토콜로 변환됩니다.
아래는 클라이언트에서 서버로 보내는 요청 예시입니다.
"Upgrade: websocket"과 "Connection: Upgrade" 를 통해 WebSocket을 위한 요청임을 알 수 있습니다. 또한 "Sec-WebSocket-Key"를 통해 응답을 검증할 때 사용할 키를 보냅니다.
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
아래는 서버에서 클라이언트로 보내는 응답 예시입니다.
요청 메세지처럼 "Upgrade: websocket"과 "Connection: Upgrade" 를 통해 WebSocket 응답임을 알 수 있습니다. 또한 응답 때 받았던 "Sec-WebSocket-Key" 의 값에 특정 값을 붙인 후, SHA-1로 해싱하고 base64로 인코딩 한 값을 "Sec-WebSocket-Accept"을 통해 클라이언트로 보내서 정상적인 핸드쉐이크 과정임을 검증합니다.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Spring에서의 WebSocket
직접 WebSocket 핸드쉐이크를 구현할 필요 없이 WebSocketConfigurer을 구현한 클래스만 있으면 쉽게 클라이언트와 연결할 수 있습니다.
- WebSocketConfig.java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private Handler handler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(handler, "/ws/chat").setAllowedOrigins("*");
}
}
클라이언트에서 "new WebSocket("ws://localhost:8080/ws/chat")"를 통해 서버에 접속하면 핸드쉐이크가 이루어집니다. 또한 등록된 Handler를 통해 클라이언트와 메세지를 주고받을 수 있습니다.
- Handler.java
Handler는 TextWebSocketHandler 또는 BinaryWebSocketHandler를 상속받을 수 있습니다. 이 둘의 차이점은 이름에서부터 알 수 있듯이 각각 Text와 Binary를 다루는 것으로, 단순한 채팅이 아니라 이미지나 다른 리소스를 처리해야 한다면 BinaryWebSocketHandler를 사용하면 됩니다.
@Component
@RequiredArgsConstructor
public class Handler extends TextWebSocketHandler {
private List<WebSocketSession> sessions = new ArrayList<>();
//클라이언트로부터 메세지가 들어오면 처리하는 메서드
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception{
for(WebSocketSession sess : sessions){
TextMessage msg = new TextMessage(message.getPayload());
sess.sendMessage(msg);
}
}
//연결 직후 실행되는 메서드
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception{
sessions.add(session);
}
//연결이 끊기고 나서 실행되는 메서드
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception{
sessions.remove(session);
}
}
위의 코드는 TextWebSocketHandler를 상속받은 클래스로, 3개의 메서드를 오버라이드 합니다.
보통 afterConnectionEstablished 메서드를 통해 클라이언트의 session을 저장해둡니다. 이후 메세지가 들어오면 handleTextMessage 메서드가 실행되고 session으로 클라이언트를 구분하여 메세지를 처리합니다. 마지막으로 연결이 끊기고 나면 afterConnectionClosed 메서드를 통해 저장했던 session을 제거합니다.
References
'Web > Spring' 카테고리의 다른 글
[SPRING] STOMP (0) | 2022.06.22 |
---|