Files

22 KiB

PRD: 유저-크리에이터 채팅 WebSocket 전환

1. Overview

유저-크리에이터 1:1 채팅의 실시간 전송 방식을 SSE에서 WebSocket으로 전환해, 네이티브 앱의 채팅방 화면 진입 중에는 실시간 메시지를 받고 푸시는 받지 않으며, 화면 밖에서는 푸시로 채팅방 이동 정보를 받을 수 있게 한다.


2. Problem

  • 현재 GET /api/v2/user-creator-chat/rooms/{roomId}/events SSE 연결은 모바일 네이티브 앱의 HTTP connection pool과 충돌해, SSE 연결 중 다른 API 동작이 지연되거나 막히는 문제가 발생한다.
  • SSE는 서버에서 클라이언트로만 이벤트를 보내는 단방향 방식이므로, 1:1 채팅의 접속 상태, 방 진입/이탈, 메시지 송수신 생명주기를 표현하기에 부적합하다.
  • 기존 SSE presence는 서버 메모리와 Redis TTL을 함께 사용하지만, 여러 서버 인스턴스에서 메시지를 받는 사용자 세션이 어느 서버에 있는지 전달하는 구조가 없다.
  • 클라이언트 요구사항은 "해당 채팅방 화면에 있으면 푸시 미발송, 화면에 없으면 푸시 발송 및 푸시 터치 시 해당 방 이동"이므로, 방 단위의 정확한 presence가 필요하다.

3. Goals

  • 유저-크리에이터 채팅 실시간 연결을 SSE에서 WebSocket으로 전환한다.
  • 기존 SSE endpoint와 SseEmitter 기반 구현은 제거한다.
  • WebSocket 연결은 앱 로그인 전체 수명이 아니라 채팅방 화면에 들어와 있을 때만 유지한다.
  • 서버가 여러 대라고 가정하고 Redis를 사용해 방 단위 presence와 서버 간 메시지 전달을 처리한다.
  • WebSocket 전환과 별도로 spring.jpa.open-in-view=false 적용 가능성을 점검하고, 트랜잭션 밖 lazy loading에 의존하는 API를 먼저 식별한다.
  • 같은 roomId 채팅방 화면에 상대방이 접속 중이면 새 메시지를 WebSocket으로 전달하고 푸시는 발송하지 않는다.
  • 상대방이 해당 roomId 채팅방 화면에 접속 중이 아니면 새 메시지 저장 후 푸시를 발송한다.
  • 푸시 payload에는 앱이 해당 채팅방으로 이동할 수 있는 deep_link만 포함한다.
  • 기존 방 생성, 방 열기, 과거 메시지 조회, 음성 메시지 업로드 API는 유지한다.
  • 텍스트 메시지는 WebSocket으로 전송하고, 서버는 저장 결과를 sender에게 ack로 돌려준다.

4. Non-Goals

  • 전체 앱 로그인 수명 동안 유지되는 글로벌 WebSocket 연결은 이번 범위에 포함하지 않는다.
  • AI 캐릭터 채팅, 라이브 채팅, 기존 /api/chat/room 기능은 이번 범위에서 변경하지 않는다.
  • 메시지 읽음 처리, typing indicator, 온라인 사용자 목록 노출은 이번 범위에 포함하지 않는다.
  • 음성 파일 자체를 WebSocket binary로 전송하지 않는다.
  • DB 스키마 변경은 이번 범위에 포함하지 않는다.
  • STOMP broker 도입은 이번 범위에 포함하지 않는다.

5. Target Users

  • iOS/Android 네이티브 앱 사용자: 채팅방 화면에서 실시간으로 메시지를 주고받는 회원
  • 모바일 클라이언트: 채팅방 화면 진입/이탈 시 연결 생명주기와 푸시 이동 처리를 구현하는 클라이언트
  • 운영 서버: 여러 인스턴스에서 동일한 presence와 메시지 전달 정책을 유지해야 하는 서버

6. User Stories

  • 사용자는 채팅방 화면에 들어와 있으면 새 메시지를 즉시 보고 싶고 같은 메시지의 푸시는 받고 싶지 않다.
  • 사용자는 채팅방 화면 밖에 있으면 상대방 메시지를 푸시로 받고 싶다.
  • 사용자는 푸시 알림을 터치하면 해당 유저-크리에이터 채팅방으로 바로 이동하고 싶다.
  • 앱은 채팅방 진입 시 초기 메시지를 REST로 조회하고 이후 새 메시지는 WebSocket으로 받고 싶다.
  • 서버는 여러 인스턴스 중 어느 인스턴스에 상대방 WebSocket session이 붙어 있어도 메시지를 전달하거나 푸시 여부를 올바르게 결정해야 한다.

