Pull Request · #312

# 실시간 알림 채널 통합

머지됨 작성자 최민준 리뷰어 박도현 · 이수아 브랜치 `feat/realtime-notification` → `main` 파일 14개 변경 +847 −212

TL;DR

댓글 스레드 v1의 마지막 조각인 실시간 WebSocket 알림을 구현합니다. 서버 측 Redis Pub/Sub 브로커를 도입해 수평 확장을 보장하고, 클라이언트는 커넥션 단절 시 지수 백오프로 자동 재연결합니다. 기존 이메일 다이제스트 큐와 통합해 "조용한 시간대" 정책을 함께 지원합니다.

## 변경 전 / 후

이전

- 댓글 작성 후 페이지 새로고침 필요
- 알림: 이메일 배치(5분 주기) 단독
- 단일 인스턴스 인메모리 이벤트 버스
- 모바일 백그라운드 시 알림 유실

이후

- WebSocket으로 새 댓글 즉시 표시 (≤300 ms)
- 알림: WebSocket + 이메일 다이제스트 통합
- Redis Pub/Sub 기반 수평 확장 지원
- FCM 푸시 알림으로 모바일 백그라운드 커버

## 파일 변경 내역

▶ notification/WebSocketNotificationService.java NEW +312

WebSocket STOMP 메시지 브로드캐스트 서비스. Redis 채널 구독 후 연결된 모든 세션에 이벤트를 전파합니다.

    + @Service
    + public class WebSocketNotificationService implements ApplicationListener<CommentCreatedEvent> {
    +     private final SimpMessagingTemplate messaging;
    +     private final RedisPublisher publisher;

    +     @Override
    +     public void onApplicationEvent(CommentCreatedEvent event) {
    +         publisher.publish("notification:" + event.getThreadId(), event);
    +     }
    + }

▶ notification/config/WebSocketConfig.java NEW +68

STOMP over WebSocket 엔드포인트 설정. `/ws` 핸드셰이크 엔드포인트와 `/topic` 메시지 브로커를 등록합니다.

    + @Configuration
    + @EnableWebSocketMessageBroker
    + public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    +     @Override
    +     public void registerStompEndpoints(StompEndpointRegistry registry) {
    +         registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
    +     }
    + }

▶ frontend/hooks/useCommentNotification.ts NEW +147

React 훅 — STOMP 클라이언트 수명 주기 관리, 자동 재연결(지수 백오프), Zustand 스토어에 신규 댓글 merge.

    + export function useCommentNotification(threadId: string) {
    +   const addComment = useCommentStore(s => s.addComment);
    +   useEffect(() => {
    +     const client = new Client({ brokerURL: WS_URL });
    +     client.onConnect = () => {
    +       client.subscribe(`/topic/thread/${threadId}`, msg => {
    +         addComment(JSON.parse(msg.body));
    +       });
    +     };
    +     client.activate();
    +     return () => client.deactivate();
    +   }, [threadId]);
    + }

▶ notification/EmailDigestService.java 수정 +28 −45

이메일 다이제스트와 WebSocket 알림 중복 발송 방지 로직 추가. 사용자 "조용한 시간대" 설정을 존중하도록 리팩터링.

    - if (user.emailEnabled) {
    + if (user.emailEnabled && !user.isQuietHours()) {
          queue.enqueue(new EmailDigestJob(user, comment));
      }

## 핵심 설계 결정

1

Redis Pub/Sub 기반 인스턴스 간 이벤트 전파

단순 인메모리 이벤트 버스로는 로드 밸런서 뒤에서 특정 인스턴스에 연결된 클라이언트에게만 이벤트가 전달됩니다. Redis 채널을 허브로 삼아 모든 인스턴스가 구독하도록 설계해 수평 확장 시에도 유실 없이 브로드캐스트됩니다.

2

지수 백오프 자동 재연결

모바일 네트워크 불안정 시 폴링 폭풍(polling storm)을 막기 위해 클라이언트 측에서 1s → 2s → 4s → 8s (최대 30s) 지수 백오프를 적용합니다. @stomp/stompjs의 `reconnectDelay` 콜백에서 구현, 탭이 비활성이면 재연결 시도를 중단합니다.

3

피처 플래그로 단계적 롤아웃

모든 사용자에게 즉시 노출하지 않고 Unleash 피처 플래그 `ws_notification_v1`로 10% → 50% → 100% 단계 배포합니다. 플래그가 꺼진 사용자는 기존 폴링(30초) 방식으로 폴백됩니다.

## 테스트 계획

WebSocket 연결 성공 및 구독 메시지 수신 단위
Redis Pub/Sub 이벤트 → WebSocket 전파 E2E 통합
클라이언트 재연결 지수 백오프 동작 단위
조용한 시간대 이메일 다이제스트 미발송 통합
다중 인스턴스 환경 브로드캐스트 누락 없음 E2E
피처 플래그 OFF 사용자 폴링 폴백 확인 E2E

## 롤아웃 단계

단계 1

10%

내부 사용자

단계 2

50%

진행 중 · 이상 없음

단계 3

100%

2026-05-16 예정

모니터링

D+7

Datadog 대시보드