[Spring] 실시간 채팅 기능 구현-2

·2023년 2월 1일
3

킁킁메이트

목록 보기
7/10
post-thumbnail
post-custom-banner

💡개발 환경
Java 11, Srping 2.7.X, MySQL

⭐ 이전 글 [Spring] 실시간 채팅 기능 구현-1

Redis pub/sub

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'

RedisConfig

@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;
    }
   
}

RedisPublisher

@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);
    }
}

RedisSubscriber

@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());
        }
    }
}

비즈니스 로직

MessageController

메세징 요청을 보낼 때에는 @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);
    }
 }

RoomService

@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;
    }
}
profile
🧑‍💻백엔드 개발자, 조금씩 꾸준하게
post-custom-banner

0개의 댓글