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. 더 나아가 메세지가 유실되면 안 되고, 트래픽이 많아질 경우 전용 브로커를 사용하여 개선할 수 있습니다.
'Spring' 카테고리의 다른 글
Spring Security - Swagger 적용하기 (0) | 2024.09.18 |
---|---|
WebScoket 실시간 매칭 시스템 - 동시성 문제 발생과 해결 과정 (0) | 2024.06.25 |
좌표값(객체) 테스트에 대한 고민 (0) | 2023.11.25 |
테스트 코드 - Presentation Layer (0) | 2023.11.12 |
CSRF(Cross-Site Request Forgery)란? (1) | 2023.10.19 |