[번역] Spring-WebSocket

rin·2020년 6월 19일
13

Document 번역

목록 보기
12/22
post-thumbnail

Spring document - Web Socket을 번역합니다.

번역하는 이유 : 실시간 채팅방 만들고 싶어서 😼

WebSocket 이란?

웹 소켓 프로토콜인 RFC 6455는 단일 TCP 연결을 통해 클라이언트와 서버 사이에 전이중 양방향 통신(쌍방이 동시에 송신할 수 있는 것) 채널을 설정하는 표준화된 방법을 제공한다. HTTP와는 다른 TCP 프로토콜이지만 포트 80 및 443을 사용해 HTTP를 통해 작동하고, 기존 방화벽 규칙을 재사용할 수 있도록 설계되었다.

❗️ NOTE
wifipedia - Http/1.1 upgrade header

upgrade header
upgrade 헤더 필드는 HTTP/1.1에 도입된 HTTP 헤더 필드이다. exchange에서 클라이언트는 나중에 새로운 HTTP 프로토콜 버전 업그레이드되거나 다른 프로토콜 전환되는 cleartext request를 생성하는 것으로부터 시작된다. 클라이언트는 커넥션의 업그레이드를 요청해야한다. 만약 서버 업그레이드를 강제하기 위해서는 425 upgrade 필수 response를 보내야한다. 그 다음 클라이언트는 연결을 계속 열어두면서 적절한 upgrade 헤더로 새 요청을 보낼 수 있다.

use with WebSocket
웹소켓은 호환 가능한 방법으로 HTTP 서버와의 연결을 설정하기 위해 이 메커니즘을 사용한다. 웹소켓 프로토콜은 두 부분으로 구성되어 있는데 하나는 업그레이드된 연결을 설정하기 위한 handshake이고 다른 하나는 실제 데이터 전송이다. 가장 먼저, 클라이언트가 upgrade: WebSocketConnection: Upgrade 헤더와 몇 가지 프로토콜별 헤더를 사용해 웹 소켓 연결을 요청하여 사용중인 버전과 handshake를 설정한다. 서버가 프로토콜을 지원하는 경우, 동일하게 upgrade: WebSocketConnection: Upgrade 헤더로 응답하여 handshake를 완료한다. handshake가 완료되면 데이터 전송이 시작된다.

웹 소켓의 상호작용은 HTTP upgrade 헤더를 사용하여 업그레이드하거나 웹 소켓 프로토콜로 전환하는 HTTP 요청으로부터 시작된다. 다음 예는 그러한 상호작용을 보여준다.

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket 1️⃣
Connection: Upgrade 2️⃣
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080

1️⃣ Upgrade 헤더
2️⃣ Upgrade 커넥션을 사용함

웹 소켓 지원 서버는 일반적인 200 상태 코드 대신 다음과 같이 요청과 유사한 response를 반환한다.

HTTP/1.1 101 Switching Protocols 1️⃣
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

1️⃣ 프로토콜 스위치

핸드셰이크 성공 후, HTTP upgrade 요청의 기반이 되는 TCP 소켓은 클라이언트와 서버가 모두 메세지를 주고 받을 수 있도록 열린 상태를 유지한다.

WebSocket 서버가 웹 서버(e.g. nginx) 뒤에서 실행 중인 경우 WebSocket upgrade 요청을 WebSocket 서버에 전달하도록 WebSocket 서버에 전달하도록 웹 서버를 구성해야 할 수 있음을 유의하라. 마찬가지로 에플리케이션이 클라우드 환경에서 실행되는 경우 WebSocket 지원과 관련하여 해당 클라우드 제공자의 지침을 확인하길 바란다.

HTTP vs. WebSocket

WebSocket은 HTTP 호환 가능하도록 설계되었고 HTTP 요청으로 시작하지만 두 프로토콜의 아키텍처와 어플리케이션 프로그래밍 모델이 매우 다르다는 것을 이해해야한다.

HTTP와 REST에서 어플리케이션은 여러 URL로 모델링된다. 응용 프로그램과 상호 작용하기 위해 클라이언트는 해당 URL (request-response style)에 엑세스한다. 서버는 HTTP URL, 메소드, 헤더를 기반으로 요청을 적절한 핸들러로 라우팅한다.

반면에 WebSocket에는 일반적으로 초기 연결을 위한 URL이 하나만 있다. 결과적으로 모든 어플리케이션 내 메세지는 동일한 TCP 연결을 통해 흐른다. 이는 완전히 다른 비동기식 이벤트 중심의 메세지 전달 아키텍처를 나타낸다.

