개요

이전 글 에서는 WebSocket 으로 간단한 채팅 서버를 구현했다. 이것을 STOMP 를 사용해서 구현해보도록 하겠다.

Spring 에서 Websockt 의존성을 받아오면 Spring Messaging 이라는 의존성도 함께 추가된다.

이를 이해하기 위해서는 STOMP 를 이해해야한다.

STOMP

  • Simple Text Oriented Messaging Protocol

STOPM 는 메세지 브로커를 활용해 쉽게 메세지를 주고받을 수 있는 프로토콜이다.

pub-sub 이라는 발행-구독 형태를 사용해 메세지를 주고받을 수 있다.

웹 소켓 위에 얹어 함께 사용할 수 있는 하위(서브) 프로토콜이다!

데이터 형식

WebSocket 을 사용할 때는 메세지를 주고받는 형식은 따로 정해져있지 않다.

반면 STOMP 에서는 커맨드, 헤더, 바디의 구조로 데이터 형식을 지정한다.

COMMAND
header1:value1
header2:value2
Body^@
  • COMMAND
    • SEND, SUBSCRIBE 를 지시할 수 있다.
  • HEADER
    • 기존 websocket 으로 표현 불가능한 헤더를 작성할 수 있다.
  • BODY
    • 실제 데이터가 담기는 부분이다.

이는 실제 데이터 예시를 살펴보면 이해가 빠르다

SEND
destination:/pub/chat
content-type:application/json

{"chatRoomId":1, "type":"TALK", "sender":"UserA", "message":"Hello world!"}
^@

위 데이터는 UserA 가 1번 채팅방에 메세지를 보내는 데이터 예시이다.

  • COMMAND
    • SEND 로 메세지 전송을 의미한다.
  • HEADER
    • 메세지를 보낼 destination, type 이 지정되어있다.
  • BODY
    • 실제 메세지가 담고있는 정보가 존재한다.

PUB-SUB

image

STOMP 의 구조는 위와 같다.

발신자는 a 라는 토픽에 메세지를 보내고, 구독자들은 a 라는 토픽을 구독하고 있다 가정한다.

발신자는 destination 을 “/topic/a” 로 설정해 메세지 브로커를 거쳐 구독자들에게 바로 메세지를 보낼 수 있다.

발신자가 서버 내에서 임의의 처리 및 가공이 필요하다면 destination 을 “/app/a” 로 설정해 메세지 가공 후 메세지 브로커에게 이를 전달할 수 있다.

구현

1. WebSocketBrokerConfig

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // /ws-stomp로 연결하는 endpoint를 생성하고, CORS 허용
        registry
                .addEndpoint("/ws-stomp")
                .setAllowedOrigins("*");
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // /pub로 시작되는 메시지가 message-handling methods로 라우팅 되어야 한다.
        registry.setApplicationDestinationPrefixes("/pub");
        // /sub, /topic, /queue 로 시작되는 메시지가 메시지 브로커로 라우팅 되어야 한다.
        registry.enableSimpleBroker("/sub", "/topic", "/queue");
    }
}

STOMP 구현은 WebSocket 보다도 간편하게 할 수 있다. WebSocketMessageBrokerConfigurer 를 구현하고 두 가지 설정만 하면 된다.

  • registerStompEndpoints
    • /ws-stomp로 연결하는 endpoint를 생성하고, CORS 허용한다.
    • WebSocket 과 거의 동일하다.
  • configureMessageBroker
    • setApplicationDestinationPrefixes
      • /pub로 시작되는 메시지가 message-handling methods로 라우팅 되도록 지정한다.
      • 메세지 도착의 prefix 를 지정하는 것이다.
    • enableSimpleBroker
      • /sub, /topic, /queue 로 시작되는 메시지가 메시지 브로커로 라우팅 되도록 지정한다.
      • 쉽게 말해 메시지 브로커가 메세지를 전달할 수 있는 경로이다.

2. Controller

@Controller
@RequiredArgsConstructor
@Slf4j
public class StompController {