7. Core Features

Feature A. WebSocket 연결과 인증

Requirements

  • WebSocket endpoint는 /ws/v2/user-creator-chat을 기본안으로 한다.
  • 네이티브 앱은 WebSocket handshake에 기존 Authorization: Bearer <accessToken> 헤더를 전달한다.
  • 서버는 기존 JWT 검증 흐름을 재사용해 인증 회원을 식별한다.
  • 인증 실패 시 WebSocket 연결을 수락하지 않는다.
  • 연결은 특정 채팅방 화면에 들어왔을 때만 생성한다.
  • 연결 직후 클라이언트는 JOIN_ROOM 메시지로 roomId를 전달한다.
  • 서버는 JOIN_ROOM 처리 시 회원이 해당 방의 활성 참여자인지 검증한다.
  • 검증 성공 시 서버는 해당 WebSocket session을 roomId/memberId/sessionId/serverId 기준으로 등록한다.
  • 하나의 WebSocket session은 하나의 roomId만 활성 방으로 가진다.

Edge Cases

  • 인증은 성공했지만 JOIN_ROOMroomId 참여자가 아니면 error 메시지를 보내고 연결을 종료한다.
  • 같은 회원이 같은 방을 여러 기기에서 열 수 있으므로 presence는 session 단위로 관리한다.
  • 같은 session에서 다른 roomId로 다시 JOIN_ROOM을 보내면 기존 방 presence를 제거한 뒤 새 방으로 전환한다.

Feature B. WebSocket 메시지 프로토콜

Requirements

  • WebSocket은 raw JSON message protocol을 사용한다.
  • 공통 envelope는 다음 필드를 사용한다.
{
  "type": "JOIN_ROOM",
  "requestId": "client-generated-id",
  "roomId": 10,
  "payload": {}
}
  • 클라이언트에서 서버로 보내는 메시지 타입은 다음을 기본으로 한다.
    • JOIN_ROOM: 방 입장 및 presence 등록
    • SEND_TEXT: 텍스트 메시지 저장 및 전달
    • LEAVE_ROOM: 방 이탈 및 presence 제거
    • PING: 연결 유지 확인
  • 서버에서 클라이언트로 보내는 메시지 타입은 다음을 기본으로 한다.
    • JOINED: 방 입장 성공
    • MESSAGE: 새 메시지 수신
    • SEND_ACK: sender에게 메시지 저장 결과 전달
    • ERROR: 처리 실패
    • PONG: PING 응답
  • SEND_TEXT payload는 { "textMessage": "..." }를 사용한다.
  • MESSAGESEND_ACK payload는 기존 UserCreatorChatMessageItemDto와 같은 메시지 item 구조를 사용한다.
  • 빈 문자열 또는 공백뿐인 텍스트 메시지는 기존 sendTextMessage와 동일하게 common.error.invalid_request 의미의 error로 처리한다.

Edge Cases

  • 클라이언트가 JOIN_ROOM 전에 SEND_TEXT를 보내면 ERROR를 반환한다.
  • 알 수 없는 typeERROR를 반환하되 서버 connection은 유지한다.
  • 메시지 저장 성공 후 sender ack 전송이 실패해도 DB 저장은 롤백하지 않는다.

Feature C. Redis 기반 presence와 다중 서버 메시지 전달