WebSocket은 HTTP와 달리 메세지 내용에 의미를 규정하지 않는 저수준 전송 프로토콜이다. 즉, 클라이언트와 서버가 메세지 시멘틱에 동의하지 않으면 메세지를 라우팅하거나 처리 할 방법이 없다.

WebSocket 클라이언트와 서버는 HTTP 핸드셰이크 요청의 Sec-WebSocket-Protocol 헤더를 통해 더 높은 수준의 메시징 프로토콜(e.g. stomp)의 사용을 협상할 수 있다. 그런 것이 없다면 독자적인 규약을 마련할 필요가 있다.

WebSocket을 사용하는 경우

WebSocket은 웹 페이지를 동적이고 대화식으로 만들 수 있다. 그러나 많은 경우에 Ajax, HTTP 스트리밍 혹은 long polling의 조합은 간단하고 효과적인 솔루션을 제공 할 수 있다.

예를 들어 뉴스나 메일, SNS 피드는 동적으로 업데이트하는 것은 맞지만 몇 분마다 업데이트하는 것이 좋다. 반면에 협업, 게임, 금융 앱은 훨씬 더 실시간에 근접해야한다.

대기 시간만이 결정적인 요소는 아니다. 메세지의 크기가 상대적으로 적은 경우 (e.g. 네트워크 장애 모니터링) HTTP 스트리밍이나 polling이 효과적이 솔루션이 될 수 있다. WebSocket을 사용하는데 가장 적합한 사례는 짧은 대기 시간, 고주파수, 대용량의 조합인 경우이다.

또한 인터넷을 통한 제어 범위를 벗어난 제한적인 프록시가 upgrade 헤더를 전달하도록 구성되지 않았거나 유휴 상태로 보이는 오래 지속 중인 연결을 닫기 때문에 WebSocket의 상호 작용을 방해할 수 있는 점을 명심해야 한다. 이는 방화벽 내에서 내부 어플리케이션에 WebSocket을 사용하는 것이 공개 어플리케이션보다 더 간단한 결정임을 의미한다.

WebSocket API

스프링 프레임워크는 WebSocket 메시지를 처리하는 클라이언트측 및 서버측 응용프로그램을 작성하는데 사용할 수 있는 WebSocket API를 제공한다.

도큐먼트의 예제 코드를 직접 따라하면서 진행할 것이다.

Gradle 프로젝트를 생성하고 스프링 웹소켓 종속성을 추가하였다.

 // https://mvnrepository.com/artifact/org.springframework/spring-websocket
    compile group: 'org.springframework', name: 'spring-websocket', version: '5.2.3.RELEASE'

전체 코드는 github에서 확인 할 수 있다.

WebSocketHandler

웹소켓 서버는 간단하게 WebSocketHandler를 구현하거나 TextWebSocketHandlerBinaryWebSocketHandler를 확장함으로써 만들 수 있다. 다음은 TextWebSocketHandler를 사용하는 예이다.

🙎🏻 main/java 하위에 utils 패키지를 생성하고 MyHandler 클래스를 만들어주자.

package utils;

import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class MyHandler extends TextWebSocketHandler {
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
    }
}

다음 예에서 알 수 있듯이 이전의 웹소켓 핸들러를 특정 URL에 매핑하기 위한 웹소켓 Java Configration과 XML namespace를 지원한다.

🙎🏻 필자는 다른 스프링 웹 프로젝트는 XML 네임스페이스를 이용하였으므로 이번엔 자바 config로 구현해 볼 것이다. main/java 하위에 config 패키지를 생성하고 WebSocketConfig 클래스를 만들어주자.

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import utils.MyHandler;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler");
    }

    @Bean
    public WebSocketHandler myHandler(){
        return new MyHandler();
    }
}

아래 코드는 위와 동일한 설정을 XML configuration으로 표현한 것이다.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

앞의 예는 Spring MVC 애플리케이션에서 사용하기 위한 것이며 DispatcherServlet의 구성에 포함되어야한다. 하지만 Spring의 WebSocket 지원이 Spring MVC에 의존하는 것은 아니다. WebSocketHttpRequestHandler의 도움으로 WebSocketHandler를 다른 HTTP 서비스 환경에 통합하는 것은 비교적 간단하다.

WebSocketHandler API를 직접적 vs 간접적으로 사용할 경우(e.g. STOMP 메세징을 통해) 표준 웹소켓 세션(JSR-356)이 동시 전송을 허용하지 않으므로 애플리케이션은 메세지 전송을 동기화시켜야한다. 한가지 옵션은 WebSocketSession을 ConcurrentWebSocketSessionDecorator로 포장하는 것이다.

WebSocket Handshake

