[Web Socket] Spring Framework에서의 Web Socket

Spring Framework의 Web Socket 지원

Spring 공식 문서를 살펴보면, 다음과 같은 3가지 기능을 지원하고 있는 것을 확인할 수 있다.

 

WebSockets :: Spring Framework

The WebSocket protocol, RFC 6455, provides a standardized way to establish a full-duplex, two-way communication channel between client and server over a single TCP connection. It is a different TCP protocol from HTTP but is designed to work over HTTP, usin

docs.spring.io

  1. Web Socket API
  2. SockJS Fallback
  3. STOMP

1. Web Socket API

WebSocket 메시지를 다룰 수 있는 API 제공

WebSocket API의 핵심은 WebSocketHandler라고 할 수 있다.

WebSocketHandler

import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;

public class MyHandler extends TextWebSocketHandler {

	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		// ...
	}
}

이진 데이터를 다루기 위한 `BinaryWebSocketHandler`, 혹은 텍스트를 다루기 위한 `TextWebSocketHandler`를 상속받아 구현하는 것만으로 간단한 Web Socket 서버를 만들 수 있다. `handleTextMessage` 메서드를 개발자가 원하는 기능으로 Override한다.

WebSocketConfigurer

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");
	}

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

}

`WebSocketConfigurer`를 구현하는 설정 파일을 만들고 @EnableWebSocket 어노테이션을 선언한 뒤, 위에서 작성한 Handler 클래스를 Bean으로 등록하면 Web Socket을 통해 메시지를 주고 받는 서버를 구현할 수 있다.

 

이에 더해, Web Socket의 HTTP Handshake를 커스터마이징할 수 있는 `HandshakeInterceptor` 또한 제공한다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(new MyHandler(), "/myHandler")
			.addInterceptors(new HttpSessionHandshakeInterceptor());
	}

}

이를 통해, Handshake 과정에서 발생하는 인증, 서브 프로토콜 협상 등의 여러 가지 부가적인 작업을 수행할 수 있다.

서버 설정

또한, Spring은 Web Socket 서버 설정을 자바 코드를 통해 제어할 수 있는 기능을 제공하고 있다.

이러한 기능을 통해, Web Socket의 Message Buffer Size, 각종 Time Out과 같은 설정을 쉽게 제어할 수 있다.

예시

@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
    ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
    container.setMaxTextMessageBufferSize(8192);
    container.setMaxBinaryMessageBufferSize(8192);
    return container;
}

또한, Web 서버의 종류(Tomcat, Netty, Jetty 등)에 따라서 다른 설정을 적용할 수 있다.

예시

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(echoWebSocketHandler(), "/echo").setHandshakeHandler(handshakeHandler());
	}

	@Bean
	public DefaultHandshakeHandler handshakeHandler() {
		JettyRequestUpgradeStrategy strategy = new JettyRequestUpgradeStrategy();
		strategy.addWebSocketConfigurer(configurable -> {
				policy.setInputBufferSize(8192);
				policy.setIdleTimeout(600000);
		});
		return new DefaultHandshakeHandler(strategy);
	}

}

2. SockJS Fallback

WebSocket이 정상적으로 작동하지 않는 환경을 위한 SockJS 프로토콜 지원

SockJS := 브라우저 환경에서 실행되는 JS 라이브러리

아래 코드를 통해 SockJS 지원 기능을 활성화 할 수 있다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(myHandler(), "/myHandler").withSockJS();
	}

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

}

3. STOMP

STOMP 브로커 기능을 제공

⇒ Web Socket을 직접 사용했을 때보다 풍부한 기능을 제공한다.

  • Spring Security를 이용할 수 있다.

STOMP 사용 설정

우선, STOMP를 사용하기 위해서는 아래와 같은 설정 파일(Config Class)이 필요하다.

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/portfolio");
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.setApplicationDestinationPrefixes("/app");
		registry.enableSimpleBroker("/topic");
	}
}

