Spring에서 Stomp 사용해서 채팅하기.

태규 최·2022년 4월 12일
post-thumbnail

개인 플젝을 하면서 채팅을 구현하게 되었고 WebSocket이랑 Stomp를 사용했었고, 그간 공부했던 것을 기록하기 위해서 글을 남긴다!!

전체적인 내용은 https://docs.spring.io/spring-framework/docs/5.3.8/reference/html/web.html#websocket-stomp 공식 레퍼런스와 각종 블로그를 통해 정리한 개인적인 인사이트 이며 오류가 있을수도 있다!

0. 내가 구현하고자 한 기능

일반적이고 대중적인 그룹 채팅 창을 만드려고 했다

  1. 사용자가 채팅 방에 들어갈때마다 연결이 되어야 하고 나오면 연결이 해제가 되어야 한다.

  2. 주고받은 메시지를 데이터베이스에 저장해서 가져올 수 있다.

  3. 한 명이 채팅을 보냈을때 같은 채팅방에 있는인원들이 채팅을 전부 볼 수 있어야 한다.

1. Spring - Websocket

https://stackoverflow.com/questions/40988030/what-is-the-difference-between-websocket-and-stomp-protocols

기존 Spring 웹 소켓 방식으로는 low level이라 내가 원하는 기능들을 구현 할 수 있는지 의구심이 들었고, 설사 가능 하더라도 지금의 내 레벨에서는 구현하는데 매우 많은 시간이 걸릴거라 생각했다.

단순 websocket을 사용했을때는 1:1 단순 채팅밖에 할 수 없었고 , 세션의 라이프사이클이 언제 끝나고 생성되는지 알기가 힘들었다.

문제점들

  • 1:1 채팅 밖에 안됌

  • 브라우저가 열려 있는 동안 통신이 이루어지는데 브라우저를 종료했을때 소켓 세션이 끊어지는지 확실하지 않음.

  • WebSocket이 너무 low -level 기술이라 좀 더 고도화 된 기술이 필요함.

2. Spring - Stomp

Stomp는 Websocket의 좀더 고도화된 형태로 통신을 하게되는 프로토콜이다.
원래 외부 메시지 브로커에 연결하기 위해 개발된 것이다. stomp를 통해서 최소의 메시징 처리등을 할수가 있게 되었다.

COMMAND
header1:value1
header2:value2
Body^@

Stomp로 통신을 할 때 클라이언트는 SEND나 SUBSCRIBE로 메시지를 전송할수 있게 된다. 메시지를 보내게 되면 외부 메시지 브로커를 통해 다른 클라이언트에게 메시지를 보내거나 , 서버에게 메시지를 보내서 구독 - 발행 구조를 수행할수 있게 된다.

스프링에서는 Stomp로 통신 할 때 간단한 메시지 브로커의 역할을 수행해준다.

Stomp의 장점

  • 커스텀 메시지 프로토콜을 개발할 필요가 없다.

  • Stomp 클라이언트는 스프링에 있는 자바 클라이언트를 사용 할 수가 있다.

  • 메시지 브로커를 사용해서 구독 및 서버에서 전송하는 메시지를 관리 할 수가 있다.

  • 들어오는 메시지를 컨트롤러를 통해서 관리를 할 수가 있다.

문제점들

  • Stomp를 사용해서 1:N 채팅구조를 구현하는데 성공했지만 아직 몇개의 문제점이 여전히 남아있었다.

-Spring SimpleBroker를 사용하니 세션의 흐름과 통신이 정확히 어떻게 이루어지는지 파악하기가 힘들었으며 현재 구독 중인 채팅 방과 사용자들을 표현하기가 힘들었다.

3. Spring - Stomp - RabbitMq

위의 문제들을 RabbitMq 외부 메시지 브로커를 이용해서 해결했다.

웹 소켓 클라이언트가 메시지를 Stomp를 통해서 보내게 되면 미리 설정해둔 MessageBroker를 통하게 된다.

이 연결에서 Spring은 TCP를 통해서 메시지를 외부 Stomp MessageBroker로 전달하게 되고 메시지의 처리를 Broker에게 위임하게 된다.

위 그림에서 Spring에서 Controller를 통해서 메시지를 브로커에게 전달한다( 위 그림에서는 SimpAnnotationMethodMessageHandler로 표기되어 있는데 실제로 Controller를 구현할때 @MessageMapping과 같은 어노테이션을 사용해서 전달하게 된다)

RabbitMQ를 사용했을때의 장점

  • 어떤 세션이 어떤 채팅방에 연결이 되어있고 현재 상태를 ui를 통해서 확인 할 수 있다.

  • 다양한 형태의 구독 메시지를 설정할 수가 있다.

  • 비동기 메시징

RabbitMq의 MessageFlow

RabbitMq에서는 메시지를 전달받게 되면 먼저 Exchange를 통해서 분류를 하게 된다.

Direct 같은 경우에는 1:1로 매칭되는 연결형태이고 전송받는 수신자에 대해 Queue가 생성이되고 Exchange와 Queue를 하나로 바인딩해서 사용을 하게 된다.