HTTP 웹소켓 핸드셰이크 초기화 요청을 사용자 정의하는 가장 쉬운 방법은 HandshakeInterceptor를 통해 핸드셰이크의 "before"와 "after"를 위한 메소드를 사용할 수 있게 하는 것이다. 이런 인터셉터를 사용해 핸드셰이커를 금지하거나 WebSocketSession에서 사용할 수 있는 속성을 만들 수 있다. 다음 예제는 내장형 인터셉터를 사용해 HTTP 세션 attribute를 WebSocket session에 전달한다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler") // 특정 URL에 웹소켓 핸들러를 매핑한다.
                .addInterceptors(new HttpSessionHandshakeInterceptor()); // 핸드셰이크 요청을 인터셉트할 인터셉터
    }

    @Bean
    public WebSocketHandler myHandler(){
        return new MyHandler();
    }
}

동일한 코드를 XML 구성에서는 다음과 같이 작성한다.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
        <websocket:handshake-interceptors>
            <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
        </websocket:handshake-interceptors>
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

보다 고급 옵션은 클라이언트 오리진 검증, 하위 프로토콜 협상과 다른 세부 정보를 포함해 WebSocket 핸드셰이크 단계를 수행하는 DefaultHandshakeHandler를 확장하는 것이다. 아직 지원되지 않는 WebSocket 서버 엔진 및 버전을 위해 커스텀 RequestUpgrategy를 구성해야하는 경우에도 어플리케이션에서 이 옵션을 사용해야 할 수도 있다. (자세한 내용은 여기를 참조) Java config와 XML namespace 모두 커스텀 HandshakeHandler를 구성할 수 있다.

❗️ NOTE
스프링은 WebSocketHandlerDecorator 클래스를 제공하며 WebSocketHandler의 추가 동작을 정의하는데 사용할 수 있다. WebSocket의 Java config나 XML namespace를 사용할 때 로깅 및 예외 처리 구현이 제공되며 기본적으로 추가된다. ExceptionWebSocketHandlerDecoratorWebSocketHandler 메소드에서 발생하는 확인되지 않은 모든 예외를 포착하고 서버 오류를 나타내는 상태 1011로 WebSocket 세션을 닫는다.

배포

스프링 웹소켓 API는 DispatcherServlet이 HTTP WebSocket 핸드셰이크와 기타 HTTP 요청을 모두 지원하는 Sprung MVC 응용 프로그램에 쉽게 통합할 수 있다. 또한 WebSocketHttpRequestHandler를 호출해 다른 HTTP 처리 시나리오에도 쉽게 통합할 수 있다. 단, JSR-356 런타임에 대해서는 특별한 고려사항이 적용된다.

자바 웹소켓 API (JSR-356)은 두 가지 배포 메커니즘을 제공한다. 하나는 시작할 때 서블릿 컨테이너 클래스 경로 스캔(Sevlet3 feature)을 포함하는 것이다. 다른 하나는 Servlet 컨테이너 초기화에 사용할 registration API이다. 이러한 메커니즘 중 어느 것도 WebSocket 핸드셰이크와 (Spring MVC의 DispatcherServlet와 같은) 다른 모든 HTTP 요청을 포함한 모든 HTTP 처리에 단일 "프론트 컨드롤러"를 사용할 수 없다.

이는 Spring의 WebSocket이 JSR-356 런타임에서 실행 될 때에도 서버별로 RequestUpgradeStrategy 구현체를 지원한다는 JSR-356의 한계이다. 그러한 전략은 현재 Tomcat, Jetty, GlassFish, WebLogic, WebSphere, Undertow (and WildFly)에 존재한다.

❗️ NOTE
Java WebSocket API의 초기화 시 한계를 극복하기 위한 request가 만들어졌으며 eclipse-ee4j/websocket-api#211를 따를 수 있다. Tomcat, Underdow, WebSphere는 이를 가능하게 하는 자체 API를 제공하며 Jetty를 통해서도 가능하다. 우리는 더 많은 서버들이 같은 일을 하기를 희망한다.

두 번째 고려사항은 JSR-356이 지원되는 서블릿 컨테이너가 어플리케이션의 시작 속도를 (일부 경우에) 현저하게 늦출 가능성이 있는 ServletContainerInitializer (SCI)스캔을 수행할 것으로 예상된단 점이다. JSR-356이 지원되는 서블릿 컨테이너 버전으로 업그레이드 한 후 큰 영향이 있는 경우 다음 예시처럼 web.xml에서 <absolute-ordering /> 엘리먼트를 사용해 web fragments (and SCI scanning)를 선택적으로 활성화하거나 비활성화 할 수 있어야 한다.

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <absolute-ordering/>

