SpringBoot

Spring WebSocket

똑똑한망치 2024. 7. 16. 18:11
728x90
반응형

Spring Framework는 WebSocket API를 제공한다.

여기서 중요한 점은 Spring에서 제공하는 WebSocket API는 Spring MVC 기술에 종속되지 않는다는 것이다.

WebSocket 서버는 WebSocketHandler 인터페이스의 구현체를 통해서, 각 경로에 대한 핸들러를 구현할 수 있다. 

뿐만 아니라, Message 형식에 따라 TextWebSocketHandler 또는 BinaryWebSocketHandler 핸들러를 확장해 구현할 수도 있다.

 

 

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 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를 거친다.

Spring은 데코레이터 패턴을 이용해서 WebSocketHandler에 대한 추가적인 작업을 처리할 수 있도록 WebSocketHandlerDecorator 객체를 제공한다.

예를 들어 Message, Session 등의 정보에 대한 로깅 등의 작업을 추가할 수 있다.

아래 사진은 실제로 Spring에 등록된 WebSocketHandlerDecorator 구현체들이며, 연속적으로 전파되어 실행되는 것을 확인할 수 있다.

 

몇가지 Decorator를 살펴보자.

 

(1) ExceptionWebSocketHandlerDecorator

  • WebSocket 실행 과정에서 발생하는 예외를 모두 잡아서 처리한다.

 

(2) LoggingWebSocketHandlerDecorator

  • Message, Session 정보를 Logging 한다.

 

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

	...
}

 

 

 

STOMP 이란 ?

STOMP은 Simple Text Oriented Messaging Protocol 약자로 TCP 또는 WebSocket 같은 양방향 네트워크 프로토콜 기반으로 동작한다.

이름에서도 알 수 있듯이, STOMP는 텍스트 지향 프로토콜이지만 Message Payload에는 Text 또는 Binary 데이터를 포함할 수도 있다.

STOMP은 HTTP 위에서 동작하는 Frame 기반의 프로토콜이며, Frame은 아래와 같은 형식을 가지고 있다.

COMMAND
header1:value1
header2:value2

Body^@

 

클라이언트는 Message를 전송하기 위해 SEND, SUBSCRIBE, COMMAND를 사용할 수 있다. 

또한,  SEND, SUBSCRIBE, COMMAND 요청 Frame에는 메시지가 무엇이고 누가 받아서 처리할 지에 대한 Header 정보를 함께 포함한다.

위와 같은 과정을 통해서, STOMPPublish-Subscribe 매커니즘을 제공한다.

즉, Broker를 통해서 다른 사용자들에게 메시지를 보내거나 서버가 특정 작업을 수행할 수 있도록 메시지를 보낼 수 있게 되는 것이다.

만약 스프링에서 지원하는 STOMP 를 사용하게 된다면, 스프링 WebSocket 애플리케이션은 STOMP Broker로 동작된다.

 

스프링에서 지원하는 STOMP 는 다양한 기능을 제공한다.

  • 메시지를 @Controller의 메시지 핸들링하는 메서드로 라우팅하거나,
  • Simple In-Memory Broker를 이용해서 Subscribe 중인 다른 클라이언트에게 메시지를 브로드캐스팅한다.
    • Simple In-Memory Broker 는 클라이언트의 Subscribe 정보를 자체적으로 메모리에 유지한다.
  • 뿐만 아니라, 스프링은 RabbitMQ, ActiveMQ 같은 외부 Messaging SystemSTOMP Broker로 사용할 수 있도록 지원하고 있다.

만약 클라이언트가 특정 경로에 대해서 아래와 같이 Subscribe 한다면, 서버는 원할 때마다 클라이언트에게 메시지를 전송할 수 있다.

SUBSCRIBE
id:sub-1
destination:/topic/something.*

^@

 

또한, 클라이언트는 서버에 메시지를 전달할 수 있는데, 서버는 @MessageMapping 된 메서드를 통해서 해당 메시지를 처리할 수 있다.

뿐만 아니라, 서버는 Subscribe 한 클라이언트들에게 메시지를 브로드캐스팅할 수도 있다.

SEND
destination:/queue/something
content-type:application/json
content-length:38

{"key1":"value1","key2":"value2", 38}^@

 

Destination 정보는 STOMP 서버 구현체마다 달라질 수 있기 때문에 각 구현체의 스펙을 살펴봐야 한다.