    @MessageMapping("/chat/{chatRoomId}")
    @SendTo("/sub/chat/{chatRoomId}")
    public String message(
            @DestinationVariable Long chatRoomId,
            @Payload ChatDto request) {
        log.info("chatRoomId: {}, message: {}", chatRoomId, request.getMessage());

        return request.getMessage();
    }
}

@Getter
public class ChatDto {

    private Long chatRoomId;
    private String sender;
    private String message;
}

Spring 에서는 Stomp 를 매우 쉽게 구현할 수 있다.

@MessageMapping 은 해당 경로로 들어오는 메세지들에 대해 동작한다는 뜻이다. 이때 앞서 설정한 prefix 가 존재하기에 “/pub/chat/1” 등의 경로로 들어와야 동작한다.

경로에 존재하는 파라미터는 @DestinationVariable 을 사용해 매핑할 수 있다.

@SendTo 는 말 그대로 메서드의 반환값을 해당 경로로 전달해준다는 뜻이다.

들어온 메세지는 @Payload 로 메세지만 매핑해 원하는 처리 후 반환할 수 있다.

실행

image

발행자 / 2번 구독자 / 1번 구독자 가 존재한다. 발행자가 1번 채팅방 경로로 메세지를 보낼 때 1번 구독자가 받는 것을 확인할 수 있다.

image

2번 경로에 대해서도 동일하게 동작한다.

APIC 에서는 연결과 동시에 구독을 지정해주어야해서 여러 구독을 만들어낼 수 없었다. 이대로 끝내기에는 구독과 연결 과정이 불명확하므로 페이로드를 자세히 열어보자.

인터셉터

1. Interceptor

@Component
@Slf4j
public class StompHandler implements ChannelInterceptor {

    @Override
    public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        String sessionId = accessor.getSessionId();

        switch (Objects.requireNonNull(accessor.getCommand())) {
            case CONNECT -> log.info("CONNECT: " + message);
            case CONNECTED -> log.info("CONNECTED: " + message);
            case DISCONNECT -> log.info("DISCONNECT: " + message);
            case SUBSCRIBE -> log.info("SUBSCRIBE: " + message);
            case UNSUBSCRIBE -> log.info("UNSUBSCRIBE: " + sessionId);
            case SEND -> log.info("SEND: " + sessionId);
            case MESSAGE -> log.info("MESSAGE: " + sessionId);
            case ERROR -> log.info("ERROR: " + sessionId);
            default -> log.info("UNKNOWN: " + sessionId);
        }

    }
}

메세지가 도착했을 때 이를 열어보고 필요하다면 전처리를 위해 인터셉터를 구성했다.

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(stompHandler);
    }

해당 인터셉터를 config 에서 등록해주면 끝이다.

실행

image

실제 실행 시 APIC 에서 연결을 진행하면 연결과 동시에 구독이 진행되는 것을 로그로 파악할 수 있었다.

결론

아주 간단하게 Spring 에서 STOMP 구현이 가능하다. 이를 활용해 실시간 서비스 구현에 사용할 수 있다. 마지막으로 STOMP 의 활용 방안과 장점에 대해 정리해보자.

STOMP 장점

  • 정해진 컨벤션 (데이터 구조.. 커맨드/헤더/바디)
  • 외부 Messaging Queue 사용 가능
  • Spring Security

외부 Messaging Queue

위 코드에서 메세지 브로커, 큐는 메모리 상에 존재한다. 이는 다수의 서버를 동작시킬 때 문제가 발생할 수 있는데, A 를 구독하는 3명의 사용자의 메세지 큐가 각각의 서버에 존재한다면 발행자가 요청한 서버에서만 메세지가 전달될 가능성이 있다.

image

인메모리 시스템의 위험성 등의 이유로 RabbitMQ, Kafka 등 외부 메세지 큐를 사용할 수 있다.

Spring Security

위에서 configureClientInboundChannel 를 통해 설정한 인터셉터를 사용해 JWT 등 인증 과정도 처리할 수 있다.

Github Code

참고

우아한테크 - 10분 테크톡 Spring Websocket & STOMP

업데이트:

댓글남기기