Requirements

  • 서버는 여러 대라고 가정한다.
  • 각 서버 인스턴스는 고유한 serverId를 가진다. 기본값은 application start 시 생성한 UUID이며, 운영 환경에서는 env 기반 지정도 허용한다.
  • local memory에는 현재 서버에 붙은 WebSocket session만 저장한다.
  • Redis에는 방 단위 presence를 session 단위로 저장한다.
  • Redis key 기본안:
    • session presence: v2:user-creator-chat:ws:presence:{roomId}:{memberId}:{sessionId}
    • room member index: v2:user-creator-chat:ws:room:{roomId}:member:{memberId}:sessions
    • pub/sub channel: v2:user-creator-chat:ws:room
  • presence value에는 serverId, memberId, roomId, sessionId, lastSeenAt을 포함한다.
  • WebSocket 연결 유지 중 heartbeat 또는 메시지 송수신 시 presence TTL을 갱신한다.
  • presence TTL 기본값은 90초로 한다.
  • 서버는 메시지 저장 후 상대방의 해당 roomId presence가 Redis에 하나 이상 있는지 확인한다.
  • 상대방 presence가 있으면 Redis pub/sub으로 고정 channel에 메시지를 발행한다.
  • Redis pub/sub 메시지 payload에는 roomId, memberId, payload를 포함하고, 수신 서버는 payload의 roomId/memberId를 기준으로 local session을 필터링한다.
  • 운영 Redis는 AWS ElastiCache Serverless Valkey 7.2 또는 Redis OSS 7.1을 사용할 수 있으므로, Redis pattern subscribe가 필요한 PSUBSCRIBE/PatternTopic 방식은 사용하지 않는다.
  • Redis listener는 SUBSCRIBE 기반의 고정 ChannelTopic만 사용한다.
  • 각 서버는 자신에게 연결된 session 중 대상 roomId/memberId session에만 메시지를 전송한다.
  • 상대방 presence가 없으면 푸시 이벤트를 발행한다.

Edge Cases

  • Redis presence는 남아 있지만 실제 WebSocket 전송이 실패하면 해당 local session을 정리한다.
  • Redis pub/sub 전달 실패 또는 Redis 장애 시에는 presence 판단을 신뢰할 수 없으므로 푸시 발송 쪽으로 fail-open 한다.
  • presence TTL 만료 전 앱이 비정상 종료되어도 최대 TTL 이후에는 오프라인으로 판단되어 푸시가 발송되어야 한다.

Feature D. 푸시 발송과 채팅방 이동 payload

Requirements

  • 상대방이 해당 채팅방 화면에 있지 않으면 기존 FCM/APNs 푸시 발송 흐름을 사용한다.
  • 푸시 category는 기존 PushNotificationCategory.MESSAGE를 사용한다.
  • 푸시 payload에는 deep_link만 포함한다.
    • 운영: voiceon://chat/{roomId}
    • 개발/테스트: voiceon-test://chat/{roomId}
  • v2 채팅 푸시에서는 기존 room_id, message_id, chat_type data payload를 사용하지 않는다.
  • 앱은 푸시 터치 시 deep_link에서 {roomId}를 해석해 GET /api/v2/user-creator-chat/rooms/{roomId}/open 호출 후 WebSocket 연결을 시작한다.

Edge Cases

  • 푸시 발송 대상 push token이 없으면 메시지 저장은 성공하고 pushSent == false 의미의 내부 결과를 남긴다.
  • 상대방이 여러 기기 중 하나에서 같은 방을 열고 있으면 푸시는 발송하지 않는다.
  • 상대방이 다른 채팅방을 열고 있거나 앱의 다른 화면에 있으면 현재 방 presence가 아니므로 푸시를 발송한다.

Feature E. 기존 SSE 제거

Requirements

  • GET /api/v2/user-creator-chat/rooms/{roomId}/events endpoint를 제거한다.
  • POST /api/v2/user-creator-chat/rooms/{roomId}/events/disconnect endpoint를 제거한다.
  • UserCreatorChatRealtimeServiceSseEmitter 기반 구현을 제거한다.
  • SSE 관련 DTO, 테스트, 문서 언급은 WebSocket 기준으로 갱신한다.
  • 클라이언트 연동 문서에는 기존 SSE API가 더 이상 사용되지 않음을 명시한다.

Edge Cases

  • 제거된 SSE endpoint를 호출하면 Spring MVC 기본 404 또는 security 정책에 따른 기존 오류 흐름을 따른다.
  • 기존 REST 메시지 조회/방 생성/open API는 제거하지 않는다.

Feature F. iOS/Android 클라이언트 변경 사항

Current Native App Usage

  • 개발 중 테스트 중이던 iOS/Android 네이티브 앱은 채팅방 화면 진입 시 GET /api/v2/user-creator-chat/rooms/{roomId}/events SSE 연결을 사용하고 있었다.
  • 해당 앱은 채팅방 화면 이탈, 앱 백그라운드 진입, 로그아웃 등 실시간 수신을 중단해야 하는 시점에 POST /api/v2/user-creator-chat/rooms/{roomId}/events/disconnect를 호출하고 있었다.
  • WebSocket 전환 후에는 위 두 API가 제거되므로, 서버 배포와 같은 릴리스 범위에서 네이티브 앱의 SSE 연결/해제 코드도 함께 변경되어야 한다.

