Spring

Scale-out 상황, 채팅 서비스 Redis 도입

초보병일이 2024. 6. 20. 17:37
728x90

0. 배경

Spring에 내장된 Simple Message Broker는 서버 내부 메모리에서 동작합니다.

메세지 발행 시 서버가 다운 되어 메세지 전송을 실패하게 된다면, 인메모리 기반으로 동작하는 메세지 큐로 인해 메세지를 유실할 가능성이 높고, scale-out 상황에서 메세지를 못 받는 경우가 발생하였습니다.

 

메세지를 받지 못하는 상황을 개선하기 위한 과정입니다.

 

1. 서버가 다른 경우 메세지 수신이 안 되는 상황

2개의 서버가 존재하고 각각 다른 서버에서 구독을 하고 있다는 상황을 가정하였습니다.

내장된 SimpleMessageBroker를 사용하면 스프링 부트 서버의 내부 메모리에서 동작하게 됩니다.

 

서버간 채팅을 공유할 수 없는 상태입니다.

다른 서버에서 같은 지점을 구독 중인 상황

서버가 다른 경우 메세지를 수신할 수 없습니다.

 

채팅 전용 서버를 두고, 채팅 관련하여 모든 처리는 전용 서버에서 하면 해결할 수 있지만 여러 기능들을 분산 처리하기 위한 환경을 구성하였다고 가정했기 때문에 Redis를 도입하여 해결하겠습니다.

 

2. Redis Pub/Sub 적용

2.1 Redis를 선택한 이유

Redis는 STOMP 프로토콜을 지원하지 않지만, Redis의 Pub/Sub 기능을 통해 메세지 브로커로 사용할 수 있습니다.

메세지를 전송한 후 삭제되며 실시간 데이터 처리에 적합하지만 메세지 전송 신뢰성을 보장하지 않는다는 단점이 존재합니다.

 

메세지 전용 브로커를 사용한다면 인프라 비용 증가 및 러닝 커브가 매우 높습니다.

현재 저희 서비스에 대용량 트래픽이 없기 때문에 메세지 전용 브로커까지 필요 없었고, 더 빠르고 쉬운 Redis를 이용하여 충분히 해결할 수 있다고 판단하였습니다.

 

2.2 RedisConfig 코드 및 설명

@Slf4j
@Configuration
public class RedisConfig {

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer( // (1)
                                                                       RedisConnectionFactory connectionFactory,
                                                                       MessageListenerAdapter listenerAdapter,
                                                                       ChannelTopic channelTopic) {
        log.info("redisMessageListenerContainer");
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, channelTopic);
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(RedisMessageSubscriber subscriber) { // (2)
        log.info("listenerAdapter");
        return new MessageListenerAdapter(subscriber, "onMessage");
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate
            (RedisConnectionFactory connectionFactory) { // (3)
        log.info("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() { // (4)
        log.info("channelTopic");
        return new ChannelTopic("chatroom");
    }
}

 

(1) RedisMessageListenerContainer는 Redis의 Pub/Sub을 관리하는 컨테이너입니다.

구독 대상이 되는 채널 (ChannelTopic)과 해당 채널에 메세지가 발행되었을 때 핸들링 하는 메소드(MessageListener)를 등록해줄 수 있습니다.

 

(2) 실제 메세지를 처리하는 비즈니스 로직이 담긴 Bean을 추가해줍니다.

 

(3) RedisTemplate 설정

 

(4) ChannelTopic 단일화

 

2.3 MessageSubscriber 코드 및 설명

@Service
@Slf4j
@RequiredArgsConstructor
public class RedisMessageSubscriber implements MessageListener {
    private final ObjectMapper objectMapper;
    private final RedisTemplate redisTemplate;
    private final SimpMessagingTemplate messagingTemplate;

    /**
     * 여기서 메세지를 다시 구독자들에게 전송합니다.(레디스 pub/sub)
     */
    @Override
    public void onMessage(final Message message, final byte[] pattern) {
        try {
            String publishMessage = (String) redisTemplate.getStringSerializer().deserialize(message.getBody());
            ChatMessage chatResponseDto = objectMapper.readValue(publishMessage, ChatMessage.class);

            log.info("redisMessageSubscriber: data from {}", chatResponseDto);
            log.info("redisMessageSubscriber: data to {}", "/topic/" + chatResponseDto.getRoomId());
            messagingTemplate.convertAndSend("/topic/" + chatResponseDto.getRoomId(), chatResponseDto);

        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
}

 

실제로 메세지를 발행합니다. 

서버마다 메세지 리스너를 등록해두고, onMessage 메소드로 SimpleMessageBroker에 메세지를 보냅니다.

 

다른 서버에서 발행된 메세지도 onMessage를 타고 구독자에게 전달되는 것입니다.

3. 결론

메세지 수신 완료

다른 서버에서 메세지를 수신할 수 있음을 확인할 수 있습니다.

 

RedisTemplate의 convertAndSend 메소드는 메세지를 Listener로 전달하는 역할을 합니다. (ChannelTopic 을 이용하여)

onMessage에서 SimpleMessageBroker로 직접 서부 내부의 메세지 브로커를 통해 메세지를 발행합니다.

 

Channel을 이용하였기 때문에 다수의 서버일 경우 채팅을 공유할 수 있습니다.

 

1. 메세지 전용 브로커, Redis 각각 장단점이 존재합니다.

2. Redis를 이용하여 빠르고 쉽게 처리할 수 있다고 판단하여 선택했습니다.

3. 더 나아가 메세지가 유실되면 안 되고, 트래픽이 많아질 경우 전용 브로커를 사용하여 개선할 수 있습니다.

 

 

728x90