그러나, 일반적으로 /topic 문자열로 시작하는 구문은 일대다(1 : N) 관계의 publish-subscribe를 의미하고, /queue 문자열로 시작하는 구문은 일대일(1 : 1) 관계의 메시지 교환을 의미한다.

STOMP 서버는 MESSAGE COMMAND를 사용해 모든 Subscriber 에게 메시지를 브로드캐스트할 수 있다.

MESSAGE
message-id:d4c0d7f6-1
subscription:sub-1
destination:/topic/something

{"key1":"value1","key2":"value2"}^@

 

 

STOMP 장점

Spring Framework  Spring Security STOMP 프로토콜을 사용하여, WebSocket만 이용할 때 보다 더 풍부한 프로그래밍 모델을 제공할 수 있는데 하나씩 살펴보자.

  • 메시징 프로토콜을 만들고, 메시지 형식을 커스터마이징할 필요가 없다.
  • RabbitMQ, ActiveMQ 같은 Message Broker을 이용해서, subscription을 관리하고 메시지를 브로드캐스팅할 수 있다.
  • WebSocket 기반으로 각 커넥션마다 WebSocketHandler를 구현하는 것보다, @Controller된 객체를 이용해서 조직적으로 관리할 수 있다.
    • 즉 메시지들은 STOMP Destination 헤더를 기반으로, @Controller 객체의 @MethodMapping 메서드로 라우팅된다.
  • STOMP Destination  Message Type을 기반으로 메시지를 보호하기 위해, Spring Security를 사용할 수 있다.

 

STOMP 사용

서버

스프링은 WebSocket 또는 SockJS 기반으로 STOMP를 위해 spring-messaging and spring-websocket 모듈을 제공한다.

아래 예제와 같이, STOMP 설정을 할 수 있는데 기본적으로 커넥션을 위한 STOMP Endpoint를 설정해야 한다.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/test").setAllowedOrigins("*").withSockJS();
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.setApplicationDestinationPrefixes("/simple")
			.enableSimpleBroker("/topic", "/queue");
	}
}
  • /test 는 WebSocket 또는 SockJS 클라이언트가 WebSocket Handshake로 커넥션을 생성할 경로이다.
  • /simple 경로로 시작하는 STOMP 메시지의 Destination 헤더는 @Controller 객체의 @MessageMapping 메서드로 라우팅된다.
  • 내장된 메시지 브로커를 사용하여 클라이언트에게 subscriptions, broadcasting 기능을 지원한다.
  • 또한, /topic 또는 /queue로 시작하는 Destination 헤더를 가진 메시지를 브로커로 라우팅한다.

 

Message Flow

일단 STOMP Endpoint를 노출하면, 스프링 애플리케이션은 연결된 클라이언트에 대한 STOMP Broker가 된다.

 

구성 요소

spring-message 모듈은 스프링 프레임워크의 통합된 메시징 애플리케이션을 위한 근본적인 지원을 한다.

다음 목록에서는 몇 가지 사용 가능한 메시징 추상화에 대해 설명한다.

  • Messageheaderspayload를 포함하는 메시지의 representation 이다.
  • MessageHandlerMessage 처리에 대한 계약이다.
  • MessageChannelProducersConsumers의 느슨한 연결을 가능하게 하는 메시지 전송에 대한 계약이다.
  • SubscribableChannelMessageHandler 구독자 (Subscribers)를 위한 MessageChannel 이다.
    • 즉, subscribers를 관리하고, 해당 채널에 전송된 메시지를 처리할 subscribers를 호출한다.

아래 그림은 내장 메시지 브로커를 사용한 경우의 컴포넌트 구성을 보여준다.

  • clientInboundChannel은 WebSocket 클라이언트로부터 받은 메시지를 전달한다.
  • clientOutboundChannel은 WebSocket 클라이언트에게 메시지를 전달한다.
  • brokerChannel은 서버의 애플리케이션 코드 내에서 브로커에게 메시지를 전달한다.

 

동작 흐름

  • WebSocket 커넥션으로부터 메시지를 전달받는다.
  • STOMP Frame으로 디코드한다.
  • 스프링에서 제공하는 Message Representation 으로 변환한다.
  • 추가 처리를 위해 clientInboundChannel로 전송한다.
    • STOMP Message 의 Destination 헤더라 /app으로 시작한다면, @MessageMapping 정보와 매핑된 메서드를 호출한다.
    • 반면에 Destination 헤더가 /topic 또는 /queue 로 시작한다면, 메시지 브로커로 바로 (직접) 라우팅된다.

 

