elevne's Study Note
Spring WebSocket (STOMP - Redis) 본문
이전 시간에 작성한 것처럼 스프링에서 제공하는 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 를 받아서 클라이언트에 전송할 때는 SimpMessageSendingOperations 의 convertAndSend 메소드가 사용되었다. 주어진 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);
}
}
RedisTemplate 의 convertAndSend 메소드로 주어진 채널에 메시지를 보낼 수 있다.
메시지가 잘 전송되는 것을 확인할 수 있었다.
Reference:
'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 |