</web-app>

그다음 <name> 엘리먼트에 Servlet 3 Java 초기화 API를 지원하는 스프링의 자체 SpringServletContainer와 같은 것을 정의함으로써 web fragments를 선택적으로 활성화 할 수 있다. 다음 예제는 이를 수행하는 방법을 보여준다.

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <absolute-ordering>
        <name>spring_web</name>
    </absolute-ordering>

</web-app>

서버 구성

각 기본 WebSocket 엔진은 메세지 버퍼 크기, 유휴 시간 초과 등 런타임 특성을 제어하는 configuration 프로퍼티를 제공해야한다. Tomcat, WildFly 및 GlassFish의 경우 다음 예에서 보는 바와 같이 WebSocket Java configuration에 ServletServerContainerFactoryBean를 추가할 수 있다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler") // 특정 URL에 웹소켓 핸들러를 매핑한다.
                .addInterceptors(new HttpSessionHandshakeInterceptor()); // 핸드셰이크 요청을 인터셉트할 인터셉터
    }

    @Bean
    public WebSocketHandler myHandler(){
        return new MyHandler();
    }

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer(){
        // WebSocket의 런타임 특성 제어
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }
}

xml 구성은 다음과 같다.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <bean class="org.springframework...ServletServerContainerFactoryBean">
        <property name="maxTextMessageBufferSize" value="8192"/>
        <property name="maxBinaryMessageBufferSize" value="8192"/>
    </bean>

</beans>

❗️NOTE
클라이언트측 웹소켓 구성의 경우 WebSocketContainerFactoryBean(XML) 또는 ContainerProvider.getWebSocketContainer()(Java configuration)을 사용해야 한다.

허용된 Origins

❗️ NOTE
Origin은 protocol, host, port 3개 부분으로 구성된다.
http://www.rinLog.com:8080/
# protocol: http
# host: www.rinLog.com
# port: 8080
3개 부분이 모두 동일한 경우만 동일한 origin이라고 말할 수 있다.

스프링 프레임워크 4.1.5를 기준으로 WebSocket 및 SockJS의 기본 동작은 동일한 오리진 요청만 수락하는 것이다. 오리진의 모든 목록이나 특정 목록을 허용하는 것도 가능하다. 이런 확인 절차는 대부분의 브라우저 클라이언트를 위해 설계되었다. 또한 다른 유형의 클라이언트가 오리진 헤더 값을 수정하는 것을 막는 것은 없다. (자세한 내용은 RFC 6454: Web Origin Concept참조)

다음의 세가지 행동을 할 수 있다:

  • 동일한 오리진 요청만 허용 (defualt) : 이 모드에서 SockJS가 활성화되면 Iframe HTTP 응답 헤더 X-Frame-OptionsSAMEORIGIN으로 설정되며, JSONP 전송은 요청의 오리진 확인 불가능하므로 비활성화된다. 따라서 이 모드가 활성화된 경우 IE6와 IE7은 지원되지 않는다.
  • 지정된 오리진 목록 허용 : 이 모드에서는 지정된 오리진은 반드시 http:// 또는 https://로 시작해야한다. 이 모드에서 SockJS가 활성화되면 Iframe 전송이 비활성화된다. 따라서 이 모드가 활성화된 경우 IE6에서 IE9까지는 지원되지 않는다.
  • 모든 오리진 허용 : 이 모드를 사용하려면 허가된 오리진 값으로써 *를 사용해야한다. 이 모드에서는 모든 전송을 사용할 수 있다.

다음 예시와 같이 WebSocket 및 Sock JS 허용 오리진을 구성할 수 있다.

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(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }

}

xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers allowed-origins="https://mydomain.com">
        <websocket:mapping path="/myHandler" handler="myHandler" />
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>
profile
🌱 😈💻 🌱

2개의 댓글

comment-user-thumbnail
2020년 10월 14일

이렇게 번역을 해주시다니 정말 감사합니다 ㅠㅜ

답글 달기
comment-user-thumbnail
2020년 11월 9일

안녕하세요 WebSocket으로 특정 사용자에게 notification 알림을 띄우려고 합니다.
특정 사용자의 HttpSession값을 가지고 오기 위해서는
handshakeinterceptor을 사용해야했는데
이는 http에서만 지원이 되었고, https에서는 지원이 안됩니다ㅠㅠ
WebSocket만으로는 특정 사용자의 session값을 가져올 수 없나요?
가지고 올 수 있다면 방법 좀 알려주실 수 있나요 ,,
가져올 수 없다면 Stomp를 사용해야하나요

답글 달기