Annotated Controllers

애플리케이션은 클라이언트로부터 받은 메시지를 처리하기 위해 @Controller 클래스를 사용할 수 있다.

이러한 컨트롤러는 @MessageMapping, @SubscribeMapping, @ExceptionHandler 메서드를 선언할 수 있는데 구체적으로 어떤 역할을 하는 지 살펴보자.

 

@MessageMapping

@MessageMapping 메서드는 지정한 경로를 기반으로 메시지를 라우팅할 수 있다.

@MessageMapping 은 메서드뿐만 아니라 타입 레벨, 즉 클래스에도 설정할 수 있는데 이는 컨트롤러 안에서 공통된 경로를 제공하기 위해서 사용된다.

기본적으로 매핑은 Ant-Style Path 패턴으로 구성하고, Template 변수도 지원한다. (ex, /something, /something/{id} )

Template 변수는 @DestinationVariable 로 선언한 메서드 인자를 통해서 전달받을 수 있다.

 

Method Arguments

@DestinationVariable 과 같은 메서드에서 지원하는 인자 목록이다.

  • Message
    • 완전한 Message 정보에 접근한다.
  • MessageHeader
    • Message 안에 Header 정보에 접근한다.
  • MessageHeaderAccessor, SimpMessageHeaderAccessor, StompHeaderAccessor
    • 타입이 지정된 접근자 메서드를 통해서 Header 정보에 접근한다.
  • @Payload
    • MessageConverter 의해서 변환된 메시지의 Payload에 접근한다.
    • 만약 다른 인자와 일치하지 않으면 매칭되기 때문에 반드시 요구되지는 않는다.
    • Payload 인자를 자동으로 검증하기 위해 스프링의 @Validated 을 함께 사용할 수도 있다.
  • @Header
    • 구체적인 Header 값에 접근한다.
  • @DestinationVariable
    • 메시지의 Destination 헤더의 경로를 기반으로 Template 변수의 값을 추출하여 접근한다.
    • Value는 선언된 타입에 따라 필수적으로 변환된다.
  • java.security.Principal
    • WebSocket HTTP Handshake 시 로그인 한 사용자를 반영한다.
@Controller
public class TestController {

	@MessageMapping("/good/{id}")
	public String handle(Message message, MessageHeaders messageHeaders, 
		MessageHeaderAccessor messageHeaderAccessor, SimpMessageHeaderAccessor simpMessageHeaderAccessor, 
		StompHeaderAccessor stompHeaderAccessor, @Payload String payload, 
		@Header("destination") String destination, @Headers Map<String, String> headers,
		@DestinationVariable String id) {

		System.out.println("---- Message ----");
		System.out.println(message);

		System.out.println("---- MessageHeaders ----");
		System.out.println(messageHeaders);

		System.out.println("---- MessageHeaderAccessor ----");
		System.out.println(messageHeaderAccessor);

		System.out.println("---- SimpMessageHeaderAccessor ----");
		System.out.println(simpMessageHeaderAccessor);

		System.out.println("---- StompHeaderAccessor ----");
		System.out.println(stompHeaderAccessor);

		System.out.println("---- @Payload ----");
		System.out.println(payload);

		System.out.println("---- @Header(\"destination\") ----");
		System.out.println(destination);

		System.out.println("----  @Headers ----");
		System.out.println(headers);

		System.out.println("----  @DestinationVariable ----");
		System.out.println(id);

		return payload;
	}
}

 

 

 

 

 

[참고]

https://velog.io/@koseungbin/WebSocket

 

WebSocket

이 글은 Spring WebSocket(https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.htmlWebSocket 프로토콜은 표준된 방법으로 서버-클라이언트 간

velog.io

반응형

'SpringBoot' 카테고리의 다른 글

QueryDSL 이란?  (0) 2024.07.17
JPA Entity에 @Setter를 지양하는 이유  (0) 2024.06.29
SpringSecurity Bcrypt 를 이용한 비밀번호 암호화  (0) 2024.06.26
@Controller와 @RestController 차이점  (0) 2024.06.25
RedirectAttributes  (0) 2024.05.02