Requirements

  • 클라이언트는 채팅방 화면 진입 시 기존 SSE 연결을 열지 않는다.
  • 클라이언트는 채팅방 화면 진입 시 기존 GET /api/v2/user-creator-chat/rooms/{roomId}/open으로 초기 메시지와 상대방 정보를 조회한다.
  • openRoom 성공 후 WebSocket /ws/v2/user-creator-chat에 연결한다.
  • WebSocket handshake에는 기존 API와 동일한 access token을 Authorization: Bearer <accessToken> 헤더로 전달한다.
  • WebSocket 연결 직후 JOIN_ROOM 메시지를 전송한다.
  • JOINED를 수신하면 해당 채팅방이 실시간 수신 상태라고 판단한다.
  • 기존 SSE connected 이벤트 기반 연결 확인 로직은 WebSocket JOINED 수신 기준으로 변경한다.
  • 기존 SSE message 이벤트 수신 로직은 WebSocket MESSAGE 수신 로직으로 변경한다.
  • 텍스트 메시지 전송은 기존 POST /api/v2/user-creator-chat/rooms/{roomId}/messages/text 대신 WebSocket SEND_TEXT를 사용한다.
  • 텍스트 메시지 전송 UI는 requestId를 생성해 pending 메시지와 서버 SEND_ACK를 매칭한다.
  • SEND_ACK를 수신하면 pending 메시지를 서버가 내려준 messageId, createdAt, 프로필 정보 기준으로 확정한다.
  • 상대방 메시지는 MESSAGE 이벤트 수신 시 현재 채팅방 메시지 목록에 append한다.
  • 음성 메시지는 기존 multipart REST API를 유지한다.
  • 기존 events/disconnect 호출 위치는 WebSocket LEAVE_ROOM 전송 후 socket close 처리로 대체한다.
  • 채팅방 화면 이탈, 앱 백그라운드 진입, 로그아웃 시 LEAVE_ROOM을 보낸 뒤 WebSocket을 close한다.
  • 네트워크 오류로 WebSocket이 끊기면 현재 채팅방 화면에 남아 있는 동안에만 재연결한다.
  • 재연결 성공 후 JOIN_ROOM을 다시 보내고, 필요하면 REST messages API로 누락 메시지를 동기화한다.
  • 기존 SSE retry/backoff 코드는 WebSocket 재연결 정책으로 대체하고, 채팅방 화면 밖에서는 재연결하지 않는다.
  • 앱은 heartbeat로 PING을 주기적으로 보내고 PONG을 수신해 연결 상태를 판단한다.
  • 푸시 알림을 터치하면 payload의 deep_link를 확인해 해당 채팅방 화면으로 이동한다.
  • 푸시로 채팅방에 진입한 뒤에도 일반 진입과 동일하게 openRoom 호출 후 WebSocket 연결/JOIN_ROOM을 수행한다.
  • 클라이언트는 제거된 SSE endpoint와 events/disconnect endpoint를 더 이상 호출하지 않는다.
  • 클라이언트는 기존 POST /api/v2/user-creator-chat/rooms/{roomId}/messages/text 응답의 deliveredRealtime/pushSent를 텍스트 전송 UI 판단에 사용하지 않는다. 텍스트 전송 성공 여부는 WebSocket SEND_ACK/ERROR/timeout으로 판단한다.

