본문 바로가기

항해 99/Spring

WebSocket - 기본 websocket

Web Socket

 

WebScoket 소개

WebSocket 프로토콜은 표준된 방법으로 서버-클라이언트 간에 단일 TCP 커넥션을 이용해 양방향 통신을 제공함

 

특징

기존의 다른 TCP 기반의 프로토콜과 다르게, WebSocket은 HTTP 요청 기반으로 Handshake 과정을 거쳐 커넥션을 생성함

초기 WebSocket Handshake 요청은 추가적인 방화벽 설정 없이 80, 443 포트틀 사용하여 양방향 통신이 가능

HTTP 규격 그대로 유지할 수 있기 때문에 HTTP 인증, CORS 등을 동일하게 적용할 수 있다는 장점이 있음

 

커넥션 Flow

WebSocket은 커넥션을 맺기 위해 HTTP 요청을 보내는데, 아래와 같이 HTTP 요청 헤더에 Upgrade 헤더와 Connection 포함함

# Upgrade
- 이미 생성된 커넥션을 다른 프로토콜로 업그레이드/변경
- 클라이언트가 Upgrade 헤더 값에 나열한 프로토콜 리스트를 서버가 선택한다.
- 앞쪽에 배치할수록 우선순위가 높음
- 서버는 Upgrade 하기로 선택한 프로토콜을 응답 Uprade 헤더에 추가해 전달한다.
GET ws://localhost:3000/sockjs-node HTTP/1.1
Host: localhost:3000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36
Upgrade: websocket
Origin: http://localhost:3000
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6
Sec-WebSocket-Key: xwGnajy+I6YJ/AW7pTKioA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

 

서버는 아래와 같이 101 Switching Protocols 상태 코드로 응답하는데, Handshake 이후에도 TCP 커넥션은 지속석으로 유지 됨

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 6Ux2cxOp2HhzP9SLCuADGUKiLbU=

 

HTTP vs WebSocket

WebSocket이 HTTP 요청이 시작되는 호환성을 가지고 있지만, 분명하게 두 프로토콜은 다른 방식으로 동작함

 

HTTP는 여러 URL을 기반으로 서버 애플리케이션과 Request/Response 형식으로 상호 작용함

 

WebSocket은 반대로 오직 초기의 커넥션 수립을 위한 하나의 URL만 있고, 모든 애플리케이션 메시지는 동일한 TCP 커넥션에서 전달 됨

 

즉, WebSocket은 HTTP 프로토콜과 다른 asynchronous, event-driven, messaging 아키텍쳐 모델임

 

또한, HTTP 경우에는 서버가 URI, Method, Headers 정보로 적절한 핸들러로 라우팅해 처리할 수 있음

 

WebSocket은 HTTP와 다르게 메시지 내용에 의미를 두지 않기 때문에, 클라이언트-서버 간에 임의로 메시지에 의미를 부여하지 않으면 처리할 수 없음

 

STOMP 메시징 프로토콜을 통해서 해결할 수 있는데, 상위 프로토콜이 규정한 협약을 토대로 메시지를 처리할 수 있음

 

WebSocket 사용 이유?

 

Polling

새로운 정보가 있는지 확인하기 위해 주기적으로 HTTP 요청을 보낸다, 이러한 방식은 지속적으로 요청을 보내기 때문에, 매번 커넥션을 생성하기 위한 Handshake 비용이 많아지며 서버에 부담을 주게 된다

 

Long Polling

일반적인 Polling을 개선한 방식, 클라이언트는 서버에 요청을 보내고, 서버는 변경 사항이 있는 경우에만 응답하여 커넥션을 종료함

 