바인딩된 Direct Exchange는 라우팅 키를 이용해서 1:1 통신을 하게 된다.

Topic Exchange는 메시지가 들어오고 각 패턴에 맞는 큐에게 메시지를 전송한다.

큐에서는 패턴에 맞는 메시지를 수신받고 소비하면서 수신자에게 메시지를 전송한다.

Fanout Exchange는 구독을 한 이용자 전원에게 메시지를 전송하는 방식이다.

여기서 내가 사용한 방식은 따로 패턴을 지정할 필요가 없으므로 Fanout Exchange를 사용했다.

RabbitMq - Stomp

RabbitMq는 Stomp 플러그인을 지원하고 Spring과도 잘 맞으므로 Stomp 프로토콜을 통해서 메시지를 전송하게 됐다.

@Bean
public RabbitAdmin rabbitAdmin() {
    return new RabbitAdmin(connectionFactory());
}

@Bean
public RabbitTemplate rabbitTemplate() {
    RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
    rabbitTemplate.setMessageConverter(jsonMessageConverter());

    return rabbitTemplate;
}

@Bean
public ConnectionFactory connectionFactory() {
    CachingConnectionFactory factory = new CachingConnectionFactory();
    factory.setHost("localhost");
    factory.setUsername("guest");
    factory.setPassword("guest");
    return factory;
}

@Bean
public Jackson2JsonMessageConverter jsonMessageConverter() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
    objectMapper.registerModule(dateTimeModule());
    Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(objectMapper);
    return converter;
}

@Bean
public MappingJackson2MessageConverter consumerJackson2MessageConverter() {
    return new MappingJackson2MessageConverter();
}

@Bean
public Module dateTimeModule() {
    return new JavaTimeModule();
}

@Bean
public RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry() {
    return new RabbitListenerEndpointRegistry();
}

@Bean
public DefaultMessageHandlerMethodFactory messageHandlerMethodFactory() {
    DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
    factory.setMessageConverter(consumerJackson2MessageConverter());
    return factory;
}

@Override
public void configureRabbitListeners(final RabbitListenerEndpointRegistrar registrar) {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setPrefetchCount(1);
    factory.setConsecutiveActiveTrigger(1);
    factory.setConsecutiveIdleTrigger(1);
    factory.setConnectionFactory(connectionFactory());
    registrar.setContainerFactory(factory);
    registrar.setEndpointRegistry(rabbitListenerEndpointRegistry());
    registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory());
}

RabbitMq에서 각 설정들을 진행했다.

RabbitMessage 템플릿과 전송받은 데이터를 바인딩할 객체들과 설정들을 빈으로 등록해줬다.

RabbitMq에서 Stomp를 사용하기 위해

💥 중요 !!!

rabbitmq-plugins enable rabbitmq_stomp

설정을 키고 사용을 해야 했다. 안키고 사용을 하니 포트 할당이나 무언가 문제가 생겨서 사용을 할 수가 없었다.

Stomp는 RabbitMq에 대해 Broker 스펙을 규정하고 있지 않다. 대신 destination 경로 패턴에 따라 특정 스펙을 정할수가 있다.

Subscribe 프레임에 대해서 /exchange/"name"/"pattern" 형태의 패턴을 사용할수가 있다.
name이름을 가진 Exchange을 자동으로 생성하고 pattern명을 가진 큐와 exchange를 바인딩한다.

Send 프레임에 대해서는 /exchange/"name"/"routing-key" name명을 가진 exchange에 대해서 routing key로 라우팅을 한다.

구독을 하니 큐가 자동으로 생성이 됐고 바인딩이 자동으로 됐다!

결론

기존 Spring SimpleMessageBroker도 충분히 사용이 가능하지만 기왕이면 RabbitMq로 사용을 하는게 더 좋다고 생각을 했다.

RabbitMq에서도 패턴이 되게 다양하고 세부 구조에 대해서는 좀 더 심도있는 분석이 필요하다.

사용을 하는것과 그 전체를 이해하는건 결이 다르고 큐와 exchange에 대해서도 여러가지 방법이 있다.

내가 예상했던 대부분의 스펙을 rabbitmq를 통해서 해결할 수 있었다.

그럼에도 불구하고 아직 문제점이 몇가지 남아있다.

먼저 첫번째로 지금은 채팅방을 만들고 그 채팅방 id를 가진 url를 구독을 하게 했는데, 채팅방이 없는 상태에서 연결을 시도하면 당연히 오류가 발생하게 되고 이 오류를 처리하는데 있어서 문서들을 되게 심도있게 읽어야 할것 같다.

exception handling이 생각보다 쉽지는 않은것 같다.

그럼에도 불구하고 외부 브로커를 사용하는것이 좀 더 낫다는것이 내 결론이다.

여담으로


js 진영에서는 socket.io가 제일 많이 사용되는데 java 진영에서는 왜 stomp가 많이 사용되는지도 알아봐야 겠따.

0개의 댓글