Native App Migration Checklist

  • SSE client 또는 EventSource wrapper 제거: GET /api/v2/user-creator-chat/rooms/{roomId}/events 호출, Accept: text/event-stream, SSE event parser, SSE reconnect/retry timer를 삭제한다.
  • 연결 확인 기준 변경: SSE connected 이벤트 수신 완료를 WebSocket JOINED 수신 완료로 대체한다.
  • 메시지 수신 기준 변경: SSE message event payload append를 WebSocket MESSAGE envelope payload append로 대체한다.
  • 연결 해제 기준 변경: POST /api/v2/user-creator-chat/rooms/{roomId}/events/disconnect 호출을 제거하고, 같은 lifecycle 위치에서 LEAVE_ROOM 메시지를 보낸 뒤 WebSocket을 close한다.
  • 텍스트 전송 변경: POST /api/v2/user-creator-chat/rooms/{roomId}/messages/text 호출을 제거하고, SEND_TEXT 메시지와 SEND_ACK 매칭 방식으로 pending/성공/실패 상태를 관리한다.
  • 음성 전송 유지: POST /api/v2/user-creator-chat/rooms/{roomId}/messages/voice multipart 호출은 유지하되, 음성 전송 후 상대방 실시간 수신 여부는 서버 정책에 따른다.
  • 토큰 갱신 처리 변경: access token refresh 시 기존 WebSocket을 close하고 새 token의 Authorization 헤더로 다시 연결한 뒤 JOIN_ROOM을 다시 보낸다.
  • 푸시 이동 처리 확인: deep_linkvoiceon://chat/{roomId} 또는 voiceon-test://chat/{roomId}를 기준으로 채팅방에 진입하고, 진입 후 openRoom 호출과 WebSocket JOIN_ROOM을 일반 진입과 동일하게 수행한다.

Edge Cases

  • WebSocket 연결은 성공했지만 JOIN_ROOM이 실패하면 채팅방 화면에 오류를 표시하고 텍스트 전송을 막는다.
  • SEND_TEXT 후 일정 시간 안에 SEND_ACK가 오지 않으면 메시지를 전송 실패 상태로 표시하고 재시도 UI를 제공한다.
  • 재연결 전 pending 메시지를 자동 재전송할 경우 같은 requestId를 재사용해 중복 표시를 방지한다.
  • 앱이 다른 채팅방으로 이동하면 기존 방 WebSocket session을 종료하고 새 방으로 다시 연결한다.
  • 푸시 payload에 deep_link가 없거나 voiceon://chat/{roomId} / voiceon-test://chat/{roomId} 형식에서 roomId를 해석할 수 없으면 채팅방 직접 이동을 하지 않고 앱 기본 알림 처리 흐름을 따른다.

Feature G. OSIV 비활성화 사전 점검

Requirements

  • 현재 spring.jpa.open-in-view 설정이 명시되어 있는지 확인한다.
  • 설정이 명시되어 있지 않으면 Spring Boot 2.7 기본값상 OSIV enabled로 동작할 수 있으므로, 운영 설정에 명시적으로 둘지 여부를 결정한다.
  • spring.jpa.open-in-view=false를 바로 적용하기 전에 트랜잭션 밖 lazy loading 의존 API를 먼저 식별한다.
  • 점검 대상은 다음 패턴을 포함한다.
    • controller에서 @AuthenticationPrincipal로 받은 Member의 LAZY 연관(auth, notification 등)을 직접 접근하는 코드
    • controller가 JPA entity 또는 LAZY 연관을 가진 객체를 그대로 ApiResponse.ok(...)로 반환하는 코드
    • @Transactional이 없는 service/facade/query method에서 repository 조회 후 LAZY 연관을 DTO 변환에 사용하는 코드
    • Jackson 직렬화 시점에 JPA entity의 LAZY 연관이 열릴 수 있는 응답
    • self-invocation 때문에 기대한 @Transactional이 적용되지 않는 service 내부 호출
  • 발견된 항목은 API 경로, 파일 경로, lazy 접근 대상, 권장 수정 방향을 문서에 기록한다.
  • 권장 수정 방향은 controller에서 lazy 접근을 하지 않고 service/query 계층의 트랜잭션 안에서 DTO projection, fetch join, 명시 조회로 필요한 값을 채우는 것이다.
  • lazy loading 의존성이 해소된 뒤에만 spring.jpa.open-in-view=false를 application 설정에 명시한다.

Edge Cases

  • 테스트 코드가 @Transactional로 감싸져 있으면 OSIV off 문제를 가릴 수 있으므로 controller/MockMvc 또는 실제 HTTP 계층 테스트를 우선한다.
  • 인증 principal의 Member는 JWT filter에서 로드된 엔티티이므로, controller에서 LAZY 연관을 직접 열면 OSIV off 후 실패할 수 있다.
  • 관리자/크리에이터/사용자 API가 서로 다른 controller 패키지에 흩어져 있으므로 특정 패키지 검색만으로 점검을 끝내지 않는다.
  • OSIV off 적용 후 일부 API가 실패하면 WebSocket 전환과 섞어 수정하지 않고, lazy loading 제거 task로 분리해 먼저 처리한다.