클라이언트는 바로 다시 서버에 요청을 보내 변경 사항이 있을 때까지 대기하게 됨(커넥션을 무한히 대기할 수 없으므로, 브라우저는 약 5분 정도 대기하며 중간 프록시에 따라 더 짧게 커넥션이 종료될 수도 있음

 

변경 사항이 불규칙적인 간격으로 일어나는 경우 효율적이나, 변경 사항의 빈도가 잦다면 기존 polling과 차이가 없으므로 서버의 부담이 증가하게 됨

 

HTTP Streaming

Long Polling 과 동일하게 HTTP 요청을 보내지만, 변경 사항을 클라이언트에 응답한 이후에도 커넥션을 종료하지 않고 유지함, 따라서 매번 새로운 커넥션을 맺고 끊는 것이 아니라 하나의 커넥션을 유지하며, 변경 사항을 응답 메시지로 전달함

 

HTTP Streaming은 Long Polling 방식에 비해 서버의 부담을 줄일 수 있지만, 여러 건의 변경 사항이 일어난 경우 동시 처리가 어려워 짐(서버가 현재 업데이트된 데이터를 모두 전달해야만, 클라이언트에서 다음 업데이트 된 데이터의 시작 위치를 알 수 있음)

 

HTTP Streaming 방식은 서버가 클라이언트에게 전달하는 메시지에 대한 실시간성을 어느 정도 보장하지만, 클라이언트가 서버에게 보내는 요청은 여전히 새로운 커넥션을 생성해야 함

 

이러한 동시성과 서버 부담이라는 Trade Off 사항에서 HTTP Streaming 보다 Long Polling 방식을 많이 사용하게 됨

 

WebSocket

위와 같은 HTTP Long Polling, Streaming 방식이 가지고 있는 문제를 해결하고, 서버-클라이언트 간에 양방향 통신이 가능하도록 WebSocket 이라는 기술이 만들어짐

 

WebSocket은 서비스를 동적으로 만들어 주지만, AJAX, HTTP Streaming, HTTP Long Polling 기술이 보다 효과적인 경우도 있음(예: 변경 사항의 빈도가 자주 일어나지 않고 데이터의 크기가 작은 경우)

 

실시간성을 보장해야 하고 변경 사항의 빈도가 크다면 WebSocket은 좋은 해결책이 될 수 있음

 

Spring WebSocket

Spring Framework는 WebSocket API를 제공함(Spring에서 제공하는 WebSocket API는 Spring MVC 기술에 종속되지 않는다)

 

WebSocket 서버는 WebSocketHandler 인터페이스의 구현체를 통해서, 각 경로에 대한 핸들러를 구현할 수 있으며, Message 형식에 따라 TextWebSocketHandler or BinaryWebSocketHandler 핸들러를 확장해 구현할 수 있음

 

 

템플릿 메서드 패턴이 적용된 AstractWebSocketHandler 추상 클래스

  • 메시지 형식에 따라, 적합한 handleXXX 메서드를 호출

Spring WebSocket 설정

문자열 메시지 기반으로 테스트를 진행하기 때문에 TextWebSocketHandler를 상속받아 메시지를 전달받는다.

public class Handler extends TextWebSocketHandler {
	@Override
	public void handleTextMessage(WebSocketSession wsSession, TextMessage message) throws Exception {
		wsSession.sendMessage(message);
	}
}

 

핸들러를 Bean으로 등록하고, 클라이언트와 연결할 경로를 등록

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
		webSocketHandlerRegistry
			.addHandler(webSocketHandler(), "/test")
			.setAllowedOrigins("*");
	}

	@Bean
	public WebSocketHandler webSocketHandler() {
		return new Handler();
	}
}

 

 클라이언트에서는 브라우저에 내장된 WebSocket 기능을 이용해서 서버와 커넥션을 맺고, 메시지를 양방향으로 주고 받는다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>
        .message {
            margin: 5em;
            color: olive;
            border: solid 2px #D2691E;
            -webkit-transition: 0.5s; transition: 0.5s;
            width: 50%;
            padding: 10px 20px;
            box-sizing: border-box;
        }
        .messages {
            margin: 5em;
        }
        .send-btn {
            background-color: #F4FEE2;
            padding: 10px 20px;
        }
        li {
            list-style-type: none;
            margin: 1em;
            text-align: center;
            font-size: 2em;
        }
    </style>
</head>
<body>
    <div>
        <input type="text" class="message"/>
        <button onClick='send()' class="send-btn">보내기</button>
    </div>
    <div class="messages">
        <div id="messages"></div>
    </div>
</body>
    <script>
        let client;
        document.addEventListener("DOMContentLoaded", function() {
            client = new WebSocket('ws://localhost:8080/test');

            client.onopen = function (event) {
                console.log("Connected!!")
            };

            client.onmessage = function (event) {
                const messages = document.querySelector("#messages");
                const message = document.createElement("li");
                message.innerText = event.data;
                messages.appendChild(message)
            }
        });

        function send() {
            const message = document.querySelector(".message");
            client.send(message.value);
            message.value = '';
        }
    </script>