@EnableWebSocketMessageBroker 어노테이션을 선언하고, `WebSocketMessageBrokerConfigurer` 인터페이스를 구현하면 된다.

registerStompEndpoints 메서드

해당 메서드에서 `addEndpoint` 를 통해서

Web Socket 클라이언트가 Handshake를 위해 접속해야 하는 서버 주소를 설정할 수 있다.

configureMessageBroker 메서드

해당 메서드의 `setApplicationDestinationPrefixes`를 통해

클라이언트가 메시지를 발행하는 주소를 설정할 수 있다.

`enableSimpleBroker`를 통해

클라이언트가 메시지를 구독하는 주소를 설정할 수 있다.

Annotated Controller : @MessageMapping

위와 같은 설정을 통해 STOMP가 활성화된다면, Controller에서 @MessageMapping 어노테이션을 사용해,

클라이언트로부터 들어오는 STOMP 메시지를 핸들링할 메시지를 지정할 수 있다.

 

예시로, 클라이언트에서 “app/greeting”이라는 경로로 메시지를 보낸다면, 위에서 설명한 `setApplicationDestinationPrefixes`에 의해 “app”은 제외가 되고 “/greeting”에 해당하는 @MessageMapping 어노테이션이 선언되어 있는 Controller의 메서드로 메시지를 전달하게 된다.

이곳에서 로직이 수행된 후 반환되는 String 값은 STOMP 메시지의 Payload로 변환되어 `enableSimpleBroker`에서 설정한 “/topic”이 포함된, “/topic/greeting”을 구독하고 있는 클라이언트에게 메시지를 발행한다.

 

STOMP의 Controller에서는 아래 표에 나타나 있는 파라미터들을 사용할 수 있다.

Payload, Header, Security의 Principal 등을 인자로 받을 수 있는 것을 확인할 수 있다.

@DestinationVariable

@MessageMapping 어노테이션에 선언된 template 형태의 변수를 받아올 때 사용한다.

⇒ 일반적인 Controller에서 @PathVariable 어노테이션과 비슷한 역할을 한다. 따라서, Endpoint를 통해서도 변수를 받아 처리할 수 있다.

서버에서 클라이언트로 메시지를 전송 : SimpMessagingTemplate

`SimpMessagingTemplate`이라는 Bean을 이용해 서버에서 클라이언트로 메시지를 전송하는 기능도 제공한다.

@Controller
public class GreetingController {

	private SimpMessagingTemplate template;

	@Autowired
	public GreetingController(SimpMessagingTemplate template) {
		this.template = template;
	}

	@RequestMapping(path="/greetings", method=POST)
	public void greet(String greeting) {
		String text = "[" + getTimestamp() + "]:" + greeting;
		this.template.convertAndSend("/topic/greetings", text);
	}

}

클라이언트에서 보내는 메시지를 처리하는 것과는 다르게, 서버에서 원하는 메시지를 원하는 경로의 클라이언트로 전송할 수 있다.

Flow of Message

STOMP가 활성화되면 Spring 서버는 접속된 Web Socket 클라이언트들에 대한 STOMP Message Broker로 작동하게 된다.

메시지가 흐르는 순서대로 살펴보자.

  1. 상단의 ClientInboundChannel(request channel)은 Web Socket 클라이언트로부터 받은 메시지를 서버로 넘겨주는 역할을 한다.
  2. broker channel은 서버 내에서 발생한 메시지를 SimpleBroker에게 전달하는 역할을 한다.
  3. SimpleBroker는 클라이언트들로부터 받은 구독 요청들을 메모리에 관리하고 있다가 목적지가 일치하는 메시지가 들어올 때, 해당하는 클라이언트들에게 전송하는 역할을 한다.
  4. ClientOutboundChannel(response channel)은 서버의 메시지를 Web Socket 클라이언트로 전달하는 역할을 한다.