Skip to content

Feat: 채팅 WebSocket Redis Pub/Sub 연동 및 구조 개선#112

Merged
CheatIsKey merged 36 commits into
developfrom
feat/Chat-WebSocket-Controller#109
Apr 24, 2026
Merged

Feat: 채팅 WebSocket Redis Pub/Sub 연동 및 구조 개선#112
CheatIsKey merged 36 commits into
developfrom
feat/Chat-WebSocket-Controller#109

Conversation

@CheatIsKey
Copy link
Copy Markdown
Member

💡 개요

  • 채팅 WebSocket Redis Pub/Sub 연동 및 구조 개선

🛠️ 작업 내용

Redis Pub/Sub 연동 (다중 서버 대응)

  • ChatRedisPublisher: 메시지 저장 후 Redis "chat:room:{roomId}" 채널에 발행
  • ChatRedisSubscriber: Redis 채널 구독 후 WebSocket 구독자에게 브로드캐스트
  • 기존 SimpleBroker 직접 브로드캐스트 → Redis 경유 방식으로 전환
  • 서버 A에서 발행한 메시지를 서버 B의 구독자도 수신 가능

RedisConfig 수정

  • 채팅 전용 chatRedisTemplate 빈 추가 (JSON 직렬화)
  • JavaTimeModule 등록으로 LocalDateTime 직렬화 지원
  • PatternTopic("chat:room:*")으로 모든 채팅방 채널을 단일 리스너로 처리
  • RedisMessageListenerContainer 빈 등록

WebSocket 예외 처리 전역화

  • ChatWebSocketController 내부 @MessageExceptionHandler 제거
  • WebSocketExceptionHandler를 common/exception 패키지로 분리
  • @ControllerAdvice + @MessageExceptionHandler로 전역 처리
  • CustomException → WARN 레벨, 그 외 Exception → ERROR 레벨 분리

ChatErrorResponse DTO 분리

  • ChatWebSocketController 내부 record 선언 제거
  • domain/chat/dto/response/ChatErrorResponse.java로 독립 분리

SUBSCRIBE 인가 검증 추가

  • StompChannelInterceptor에 SUBSCRIBE 시점 Principal 존재 여부 확인 추가
  • 인증되지 않은 세션의 채널 구독 시도 차단
  • SockJS 폴백과 무관한 레벨(STOMP 프레임 처리 단계)에서 수행

모든 상태 변경 메서드(closeChatRoom, joinChatRoom)에 비관적 락 적용

Redis Pub/Sub 연동 (다중 서버 대응)
- ChatRedisPublisher: 메시지 저장 후 Redis "chat:room:{roomId}" 채널에 발행
- ChatRedisSubscriber: Redis 채널 구독 후 WebSocket 구독자에게 브로드캐스트
- 기존 SimpleBroker 직접 브로드캐스트 → Redis 경유 방식으로 전환
- 서버 A에서 발행한 메시지를 서버 B의 구독자도 수신 가능

RedisConfig 수정
- 채팅 전용 chatRedisTemplate 빈 추가 (JSON 직렬화)
- JavaTimeModule 등록으로 LocalDateTime 직렬화 지원
- PatternTopic("chat:room:*")으로 모든 채팅방 채널을 단일 리스너로 처리
- RedisMessageListenerContainer 빈 등록

WebSocket 예외 처리 전역화
- ChatWebSocketController 내부 @MessageExceptionHandler 제거
- WebSocketExceptionHandler를 common/exception 패키지로 분리
- @ControllerAdvice + @MessageExceptionHandler로 전역 처리
- CustomException → WARN 레벨, 그 외 Exception → ERROR 레벨 분리

ChatErrorResponse DTO 분리
- ChatWebSocketController 내부 record 선언 제거
- domain/chat/dto/response/ChatErrorResponse.java로 독립 분리

SUBSCRIBE 인가 검증 추가
- StompChannelInterceptor에 SUBSCRIBE 시점 Principal 존재 여부 확인 추가
- 인증되지 않은 세션의 채널 구독 시도 차단
- SockJS 폴백과 무관한 레벨(STOMP 프레임 처리 단계)에서 수행

모든 상태 변경 메서드(closeChatRoom, joinChatRoom)에 비관적 락 적용
- findByIdForUpdate()로 SELECT FOR UPDATE 처리
- 고객 종료와 관리자 참여 동시 요청 시 데이터 정합성 보장
@CheatIsKey CheatIsKey self-assigned this Apr 24, 2026
@CheatIsKey CheatIsKey added the enhancement New feature or request label Apr 24, 2026
@github-actions
Copy link
Copy Markdown

🤖 AI 코드리뷰

리더보드 — 현재 5위 (리뷰 90건) | 전체 8개 팀 참여 중
업데이트 확인 — 리뷰가 질문을 던지다 — 생각해보기

🟡 개선 제안

1. ChatRedisSubscriber 예외 삼키기 (Exception Swallowing)

} catch (Exception e) {
    log.error("[Redis Sub] 메시지 처리 실패", e);
}

운영 환경에서 메시지 역직렬화가 실패하거나, WebSocket 브로드캐스트 도중 장애가 발생하면 해당 메시지는 그냥 소리 소문 없이 유실됩니다. 채팅 서비스에서 메시지가 드문드문 증발하는 것보다는 명시적으로 실패를 알리는 것이 낫습니다. 최소한 Dead Letter Queue(DLQ)로 보내거나, 복구 가능 여부에 따라 애플리케이션 로직을 중단시키는 방향으로 설계해야 합니다. "예외 삼키기"는 절대 용납되지 않습니다.

