(18장) WebSocekt과 STOMP를 사용하여 메시징하기

웹 소켓은 단일 소켓을 통해서 풀듀플렉스(full-duplex) 통신을 제공하는 프로토콜이다.

웹 브라우저오 서버간의 비동기 메시징을 활성화한다. 풀 듀플렉스는 서버가 브라우저에 메시지를 전송할 수 있고, 브라우저도 서버에 메시지를 전송할 수 있음을 의미한다.
스프링4.0은 웹 소켓 통신을 지원하며, 다음을 포함한다.


  • 메시지 전송과 수신을 위한 하위 레벨 API
  • 스프링 MVC 컨트롤러에서의 메시지 처리를 이한 상위 레벨 API
  • 메시지 전송을 위한 메시징 템플릿
  • 브라우저, 서버, 프록시에서 웹 소켓 지원 부족을 지원하기 위한 SockJS

1. 스프링의 하위 레벨 웹 소켓 API사용하기


간단한 형태로 웹 소켓은 두 애플리케이션 사이의 통신 채널이다. 풀 듀플렉스이므로 양쪽 종단은 메시지를 전송할 수 있고, 다른 쪽은 그 메시지를 받아서 처리한다.

웹 소켓 통신은 여러 종류의 애플리케이션 사이에서 사용될 수 있다. 그러나 웹 소켓의 대부분은 서버와 브라우저 기반 애플리케이션 사이의 통신을 용이하게 하기 위해서 사용된다. 브라우저에서의 자바스크립트 클라이언트는 서버에 대한 연결을 오픈하고, 서버는 그 연결을 통해 브라우저에 업데이트하라고 보낸다. 이러한 방법은 업데이트를 위해 서버를 폴링(Polling)하는 일반적인 방식보다 더 효율적이며, 좀 더 자연스럽다.

하위 레벨 웹 소켓을 지원하기 위해서 스프링에서 메시지를 처리하고, WebSocketHandler를 구현한 클래스를 작성한다.

public interface WebSocketHandler{
  void afterConnectionEstablished(WebSocketSession session)throws Exception;

 void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;

 void handleTransportError(WebSocketSession session,Throwable exception) throws Exception;

 void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)throws Exception;

 boolean supportPartialMessages();

}

--->AbstractWebSocketHandler는 WebSocketHandler의 추상화 구현체다.

WebSocketHandler에서 정의된 다섯 가지 메소드와 더불어 AbstractWebSocketHandler에서 정의된 세 가지 추가 메소드를 오버라이드 한다.
  • handleBinaryMessage()
  • handlePongMessage()
  • handleTextMessage()

*TextWebSocketHandler는 바이너리 메시지를 처리하지 못하는 AbstractWebSocketHandler의 서브클래스다. 바이너리 연결 수신 시, 웹 소켓 연결에 밀접한 handleBinaryMessage()를 오버라이드 한다.



여태까지 한것은 메시지 핸들러 클래스를 사용할 수 있게 한것이며, 이 클래스를 설정하면, 스프링에서는 메시지를 핸들러로 보낼 수 있다. 스프링의 자바 설정에서 @EnableWebSocket을 사용하여 설정 클래스를 애너테이션하고, 다음 코드에 나타낸 것과 같이 WebSocketConfigurer인터페이스를 구현한다.

<자바 설정에서 웹 소켓을 활성화하고 메시지 핸들러를 매핑한다.>

@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{

  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry){
    registry.addHandler(macroHandler(),"/macro"); <-"/macro"로 MacroHandler를 매핑
  }

  @Bean
  public MacroHandler macroHandler(){ <---MacroHandler 빈 선언
    return new MacroHandler();
   }
}



XML로 스프링을 설정한다면 다음과 같이 웹 소켓 네임스페이스를 이용한다.

<WebSocket 네임스페이스는 웹 소켓을 위한 XML설정을 활성화한다.>


<?xml version="1.0" encoding="UTF-8"?>

<beans: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/websocket
              http://www.springframework.org/schema/websocket/spring-websocket.xsd
              http://www.springframework.org/schema/beans
             http://www.springframework.org/schema/beans/spring-beans.xsd">

<websocket:handlers>
  <websocket:mapping handler="macroHandler" path="/macro" /> <-"/macro"로 MacroHandler를 매핑

</websocket:handlers>

<bean id="macroHandler" class="macropolo.MacroHandler" /> <-MacroHandler 빈 선언