</html>

 

WebSocket Session 동시성

WebSocketHandler를 사용하는 경우, 표준 WebSocket session(JSR-356)은 동시 전송을 지원하지 않음

 

따라서 STOMP 메시징 프로토콜을 이용해서 메시지 전송을 동기화하거나, WebSocketSession을 ConcurrentWebSocketSessionDecorator으로 Wrapping해야 한다.

ConcurrentWebSocketSessionDecorator은 오직 하나의 스레드만 메시지를 전송하도록 보장해주기 때문이다.

 

WebSocket Handshake

각 WebSocketHandler마다 HandShake 전(before) / 후(after)로 필요한 작업이 있다면, HandshakeInterceptor 인터페이스를 구현해서 등록하면 됨, 이를 통해 HandShake를 막거나 WebSocketSession의 속성을 사용할 수 있다.

HTTP Session을 WebSocket Session에 전달하는 HttpSessionHandshakeInterceptor 예제

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
		webSocketHandlerRegistry
			.addHandler(webSocketHandler(), "/test")
			.addInterceptors(new HttpSessionHandshakeInterceptor())
			.setAllowedOrigins("*");
	}

	@Bean
	public WebSocketHandler webSocketHandler() {
		return new Handler();
	}
}
  • 직접 HandShake 단계의 작업을 수행해야 하는 경우, AbstractHandshakeHandler 를 확장해 직접 구현 가능
  • 지원하지 않는 WebSocket 서버 엔진이나 버전을 적용하기 위해, RequestUpgradeStrategy를 직접 구현할 수 있음(구현한 RequestUpgradeStrategy 객체는 AbstractHandshakeHandler 생성자를 통해 전달 됨)

DefaultHandshakeHandler를 추가하는 과정

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
		webSocketHandlerRegistry
			.addHandler(webSocketHandler(), "/test")
			.setHandshakeHandler(new DefaultHandshakeHandler())
			.setAllowedOrigins("*");
	}

	@Bean
	public WebSocketHandler webSocketHandler() {
		return new Handler();
	}
}

 

WebSocketHandlerDecorator

Spring은 WebSocketHandler가 호출되기까지 여러 Decorator를 거침(스프링은 데코레이터 패턴을 이용해서 WebSocketHandler에 대한 추가적인 작업을 처리할 수 있도록 WebSocketHandlerDecorator 객체를 제공)

  • 예: Message, Session 등의 정보에 대한 로깅 등의 작업을 추가할 수 있음

WebSocketHandlerDecorator 구현체 실제 동작 과정 예시

  • ExceptionWebSocketHandlerDecorator WebSocketHandler 실행 과정에서 발생하는 예외를 모두 잡아서 처리함
  • LoggingWebSocketHandlerDecorator는 Message, Session 정보를 logging함
  • 마지막으로, AbstractWebSocketHandler는 Message 타입에 따라 handleXXX() 메서드를 호출함

WebSocket 속성 설정

WebSocket Engine에 대한 메시지 버퍼 크기, 유휴 제한 시간 등 같은 런타임 특성을 ServletServerContainerFactoryBean으로 등록할 수 있음

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
	...
	@Bean
	public ServletServerContainerFactoryBean createWebSocketContainer() {
		ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
		container.setMaxTextMessageBufferSize(10000);
		container.setMaxSessionIdleTimeout(1000L);
		container.setAsyncSendTimeout(1000L);
		return container;
	}
}

 

Allowed Origins

WebSocket을 위해 스프링은 기본적으로 Same-Origin 요청을 지원함(동일 출처 도메인에 대해서만 커넥션을 수락)

 

각 핸들러 마다 지원할 도메인을 지원할 수 있도록 설정 가능

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
		webSocketHandlerRegistry
			.addHandler(webSocketHandler(), "/test")
			.setAllowedOrigins("http://something.co.kr", "https://example.com");
	}

	...
}

 

상세 사항 참고

'항해 99 > Spring' 카테고리의 다른 글

WebSocket - STOMP 1  (1) 2024.03.29
WebSocket - SockJS  (0) 2024.03.28
Redis  (1) 2024.03.25
MapStruct  (0) 2024.03.22
대댓글 기능 구현  (1) 2024.03.21