💡개발 환경
Java 11, Srping 2.7.X, MySQL
⭐ 이전 글 [Spring] 실시간 채팅 기능 구현-1
STOMP가 pub/sub 기능을 지원하지만 MessageBroker가 In-Memory로 기반이기 때문에 처리 해야 할 데이터가 많아질 경우, 서버 전체에 과한 부담을 안겨줄 수 있다는 문제점이 발생하였다.
이러한 서버의 부하를 분산하기 위해 Redis를 사용하기로 하였다!
Redis도 STOMP와 마찬가지로 Publish/Subscriber 기능을 지원한다.
구독 정보를 redis 서버에 ChannelTopic으로 저장해 같은 Topic을 구독하고 있는 사용자에게 메세지를 송수신한다.
💡Redis는 NoSQL로 데이터의 고속 읽기/쓰기에 최적화 되어 있기때문에 실시간 채팅을 구현하는데에도 적합하다고 생각했다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
@Configuration
@RequiredArgsConstructor
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> chatRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> chatRedisTemplate = new RedisTemplate<>();
chatRedisTemplate.setConnectionFactory(connectionFactory);
chatRedisTemplate.setKeySerializer(new StringRedisSerializer());
chatRedisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
return chatRedisTemplate;
}
// redis pub/sub 메세지를 처리하는 listener 설정
@Bean
public RedisMessageListenerContainer redisMessageListener(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
@Service
@RequiredArgsConstructor
public class RedisPublisher {
@Resource(name = "chatRedisTemplate")
private final RedisTemplate<String, Object> redisTemplate;
public void publish(ChannelTopic topic, PublishMessage message) {
redisTemplate.convertAndSend(topic.getTopic(), message);
}
}
@Service
@RequiredArgsConstructor
public class RedisSubscriber implements MessageListener {
private final ObjectMapper objectMapper;
@Resource(name = "chatRedisTemplate")
private final RedisTemplate<String, Object> redisTemplate;
private final SimpMessageSendingOperations messagingTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
try {
String publishMessage = (String) redisTemplate.getStringSerializer().deserialize(message.getBody());
PublishMessage chatMessage = objectMapper.readValue(publishMessage, PublishMessage.class);
messagingTemplate.convertAndSend("/sub/chats/" + chatMessage.getRoomId(), chatMessage);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
메세징 요청을 보낼 때에는 @MessageMapping
어노테이션을 사용한다.
💡 HTTP Method 매핑의 경우 Parameter 값을 받아오기 위해서 @PathVariable
을 사용하였지만 메세징의 경우 @DestinationVariable
을 사용한다.
메서드 매개 변수를 나타내는 주석은 대상 템플릿 문자열의 템플릿 변수에 바인딩되어야 합니다.
@MessageMapping
과 같은 메시지 처리 방법에서 지원됩니다.
@DestinationVariable
템플릿 변수는 항상 필요합니다.
@RestController
@Slf4j
@RequiredArgsConstructor
public class MessageController {
private final ChatService chatService;
private final ChatMapper mapper;
private final RedisPublisher redisPublisher;
@Resource(name = "chatRedisTemplate")
private final RedisTemplate redisTemplate;
@MessageMapping("/chats/messages/{room-id}")
public void message(@DestinationVariable("room-id") Long roomId, MessageDto messageDto) {
PublishMessage publishMessage =
new PublishMessage(messageDto.getRoomId(), messageDto.getSenderId(), messageDto.getContent(), LocalDateTime.now());
log.info("publishMessage: {}", publishMessage.getContent());
// 채팅방에 메세지 전송
redisPublisher.publish(ChannelTopic.of("room" + roomId), publishMessage);
log.info("레디스 서버에 메세지 전송 완료");
chatService.saveMessage(messageDto, roomId);
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class RoomService {
private final Map<String, ChannelTopic> topics;
private final RedisMessageListenerContainer redisMessageListener;
private final RedisSubscriber redisSubscriber;
public Long createRoom(long receiverId, MemberDetails memberDetails) {
Member receiver = memberService.validateVerifyMember(receiverId);
Member sender = memberService.validateVerifyMember(memberDetails.getMemberId());
ChatRoom chatRoom =
ChatRoom
.builder()
.sender(sender)
.receiver(receiver)
.build();
ChatRoom saveChatRoom = roomRepository.save(chatRoom);
// 토픽 생성
String roomId = "room" + saveChatRoom.getRoomId();
if(!topics.containsKey(topicRoomId)) {
ChannelTopic topic = new ChannelTopic(topicRoomId);
redisMessageListener.addMessageListener(redisSubscriber, topic);
topics.put(topicRoomId, topic);
}
return saveChatRoom.getRoomId();
}
public ChatRoom findRoom(long roomId) {
ChatRoom chatRoom = findExistRoom(roomId);
return chatRoom;
}
}