elevne's Study Note

Spring WebSocket (STOMP - Redis) 본문

Backend/Spring

Spring WebSocket (STOMP - Redis)

elevne 2023. 7. 1. 14:36

이전 시간에 작성한 것처럼 스프링에서 제공하는 STOMP 를 통해 채팅서버를 구현할 수 있지만, 이러한 Simple Message Broker 은 스프링 서버 내부 메모리에서 작동한다. 이러한 구조에서 서바가 다운되거나 재시작하면 메시지 큐 내의 데이터가 유실될 수 있으며, 서버가 여러 대로 구성되어 있을 경우 서버간 채팅방을 공유할 수 없게된다는 문제가 발생한다. 이럴 때 외부 메시지 브로커를 활용할 수 있다. Redis 는 STOMP 를 따로 지원하지는 않지만 Redis 가 제공하는 pub/sub 기능이 있다. (STOMP 프로토콜을 지원하는 RabbitMQ 와 같은 전용 메시지 브로커를 사용하면 더 많은 기능을 사용할 수 있다고 한다)

 

 

Redis 를 메시지 브로커로 추가하면 다음과 같이 동작한다. 먼저 채팅방을 생성하면, Redis 저장소에 채팅방 정보를 저장한다. 그 후 사용자가 해당 채팅방에 입장할 때 WebSocket 연결이 수행되고, 해당 채팅방을 Subscribe 한다. 해당 채팅방에 메시지 리퀘스트를 보내면, 메시지를 받아서 Redis Message Queue 에 Publish 한다. 그럼 그 Message Queue 에 대한 Subscriber 이 WebSocket Subscriber 에게 메시지를 전달한다. (Redis 는 보낸 메시지를 따로 저장하지 않아서, 해당 채널을 Subscribe 하고있지 않다면 메시지가 유실될 수 있다)

 

 

 

우선 Spring Boot 내에서 Redis 를 사용하기 위해 아래와 같이 @Configuration 파일을 작성해준다.

 

 

@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory connectionFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName("localhost");
        configuration.setPort(6379);
        return new LettuceConnectionFactory(configuration);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
        return redisTemplate;
    }

    @Bean
    public ChannelTopic channelTopic() {
        return new ChannelTopic("CHAT");
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(RedisSubscriber subscriber) {
        return new MessageListenerAdapter(subscriber, "onMessage");
    }

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory factory, MessageListenerAdapter adapter, ChannelTopic topic
    ) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        container.addMessageListener(adapter, topic);
        return container;
    }
}

 

 

 

위에서 사용된 RedisTemplate 클래스는 데이터 액세스 코드를 단순화하는 도우미 클래스이다. 주어진 객체와 Redis 저장소의 Binary data 간의 serialization/deserialization 을 자동으로 수행한다. 기본적으로는 JdkSerializationRedisSerializer 을 사용하여 직렬화한다. String intensive 한 작업인 경우 StringRedisTemplate 을 사용하는 것을 고려할 수 있다. ChannelTopic ("CHAT" 이라는 이름의 토픽) 객체와 MessageListenerAdapter 객체 빈을 생성한다 (메시지를 받으면 Subsriber 의 "onMessage" 메소드를 수행하게끔 하는 것). 이 둘은 RedisMessageListenerContainer 빈에 묶어서 등록해준다. MessageListenerAdapter 에 사용된 RedisSubscriber 객체는 아래와 같이 작성된다.

 

 

@Component
@RequiredArgsConstructor
public class RedisSubscriber implements MessageListener {

    private final ObjectMapper objectMapper;
    private final RedisTemplate redisTemplate;
    private final SimpMessageSendingOperations messageSending;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        try {
            String publishMessage = (String) redisTemplate.getStringSerializer().deserialize(message.getBody());
            com.begin.board.chatting2.Message msg = objectMapper.readValue(publishMessage, com.begin.board.chatting2.Message.class);
            messageSending.convertAndSend("/queue/message/wonil", msg);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

 

 

 

Message 를 받아서 클라이언트에 전송할 때는 SimpMessageSendingOperationsconvertAndSend 메소드가 사용되었다. 주어진 destination 으로 메시지를 전송해준다.

 

 

 

Controller 에서도, 직접 메시지를 바로 클라이언트로 보내는 것이 아니라 Redis 에 Publish 할 수 있도록 코드를 아래와 같이 수정해주었다.

 

 

@MessageMapping("/queue/redis/wonil")
public void message(@Payload Message message) {
    chatService.sendMessage(message);
}

 

 

@Service
@RequiredArgsConstructor
public class RedisChatService {

    private final RedisTemplate redisTemplate;
    private final ChannelTopic topic;

    @Transactional
    public void sendMessage(Message msg) {
        redisTemplate.convertAndSend(topic.getTopic(), msg);
    }

}

 

 

 

RedisTemplateconvertAndSend 메소드로 주어진 채널에 메시지를 보낼 수 있다.

 

 

 

result

 

 

result

 

 

메시지가 잘 전송되는 것을 확인할 수 있었다.

 

 

 

 

 

 

 

Reference:

https://velog.io/@ohjinseo/WebSocket-Spring-Boot-stomp-Redis-PubSub-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84

https://hi-june.github.io/rualone/RUAlone05/#%EC%B1%84%ED%8C%85-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9D%98-%EA%B3%A0%EB%8F%84%ED%99%94

'Backend > Spring' 카테고리의 다른 글

Spring Boot Logback  (0) 2023.07.25
Spring WebSocket (STOMP - Kafka)  (0) 2023.07.10
Spring WebSocket (STOMP)  (0) 2023.06.30
Spring Security : JWT 적용해보기 (3)  (0) 2023.06.29
Spring WebSocket  (0) 2023.06.28