</beans>



2. 웹 소켓 지원 부족에 대해 대응하기

웹 소켓이 지원되면, 웹소케싀 효과는 환상적이다. 하지만, 지원되지 않는다면, 다른 대체 계획을 세워야 한다.

운좋게도 웹 소켓을 대체할 수 있는 것으로 SockJS가 있다. SockJS는 외견상 가장 가까운 웹 소켓 API를 미러링하는 웹 소켓 애뮬레이터다. 웹 소켓이 유효하지 않을 때, 다른 형태의 통신 방법으로 선택할 수 있다. SockJS가 웹소켓이 사용되는 곳을 따라가서 대체할 수 있다.

SockJS에는 다양한 옵션들이 있지만, SockJS는 웹 소켓 지원이 유비쿼터스적이고, 대응 전략을 처리할 경우에 일관적인 프로그래밍 모델을 개발할 수 있게한다.

예를들면, 서버 쪽에서 SockJS통신을 하기 위해서 스프링 설정에서 통신을 요청한다. 다음 코드의 registerWebSocketHandlers()를 재방문하여 약간의 추가를 한 뒤 웹 소켓을 사용한다.

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


웹 소켓은 브라우저-서버 통신을 사용할 수 있게하고, SockJS는 웹 소켓이 지원되지 않을 때의 대응 통신 방법을 제공한다. 그러나 이 두가지 방법은 통신 형태가 사용하기에 너무 하위 레벨이다. 
브라우저-서버 통신에 대해서 적합한 메시징 시맨틱을 추가하기 위해 웹 소켓 위에 STOMP(Simple Text Oriented Messaging Protocol)를 놓아보자.


3. STOMP 메시징 작업하기


HTTP가 존재하지 않고, TCP소켓 외 아무것도 없이 웹 애플리케이션을 만들어야 한다고 가정해보자. 정신이 없더라도, 효율적 통신을 수행하기 위해서 클라이언트와 서버가 동의한 유선 프로토콜을 수정해야 한다. 간단히 말하면, 이는 어려운 작업이다.

고맙게도 HTTP프로토콜은 웹 브라우저가 어떻게 요청하는지 웹 서버가 그 요청에 대해서 어떻게 응답하는지에 대해 자세한 사항을 알려준다. 결과적으로 대부분의 개발자들은 낮은 레벨의 TCP소켓 통신을 수행하는 코드를 만들 필요가 없다.

웹소켓(또는 SockJS)을 직접 사용하는 것은 TCP소켓만을 사용하는 웹 애플리케이션을 개발하는 것과 많이 비슷하다. 상위 레벨 유선 프로토콜을 사용하지 않고, 애플리케이션 간에 전송되는 메시지 시맨틱을 정의하는 것은 사용자의 의사에 달려있다. 그리고 연결의 양 끝단에서 해당 시맨틱 사용을 동의하는지를 확인해야 한다.

다행히도 원시(raw)웹 소켓의 연결을 사용할 필요는 없다. TCP소켓 상위에 존재하는 요청-응답 모델의 HTTP와 마찬가지로 STOMP는 웹 소켓 상위에 메시지 시맨틱을 정의하기 위한 프레임 기반의 유선 포맷을 놓는다. STOMP메시지 프레임은 HTTP요청 구조와 매우 유사하다.

HTTP요청과 응답처럼 STOMP프레임은 명령어, 한 개 이상의 헤더, 페이로드로 구성된다.
다음은 데이터 전송을 위한 STOMP프레임이다.

예)
SEND
destination:/app/macro
content-length:20