2. RedisConfigChatRedisSubscriber 간 ObjectMapper 불일치
RedisConfig에서는 채팅용 chatRedisTemplate을 위해 activateDefaultTyping을 설정한 커스텀 ObjectMapper를 Bean으로 생성해 직렬화기에 주입했습니다. 그러나 ChatRedisSubscriber에서는 Spring 컨테이너의 기본 ObjectMapper를 주입받아 역직렬화하고 있습니다.
현재 우연히 에러가 나지 않더라도, 직렬화/역직렬화에 사용되는 ObjectMapper 설정이 다르면 향후 DTO 필드가 변경되거나 타입 정보가 꼬일 경우 런타임 JsonProcessingException을 유발합니다. 명시적으로 동일한 설정을 가진 ObjectMapper를 사용하거나 설정을 일치시켜야 합니다.

3. WebSocket SUBSCRIBE 권한 검증 미흡
StompChannelInterceptor에서 인증(Authentication) 여부만 검사하고 있습니다. 실제로 해당 사용자가 구독하려는 /sub/chat/{roomId} 채팅방에 입장할 권한(참여자)이 있는지 비즈니스 로직 검증이 누락되어 있습니다. 악의적인 사용자가 다른 사람의 1:1 대화방을 구독할 수 있는 심각한 정보 유출 경로가 됩니다.

🔴 보안 / 성능 주의

1. Deserialization 취약점 (Remote Code Execution 위험)

objectMapper.activateDefaultTyping(
    objectMapper.getPolymorphicTypeValidator(),
    ObjectMapper.DefaultTyping.NON_FINAL
);

DefaultTyping.NON_FINAL을 활성화하면 JSON 페이로드에 임의의 클래스 타입을 명시하여 역직렬화를 유도할 수 있습니다. 이는 공격자가 서버 클래스패스에 존재하는 Gadget Chain을 이용해 원격 코드를 실행(RCE)할 수 있는 치명적인 보안 취약점입니다. 반드시 명시적인 허용 클래스 목록(화이트리스트)을 가진 BasicPolymorphicTypeValidator를 사용해야 합니다.

2. Redis Pub/Sub + SimpleBroker 혼용의 구조적 함정
WebSocketConfig에서 /sub prefix를 enableSimpleBroker로 설정했습니다. SimpleBroker는 Spring Application 내부의 인메모리 브로커입니다.
ChatRedisSubscriber가 메시지를 받아 simpMessagingTemplate.convertAndSend("/sub/chat/" + roomId, response)를 호출하면, 이 메시지는 Redis를 거치지 않고 서버 내부 로컬 메모리 브로커로 바로 전송됩니다.
결과적으로 메시지 발행/구독 흐름이 꼬이게 됩니다. 분산 서버 환경에서 채팅을 운영하려면 SimpleBroker 대신 외부 메시지 브로커(RabbitMQ, Redis 등)를 STOMP와 연동해야 합니다. 구조를 다시 설계하십시오.

💡 학습 포인트

  1. Spring STOMP와 External Broker: enableSimpleBroker는 단일 서버(Standalone) 환경에서만 정상 동작합니다. 다중 서버 환경에서 WebSocket 메시지를 안정적으로 라우팅하려면 registerStompEndpoints와 함께 enableStompBrokerRelay를 사용하거나, Redis Pub/Sub을 STOMP 브로커의 Relay로 사용하는 방식을 고민해야 합니다.
  2. Jackson 다형성(Polymorphism) 직렬화 보안: Spring Boot 2.x 이후부터는 기본적으로 BasicPolymorphicTypeValidator를 통해 안전한 패키지만 허용하도록 강제합니다. 커스텀 ObjectMapper를 생성할 때 기본 보안 규칙을 덮어쓰지 않았는지 항상 주의해야 합니다.

🤔 생각해보기

현재 코드는 Redis Pub/Sub으로 채팅 메시지를 브로드캐스트하지만, 서버가 재시작되거나 장애로 인해 Redis 채널을 구독하고 있지 않은 순간에 발행된 메시지는 어떻게 될까요? 사용자가 보낸 메시지가 영구적으로 유실되는 이 구조적 한계를 극복하려면 메시징 시스템의 아키텍처를 어떻게 변경해야 할까요?

💬 이 질문에 대해 궁금한 점이 있으면 코멘트에 @sparta 를 남겨보세요!
예: @sparta 메시지 유실 방지를 위해 Redis Stream이나 Kafka 도입이 필수적인가요?


AI 리뷰는 참고용입니다. 최종 판단은 팀원이 직접 합니다.

Copy link
Copy Markdown
Collaborator

@Max-1012 Max-1012 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Copy Markdown
Contributor

@DGAZA-max DGAZA-max left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승인하였습니다!

@CheatIsKey CheatIsKey merged commit 6a1ceec into develop Apr 24, 2026
2 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Allday-Project-Board Apr 24, 2026
@CheatIsKey CheatIsKey deleted the feat/Chat-WebSocket-Controller#109 branch April 24, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Feat] 실시간 채팅 WebSocket 연결

3 participants