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 정보를 함께 포함한다.
위와 같은 과정을 통해서, STOMP 는 Publish-Subscribe 매커니즘을 제공한다.
즉, Broker를 통해서 다른 사용자들에게 메시지를 보내거나 서버가 특정 작업을 수행할 수 있도록 메시지를 보낼 수 있게 되는 것이다.
만약 스프링에서 지원하는 STOMP 를 사용하게 된다면, 스프링 WebSocket 애플리케이션은 STOMP Broker로 동작된다.
스프링에서 지원하는 STOMP 는 다양한 기능을 제공한다.
- 메시지를 @Controller의 메시지 핸들링하는 메서드로 라우팅하거나,
- Simple In-Memory Broker를 이용해서 Subscribe 중인 다른 클라이언트에게 메시지를 브로드캐스팅한다.
- Simple In-Memory Broker 는 클라이언트의 Subscribe 정보를 자체적으로 메모리에 유지한다.
- 뿐만 아니라, 스프링은 RabbitMQ, ActiveMQ 같은 외부 Messaging System을 STOMP 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 모듈은 스프링 프레임워크의 통합된 메시징 애플리케이션을 위한 근본적인 지원을 한다.
다음 목록에서는 몇 가지 사용 가능한 메시징 추상화에 대해 설명한다.
- Message 는 headers와 payload를 포함하는 메시지의 representation 이다.
- MessageHandler는 Message 처리에 대한 계약이다.
- MessageChannel은 Producers과 Consumers의 느슨한 연결을 가능하게 하는 메시지 전송에 대한 계약이다.
- SubscribableChannel는 MessageHandler 구독자 (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 |