Redis와 WebSocket 기반 채팅 목록 리프레시 및 알림 시스템


새 메시지 도착 시 채팅 목록을 실시간으로 리프레시하고 알림을 표시하는 시스템을 Redis와 WebSocket으로 구현.

인스타그램 DM처럼 유저가 채팅 서비스에 연결되어있고 새로운 채팅을 받았을 때 유저의 화면을 어떻게 업데이트 해줘야할까 고민해봤다

구현은 성공했으나 우여곡절이 많았다. 채팅서비스가 이렇게 고려해야할 사항이 많은줄 몰랐다..

그래도 원하는데로 구현이 되어서 기쁘다!


목표

  • 새 메시지 도착 시 현재 연결된 유저에게 신호를 보내 채팅 목록을 리프레시하고, 새 메시지를 빨간색 마크로 표시.
  • Redis로 DB 부하를 줄이고, WebSocket으로 실시간 업데이트 제공.

WebSocket 구독 설정

구독 경로:
  • 클라이언트가 /topic/toggle-refresh/ 구독.
  • stompClient.subscribe(`/topic/toggle-refresh/${userId}`
역할:
  • 특정 유저(userId)에게 새 메시지 도착 시 리프레시 신호 전달.

메시지 전송 및 Redis 처리

메시지 저장 전:
  • MessageDto에서 chatRoomId 추출.
  • Redis에서 해당 chatRoomId의 유저 목록 조회 (키: "chatroom_users:").
  • Redis에 없으면 MySQL에서 유저 목록 가져와 캐싱 (chatRoomUserRepository.findUserIdsByChatRoomId).
@Transactional
public void syncMessagesByChatRoomId(Long chatRoomId) {
        List pendingMessageDTOs = redisService.getPendingMessages(chatRoomId);
        List pendingMessages = pendingMessageDTOs.stream()
                .map(dto -> {
                    Message message = new Message();
                    message.setChatRoom(chatRoomService.getChatRoomById(dto.getChatRoomId()));
                    // Handle system messages (senderId = 0 or null)
                    if (dto.getSenderId() == null || dto.getSenderId() == 0) {
                        message.setUser(null); // System message
                    } else {
                        message.setUser(userService.getUserById(dto.getSenderId()));
                    }
                    message.setContent(dto.getContent());
                    message.setEnrolledAt(dto.getEnrolledAt());
                    return message;
                })
                .toList();
        if (!pendingMessages.isEmpty()) {
            List savedMessages = messageRepository.saveAll(pendingMessages);
            redisService.removePendingMessage(chatRoomId);
            log.info("채팅방 {} 싱크완료", chatRoomId);
        } else {
            log.info("채팅방 {} 싱크할 메세지 없음", chatRoomId);
        }
    }
효과:
  • Redis를 통해 채팅방 유저 조회로 MySQL 부하 감소.

리프레시 요청 및 알림 표시

리프레시 신호:
  • 메시지 저장 후 WebSocket으로 /topic/toggle-refresh/에 신호 전송.
클라이언트 반응:
  • 구독 중인 유저의 채팅 목록이 리프레시되고, 새 메시지에 빨간색 마크 표시.

동작 방식

유저 구독:
  • 클라이언트가 /topic/toggle-refresh/ 구독 → 알림 대기.
메시지 전송:
  • 클라이언트가 메시지 전송 → MessageDto에서 chatRoomId 추출.
  • Redis에서 "chatroom_users:" 조회 → 없으면 DB에서 캐싱.
  • Redis에 메시지 저장 (키: "message_queue:").
알림 전달:
  • chatRoomId의 각 userId에 대해 /topic/toggle-refresh/로 신호 전송.
  • 연결된 유저의 채팅 목록 리프레시 → 새 메시지 표시.

디테일한 흐름

Redis 키 구조:
  • "user_chatrooms:": 유저가 속한 chatRoomId 목록 (Redis Set).
  • "chatroom_users:": 채팅방에 속한 userId 목록 (Redis Set).
  • "message_queue:": 미처리 메시지 저장 (Redis List).
캐싱 로직:
  • Redis에 "chatroom_users:" 없으면 DB에서 조회 후 캐싱 (SADD).
  • "user_chatrooms:"도 동일: DB에서 chatRoomIds 가져와 캐싱.
WebSocket 메시지:
  • /topic/toggle-refresh/로 간단한 신호 전송 (예: { "chatRoomId": 123, "newMessage": true }).
클라이언트 UI:
  • 리프레시 후 새 메시지 항목에 빨간색 마크 추가.

문제 및 해결: 신규 채팅방 등록

문제:
  • 신규 채팅방 생성 시 Redis에 등록되지 않아 getChatRoomIdsByUserIdgetUserIdsByChatRoomId에서 누락.
원인:
  • addChatRoomIdsAndUserIds가 생성 시 호출되지 않음.
해결:
  • 채팅방 생성 시 각 유저에 대해 addChatRoomIdsAndUserIds(userId, chatRoomId) 호출.
  • Redis에 "user_chatrooms:""chatroom_users:" 즉시 업데이트.
결과:
  • 신규 채팅방이 Redis에 등록되고, 관련 키에서 정상 조회됨.

요약

문제 해결:
  • 신규 채팅방 포함 실시간 알림, MySQL 부하 감소.
Redis:
  • "chatroom_users:""user_chatrooms:"로 유저-채팅방 관계 캐싱, 메시지 큐로 성능 최적화.
WebSocket:
  • /topic/toggle-refresh/로 실시간 신호 전달.
결과:
  • 모든 채팅방(기존+신규)에 대해 즉각적인 알림과 UI 업데이트 제공.


Posted on 2025.03.01