{\"message\":\"macro!\"}


-->STOMP 명령어는 SEND이고, 무엇인가가 전송됨을 나타낸다. 이는 두 개의 헤더 뒤에 나온다. 하나는 메시지가 전송되는 목적지이고, 다른 하나는 페이로드의 크기다. 다음은 빈 라이인 있고, 다음은 페이로드를 포함하는 프레임ㅇ다.
여기서는 JSON메시지다.

destinatin헤더는 STOPM프레임에서 가장 재미있는 부분이다 STOMP가 JMS또는 AMQP처럼 메시징 프로토콜이라는 것을 알려준다. 실제로 목적지로 전달되는 메시지는 실제 메시지 브로커에 의해서 지원된다. 또 다른 측면에서 메시지 핸들러는 전송된 메시지를 수신하기 위한 목적지 대상으로 수신 대기를 한다.

3.1 STOMP 메시징 사용하기

RequestMapping 애너테이션된 메소드가 HTTP요청을 처리하는 방법과 매우 유사하게 스프링 MVC에서의 STOMP 메시지를 처리하기 위한  @MessageMapping을 가지고 컨트롤러 메소드를 어떻게 애너테이션할지를 살펴본다. 
그런 @RequestMapping과 달리 @MessageMapping은  @EnableWebMvc애너테이션으로 사용할 수 없다.

<@EnableWebSocketMessageBroker는 웹 소켓을 통해 STOMP를 사용한다.>

@Configuration
@EnableWebSocketMessageBroker <--STOMP 메시징 사용
public class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer{

 @Overrid
  public void registerStompEndpoints(StompEndpointRegistry registry){
   registry.addEndpoint("/marcopolo").withSockJS(); <--/marcopolo를 통한 SockJS사
 }

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


--->여기서는 @EnableWebSocketMessageBroker를 사용하여 애너테이션된다. 이는 설정 클래스가 웹 소켓만을 설정하는 것이 아니라는 것을 나타낸다. 브로커 기반의 STOMP메시징을 설정한다. 
STOMP 엔드포인트로서 /marcopolo를 등록하기 위해 registerStompEndpoints() 메소드를 오버라이드 한다. 이 경로는 메시지를 전송하거나 수신하는 목적지 경로와는 다르다. 목적지 경로를 받거나 보내기 전에 클라이언트가 연결할 엔드포인트를 나타낸다.

configureMessageBroker()메소드에 의해서 오버라이딩되는 간단한 메시지 브로커를 설정한다. 이 메소드는 옵션이며, 만약에 오버라이드 할 수 없으면, 접두사 /topic을 가지는 메시지를 처리하기 위해 설정되는 인메모리(in-memory)브로커를 사용한다. 그러나 이 예제에서는 오버라이드 할 수 있으며, 메시지 브로커는 접두어 /topic과 /queue를 가지는 메시지를 처리한다. 추가적으로, 애플리케이션에서 사용되는 메시지는 접두어 /app를 가진다.


다음 그림은 메시지가 어떻게 흘러가는지를 나타낸다. 메시지가 도착하면 목적 접두어는 메시지가 어떻게 처리되는지를 결정한다. 그림에서는 애플리케이션 목저지는 /app접두어를 가지고, 브로커 목적지는 /topic, /queue를 가진다. 애플리케이션 목적지에 대한 메시지는 @MessageMapping애너테이션된 컨트롤러 메소드로 직접 라우팅 된다.
브로커를 향한 메시지는 @MessageMapping애너테이션되는 메소드에 의해서 반환되는 값으로부터의 메시지를 포함하며, 브로커로 라우팅되고, 목적지의 고객 대상으로 결국 전달된다.





STOMP 브로커 릴레이 사용하기

간단한 브로커는 시작하기에 적합하지만, 이는 메모리 기반이므로 각 노드가 브로커와 메시지 셋을 관리하는 클러스터에서는 적합하지 않다.

제품 적용을 위해서 RabbitMQ또는 ActiveMQ같은 실제 STOMP 적용 가능한 브로커를 사용하여 웹 소켓 메시지를 지원한다. 해당 브로커는 더 확장 가능하고, 견고한 메시지 전송이 가능하지만, STOMP명령어 전체 세트를 지원하는 것은 아니다. 도큐맨테이션에 따라서 STOMP에 대한 브로커를 셋업해야 한다.
일단 브로커가 준비되면 다음과 같이  configureMessageBroker()메소드를 오버랑딩하여 기본 인 메모리 브로커를 STOMP브로커로 교체한다.

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

-->configureMessageBroker()의 첫번쨰 라인은 STOMP브로커 릴레이를 사용할 수 있고, 목적지 접두어를 /topic과 /queue로 설정한다. /topic또는 /queue로 시작하는 목적지를 가지는 메시지가 STOMP브로커 대상인 스프링에 대한 단서를 제공한다.

만약, RabbitMQ는  /temp-queue, /exchange, /topic, /queue, /amq/queue, /replyqueue/ 타입의 목적지에 대해서만 허용한다. 지원 목적지 타입과 목적을 위한 브로커 도큐맨테이션을 참조한다.

두번쨰 라인은 애플리케이션 접두어를 /app으로 설정한다. 목적지가 /app로 시작하는 멧지는 @MessasgeMapping메소드로 라우팅되며, 브로커 큐나, 토픽으로 전달되지 않는다.


<STOMP브로커 릴레이는 STOMP메시지를 처리하기 위한 실제 메시지를 위임한다.>




*enableStompBrokerRelay()와 setApplicationDestinationPrefix()모두 변수-길이 문자열 인자를 가짐을 주의하고, 따라서 여러개의 목적지와 애플리케이션 접두어를 설정한다.
예를들어,

@Override
  public void configureMessageBroker(MessageBrokerRegistry registry){
   registry.enableStompBrokerRelay("/queue","/topic");
   registry.setApplicationDestinationPrefixes("/app","/foo");
 }

기본적으로 STOMP브로커 릴레이는 브로커가 포트 61613을 통해서 메시지 수신 대기함을 가정하며, 클라이언트 사용자명과 암호는 모두 "guest"다. STOMP브로커가 다른 서버에 있거나 다른 클라이언트 자격을 설정한다면, STOMP브로커 릴레이를 사용할 때 상세한 사항들을 설정한다.

@Override
  public void configureMessageBroker(MessageBrokerRegistry registry){
   registry.enableStompBrokerRelay("/queue","/topic");
            .setRelayHost("rabbit.someotherserver");
            .setRelayPort("62623");
            .setClientLogin("marcopolo");
            .setClientPasscode("letmein01");
   registry.setApplicationDestinationPrefixes("/app","/foo");
 }


3.2 클라이언트로부터의 STOMP 메시지 처리


스프링은 STOMP 메시지 처리를 위한 스프링 MVC와 매우 유사한 프로그래밍 모델을 제공한다. 실제로 STOMP의 핸들러 메소드는 @Controller애너테이션된 클래스 멤버다.

@MessageMapping으로 애너테이션되는 메소드는 특정 목적지에 도달시 메시지를 처리한다. 다음 코드는 간단한 컨트롤러 클래스이다.
<컨트롤러의 STOMP메시지를 처리하는 @MessageMapping>

@Controller
public class MarcoController{

   private static final Logger logger=
              LoggerFactory.getLogger(MarcoController.class);

  @MessageMapping("/marco")  <--/app/marco목적지에 대한 메시지 처리
   public void handleShout(Shout incoming){
   logger.info("Received Messaeg:"+incoming.getMessage());
  }
}

언뜻 보면 이는 다른 스프링 MVC컨트롤러 클래스와 유사하다. @Controller로 애너테이션 되며, 선택되고, 컴포넌트 스캐닝에 의해서 빈으로 등록된다. 또한, 핸들러 메소드를 가지며,@Controller클래스와 유사하다.

그러나 여기선 @MessageMapping으로 애너테이션됫으며 이는 handleShout()가 특정 목적지에 대한 메시지를 처리해야 함을 나타낸다. 이 경우에 목적지는 /app/macro이다.("/app"접두어는 사용자가 설정한 접두어가 애플리케이션 목적지 접두어로 설정됨을 암시한다.)

handleShout()는 Shout파라미터를 사용하므로 STOMP 메시지의 페이로드는 스프링의 메시지 변환기들 중 하나를 사용하여 Shout으로 변경된다. Shout 클래스는 메시지를 전달하는 단순한 유일 특성 자바빈이다.

public class Shout{
  private String message;

   public String getMessage(){
      return message;
}

public void setMessage(String message){
   this.message=message;
}
}

<스프링은 몇 개의 메시지 변환기를 사용하여 메시지 페이로드를 자바 타입으로 변환 시킬 수 있다.>


  • ByteArrayMessageConverter : application/octet-stream의 MIME 타입 메시지를 byte[] 로 변환 또는 역으로 변환
  • MappingJackson2MessageConverter : application/json의 MIME 타입 메시지를 자바 객체로 변환 또는 역으로 변환
  • StringMessageConverter : text/plain 의 MIME 타입 메시지를 문자열로 변환 또는 역으로 변환

구독 처리

@MessageMapping 애너테이션과 더불어 스프링은 @SubscribeMapping 애너테이션을 제공한다. 
@MessageMapping 처럼 @SubscribeMapping메소드는 AnnotationMethodMessageHandler를 통해서 메시지를 수신 받는다. 

약간 특이해 보이겠지만, 발신(outgoing)메시지는 접두어 /topic 또는 /queue를 가지고 브로커 목적지로 전달된다. 이러한 목적지를 사용하여 클라이언트는 구독하지만, 접두어 /app를 가지는 목적지에 대해서는 구독하지 않는다. 클라이언트가 /topic와 /queue 목적지를 구독한다면, @SubscribeMapping 메소드에는 이러한 구독 처리 방법이 없다.
이러한 내용이 진짜라면, @SubscribeMapping의 주된 용례는 요청-응답 패턴 구현이다.
요청-응답 패턴에서 클라이언트는 해당 목적지에 대한 1회 응답이 예상되는 구독을 한다.

예를들면, 
@SubscribeMapping({"/macro"})
public Shout handleSubscription(){
   Shout outgoing=new Shout();
   outgoing.setMessage("Polo!");
   return outgoing;
}


여기서 보다시피 handleSubscription() 메소드는 /app/macro에 대한 구독을 처리하기 위한 @SubscribeMapping으로 애너테이션된다. (@MessageMapping을 사용할 때 처럼, "/app" 접두어가 적용된다.)  구독 처리 시에, handleSubscription()은 발신 Shout객체를 생성, 반환한다. Shout 객체는 메시지로 변환되고, 클라이언트가 구독하는 동일 목적지내의 클라이언트 대상으로 반환된다.

HTTP-GET의 요청-응답 패턴과 많이 다르지 않다면, 지금까지이 대부분은 그대로 동작한다. 그러나 주된 차이점은 HTTP GET 요청이 동기이지만, 구독 요청-응답은 비동기이며, 사용가능하면 언제나 그리고 기다릴 필요없을 떄는 언제든지 클라이언트로 하여금 응답을 처리할 수 있도록 한다.

3.3 클라이언트로 메시지 보내기

지금까지 클라이언트는 메시지 전송의 대부분을 수행하였고, 서버는 그 메시지를 받기만 하였다. 웹 소켓은 서버가 HTTP 요청에 대한 응답 없이 브라우저로 데이터를 전송할 수 있는 방법을 제공한다.
스프링은 클라이언트로 데이터를 보내기 위해 두 가지 방법을 사용한다.

  • 메시지 또는 구독의 부작용(side-effect)
  • 메시징 템플릿 사용하기

메시지 처리 이후에  메시지 전송하기

메시지 수신에 대한 응답 메시지를 보내고자 한다면, void 보다는 다른 반환 형식을 사용한다. 예를들면, "Macro!" 메시지에 대한 리액션으로 "Polo!" 메시지를 보내려고 한다면, 다음처럼 handleShout() 메소드를 변경한다.

@MessageMapping("/macro")
public Shout handleShout(Shout incoming){
   logger.info("Received message:"+incoming.getMessage());

  Shout outgoing=new Shout();
  outgoing.setMessage("Polo!");
  return outgoing;
}

---> 새 버전 handleShout()에서 새 Shout 객체가 반환된다. 객체 반환 방법으로 sender메소드는 핸들러 메소드로 사용된다. @MessageMapping 애너테이션된 메소드가 반환 값을 가질 때, 반환 객체는 변환되고, (메시지 변환기를 통해)STOMP 프레임의 페이로드에 놓이며 브로커로 전달된다.

기본적으로 프레임은 핸들러 메소드를 트리거하는 동일 목적지로 전달되고, 접두어로 /topic를 가진다. handleShout()의 경우에, 반환된 Shout() 객체는 STOMP 프레임의 페이로드에 저장되고 /topic/macro 목적지로 전달된다. 목적지는 @SendTo를 사용하여 메소드를 애너테이션하여 오버로드 된다.


@MessageMapping("/macro")
@SendTo("/topic/shout")
public Shout handleShout(Shout incoming){
   logger.info("Received message:"+incoming.getMessage());

  Shout outgoing=new Shout();
  outgoing.setMessage("Polo!");
  return outgoing;
}

--->@SendTo 애너테이션을 사용하여 메시지는 /topic/shout로 전달된다. topic을 구독하는(클라이언트로서) 애플리케이션은 해당 메시지를 수신한다.


@SubscribeMapping 애너테이션된 메소드는 구독에 대응하는 메시지를 전송한다. 예를 들면, 클라이언트가 컨트롤러에 다음 메소드를 추가하여 구독할 때 Shout 메시지를 전송한다.
@SubscribeMapping("/macro")
public Shout handleShout(Shout incoming){
  Shout outgoing=new Shout();
  outgoing.setMessage("Polo!");
  return outgoing;
}


-->클라이언트가 /app/macro 목적지를 구독할 때마다(/app이라는 애플리케이션 목적지 접두어를 사용), @SubscribeMapping 애너테이션은 실행될 handleSubscription()메소드를 지정한다. Shout 객체는 변경되어 클라이언트로 다시 전송된다.

@SubscribeMapping과의 차이점은 Shout메시지가 클라이언트로 직접 전송되고, 브로커를 통하지 않는다는점이다. @SendTo애너테이션을 사용하는 메소드를 지정하면, 메시지는 브로커를 통해 지정된 목적지로 전달된다.


어디서나 메시지 보내기

@MessageMapping과  @SubscribeMapping은 메시지 수신 시퀀스 또는 구독 처리으로 메시지를 전송하기 위한 간단한 방법을 제공한다. 그러나 스프링의 SimpleMessagingTemplate은 메시지를 먼저 받지 않아도 애플리케이션 내의 어느 곳에서든지 메시지를 전송한다

이것을 사용하는 가장 쉬운 방법은 필요한 객체를 대상으로 오토와이어링 하는 것이다.


<SimpMessagingTemplate은 메시지를 전송한다.>

@Service
public class SpittleFeedServiceImpl implements SpittleFeedService{

   private SimpMessageSendingOperations messaging;

  @Autowired
   public SpittleFeedServiceImpl(SimpMessageSendingOperations 
                                                                   messaging){ <--메시징템플릿주입
                        this.messaging=messaging;
  }

 public void broadcastSpittle(Spittle spittle){
     messaging.convertAndSend("/topic/spittlefeed",spittle); <--메시지 전송
  }
}


스프링의 STOMP 지원을 설정할 때의 부작용으로 스프링 애플리케이션 컨텍스트 내에 SimpMessageTemplate 빈이 이미 존재한다. 따라서 여기서 새 인스턴스를 생성할 필요는 없다. SpittleFeedServiceImpl 생성자는 SpittleFeedServiceImpl 이 생성될 때, 기존 SimpMessagingTemplate을 주입하기 위해 @Autowired를 가지고 애너테이션된다.

메시지를 convertAndSend()를 사용하여 STOMP 토픽으로 보낼 때 또는 핸들러 메소드의 결과로서 그 토픽을 구독하는 클라이언트는 메시지를 수신한다. 라이브 Spittle피드를 사용하여 모든 클라이언트를 최신상태로 만들므로 이는 완전히 괜찮다.
그러나 가끔 전체사용자가 아니라 특정 사용자들에게만 메시지를 보내고자 할때가 있다.


4. 사용자 타깃 메시지 사용하기


사용자가 누구인지 안다면, 클라이언트가 아니라 사용자와 관련된 메시지 처리가 가능하다.사용자를 구별하는 방법은 사용한 동일한 인증(authentication)메커니즘을 통해 사용자 타깃 메시지를 사용하고, 사용자 인증을 위한 스프링 시큐리티를 사용한다.


스프링과 STOMP를 사용하여 메시징할 떄, 사용자 인증 잠점으로는 다음이 있다.
  • @MessageMapping과 @SubscribeMapping메소드는 인증된 사용자를 위한 Principal을 받는다.
  • @MessageMapping, @SubscribeMapping, @MessageException 메소드로부터 반환된 값은 인증된 사용자에게로 메시지로 전송된다.
  • SimpMessagingTemplate은 특정 사용자에게 메시지를 전송한다.

4.1 컨트롤러에서 사용자 메시지 사용하기

@MessageMapping과 @SubscribeMapping메소드가 메세지를 처리할 때 사용자를 고려하기 위한 방법이 두 가지가 있다. 핸들러 메소드에 대한 파라미터로서 Principal을 요청하여 핸들러 메소드는 사용자가 누구인지 알 수 있고, 사용자 데이터 활용에 포커싱된 정보를 사용할 수 있다. 추가적으로 핸들러 메소드는 @SendToUser로 애너테이션되며, 반환 값이 인증된 사용자의 클라이언트로(해당 클라이언트에게만 해당됨) 메시지가 전송되야 함을 나타낸다.

수신 메시지를 다루고, Spittle로서 저장 할 수 있는 다음의 handleSpittle()메소드를 생각해보자.

@MessageMapping("/spittle")
@SendToUser("/queue/notifications")
public Notification handleSpittle(Principal principal, SpittleForm form){

   Spittle spittle=new Spittle(principal.getName(), from.getText(), new Date());

   spittleRepo.save(spittle);
   
   return new Notification("Saved Spittle");
}


--->handleSpittle()은 Principal 객체뿐만 아니라 SpittleForm객체도 사용한다. Spittle인스턴스를 생성하기 위해 해당 객체들을 사용하고, 인스턴스를 저장하기 위해 SpittleRepository를 사용한다. 결국 Spittle이 저장됨을 나타내기 위해서 Notification을 반환한다.

이 메소드는 @MessageMapping으로 애너테이션되므로 메시지가 /app/spittle목적지로 전달될 때마다 실행된다.(사용자가 인증되었다는 가정하에) SpittleForm은 이 메시지를 통해서 만들어지고, Principal은 STOMP 프레임의 헤더로부투 얻을 수 있다.

그런데 무엇보다 중요한 것은 어디서 반환되는 Notification이 발생하는가 이다.
@SendToUser애너테이션은 반환되는 Notification이 /queue/notifications목적지로 메시지 형태로 전송되도록 지정한다.

스프링이 메시지를 어떻게 전송하는지를 이해하기 우해 잠시 뒤로 돌아가서 컨트롤러 메소드가 Notification을 전송하고자 하는 목적지를 대상으로 클라이언트가 어떻게 구독하는지를 살펴봐야 한다. 사용자 특정 목적지를 구독하기 위해 자바스크립트 코드를 살펴보자.

stomp.subscribe("/user/queue/notifications", handleNotifications);

목적지는 접두어 /user을 가진다. 내부적으로 접두어 /user를 가지는 목적지는 특별한 방법으로 처리된다. /user 메시지는 AnnotationMethodMessageHandler, SimpBrokerMessageHandler 또는 StompBrokerRelayMessageHandler가 아니라 UserDestinationMessageHandler를 통해서 전달된다.

이것의 주된 작업은 사용자 메시지를 사용자에게 유일한 목적지로 다시 라우팅 하는 것이다. 구독시, 타깃 목적지는 접두어 /user를 제거하고 사용자 세션에 기반을 둔 접미사를 추가한다. 
예를들면,  /user/queue/notifications에 대한 구독은 /queue/notifications-user6hr83v6t목적지로 다시 라우팅된다.




4.2 특정 사용자에게 메시지 보내기


convertAndSendUser() 메소드는 특정 사용자를 타깃으로 메시지를 보낸다.

예를들어, Spitter 텍스트가 @jabuer를 포함하면, 사용자가 메시지를 username으로 "jbauser를 가지는 클라이언트로 보낼 수 있다. 다음 코드에서 broadcastSpittle() 메소드는 사용자에게 현재 통신하고 있음을 알려주기 위해서  convertAndSendUser()를 사용한다.

@Service
public class SpittleFeedServiceImpl implements SpittleFeedService{

    private SimpMessagingTemplate messaging;
    private Pattern pattern= Pattern.compile("\\@(\\S+)"); <---사용자가 언급한 Regex 패턴

  @Autowired
  public SpittleFeedServiceImpl(SimpMessagingTemplate messaging){
    this.messaging=messaging;
  }

  
 public void broadcastSpittle(Spittle spittle){
    
   messaging.convertAndSend("/topic/spittlefeed",spittle);
   Matcher matcher=pattern.matcher(spittle.getMessage());
   if(matcher.find()){
       String username=matcher.group(1);
       messaging.convertAndSendToUser( <---사용자에게 통지(notification)보내기
           username, "/queue/notifications",
          new Notification("You just got mentioned"));
    }
  }
}


주어진 Spittle객체의 메시지가 username으로 ("@"으로 시작하는 텍스트) 표시되는 내용을 포함한다면, 새 Notification은 /queu/notifications라는 목적지로 전송된다. 따라서 Spittle이 "@jabauer" 포함메시지를 가진다면, Notification은 /user/jbauer/queue/notifications목적지로 전송된다.


댓글

이 블로그의 인기 게시물

(네트워크)폴링방식 vs 롱 폴링방식

(ElasticSearch) 결과에서 순서 정렬