Feature H. OSIV off lazy loading 회귀 보완

Requirements

  • 운영에서 확인된 LazyInitializationException 발생 지점을 우선 수정한다.
  • ChatCharacterController.getCharacterDetail 응답 조립에 필요한 ChatCharacter.tagMappings.tag는 OSIV off 상태에서도 접근 가능해야 한다.
  • HomeService.fetchData의 크리에이터 랭킹 응답 조립에 필요한 Member.tags.tag는 OSIV off 상태에서도 접근 가능해야 한다.
  • 동일 변환 메서드(toExplorerSectionCreator)를 쓰는 기존 랭킹 조회도 같은 쿼리 선로딩 정책을 공유해야 한다.
  • 공개 API 응답 스키마는 변경하지 않는다.

Edge Cases

  • 컬렉션 크기만 접근하면 nested LAZY proxy(mapping.tag)는 초기화되지 않을 수 있다.
  • 조회 테스트에 @Transactional이 붙어 있으면 서비스 반환 후 lazy 접근 실패를 가릴 수 있다.
  • fetch join으로 one-to-many를 가져오면 중복 row가 생길 수 있으므로 결과 중복 여부를 검증한다.

8. UX / UI Expectations

  • 채팅방 화면 진입 시 REST openRoom 응답으로 초기 화면을 그리고, WebSocket JOINED 이후 새 메시지를 실시간으로 append한다.
  • 채팅방 화면에 있는 동안 같은 방의 메시지 푸시가 나타나지 않아야 한다.
  • 채팅방 화면 밖에서 푸시를 터치하면 deep_link{roomId}에 해당하는 채팅방으로 이동해야 한다.
  • WebSocket 재연결 중 사용자가 보낸 메시지는 앱에서 전송 실패 또는 재시도 상태로 표시할 수 있어야 한다.
  • 앱 백그라운드 진입 또는 화면 이탈 시 LEAVE_ROOM을 보내고 WebSocket을 close한다.
  • 앱은 기존 SSE 연결 코드와 events/disconnect 호출 코드를 제거한다.
  • 앱은 텍스트 메시지 전송 성공 기준을 HTTP 200 응답이 아니라 WebSocket SEND_ACK 수신으로 변경한다.

9. Technical Constraints

  • Kotlin + Java 17 + Spring Boot 2.7.14 + Gradle Wrapper 구조를 유지한다.
  • WebSocket 구현은 spring-boot-starter-websocket을 사용한다.
  • STOMP는 도입하지 않고 raw WebSocket JSON protocol을 사용한다.
  • Redis는 현재 연결된 인프라를 사용한다.
  • RedisTemplate 또는 Redisson 중 기존 코드 패턴과 테스트 용이성을 기준으로 선택하되, presence TTL과 pub/sub을 모두 구현해야 한다.
  • Redis presence TTL, session set 정리, pub/sub listener 전달은 mock 검증만으로 완료하지 않고 embedded Redis 또는 동등한 실제 Redis 테스트 인프라로 통합 검증한다.
  • spring.jpa.open-in-view=false는 lazy loading 의존 API 점검과 수정이 끝난 뒤 명시한다.
  • 공개 REST API 스키마는 필요한 범위 외에는 변경하지 않는다.
  • 텍스트 메시지 저장은 기존 UserCreatorChatMessage 엔티티와 repository를 사용한다.
  • 음성 메시지 업로드는 기존 REST multipart API를 유지한다.
  • 기존 FcmEvent/FcmService 구조를 우선 재사용한다.
  • iOS/Android 클라이언트는 WebSocket 전용 연결을 일반 REST API 호출과 분리해 관리한다.
  • iOS/Android 클라이언트는 access token refresh 시 기존 WebSocket을 닫고 새 token으로 재연결한다.

10. Metrics

  • 채팅방 화면에 접속 중인 수신자에게 같은 방 메시지 푸시가 발송된 건수 0건
  • 채팅방 화면 밖 수신자에게 메시지 푸시가 누락된 건수 0건
  • WebSocket JOIN_ROOM 성공률
  • WebSocket 연결 중 메시지 전송 성공률
  • Redis presence TTL 만료로 정리된 orphan session 수

11. Open Questions

  • 없음. 이번 문서 기준 확정안은 채팅방 화면 진입 중에만 유지하는 raw WebSocket, Redis 기반 다중 서버 presence/pub-sub, 기존 SSE 완전 제거다.