Files
sodalive-backend-spring-boot/docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md

51 KiB

유저-크리에이터 채팅 WebSocket 전환 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development 또는 superpowers:executing-plans로 task 단위 구현을 진행한다. 각 단계는 체크박스(- [ ])로 진행 상태를 갱신한다.

Goal: 유저-크리에이터 1:1 채팅의 SSE 실시간 연결을 제거하고, 채팅방 화면 진입 중에만 유지되는 raw WebSocket + Redis presence/pub-sub 구조로 전환한다.

Architecture: REST는 방 생성, 방 열기, 과거 메시지 조회, 음성 업로드에 유지하고 텍스트 실시간 송수신과 방 presence는 WebSocket으로 처리한다. 서버는 다중 인스턴스를 전제로 local WebSocket session registry와 Redis presence/pub-sub을 함께 사용한다. 상대방이 해당 roomId에 접속 중이면 WebSocket으로만 전달하고, 접속 중이 아니면 기존 FCM/APNs 푸시 흐름에 roomId/chat_type 이동 정보를 포함해 발송한다.

Tech Stack: Kotlin, Spring Boot 2.7.14, Java 17, Spring WebSocket, Spring Data Redis, Redis pub/sub, Spring Data JPA, JUnit 5, Mockito, Gradle Wrapper


0. 구현 전 확정 사항

  • 대상 PRD: docs/20260618_유저크리에이터채팅_WebSocket전환/prd.md
  • WebSocket endpoint: /ws/v2/user-creator-chat
  • WebSocket protocol: STOMP 없는 raw JSON envelope
  • WebSocket 연결 수명: 채팅방 화면 진입 중에만 유지
  • 서버 인스턴스 전제: 여러 대
  • presence 저장소: Redis
  • 서버 간 메시지 전달: Redis pub/sub
  • local memory에는 현재 서버에 붙은 WebSocket session만 저장
  • 기존 SSE는 완전히 제거
  • 기존 REST 유지:
    • POST /api/v2/user-creator-chat/rooms/create
    • GET /api/v2/user-creator-chat/rooms/{roomId}/open
    • GET /api/v2/user-creator-chat/rooms/{roomId}/messages
    • POST /api/v2/user-creator-chat/rooms/{roomId}/messages/voice
  • 텍스트 메시지 전송은 WebSocket SEND_TEXT로 전환
  • 푸시 발송 기준:
    • 상대방이 같은 roomId에 WebSocket presence 있음: 푸시 미발송
    • 상대방이 같은 roomId에 WebSocket presence 없음: 푸시 발송
  • 푸시 payload 필수값:
    • room_id
    • message_id
    • chat_type=USER_CREATOR
  • Redis key 기본안:
    • v2:user-creator-chat:ws:presence:{roomId}:{memberId}:{sessionId}
    • v2:user-creator-chat:ws:room:{roomId}:member:{memberId}:sessions
    • v2:user-creator-chat:ws:room:{roomId}
  • presence TTL 기본값: 90초

1. 파일 구조 계획

의존성/설정

  • Modify: build.gradle.kts
    • spring-boot-starter-websocket 의존성을 추가한다.
  • Modify: src/main/resources/application.yml
    • lazy loading 의존 API 점검과 수정 후 spring.jpa.open-in-view=false를 명시한다.
  • Modify: src/test/resources/application.yml
    • 테스트에서 OSIV off 회귀를 확인할 수 있도록 동일 설정을 검토한다.
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketConfig.kt
    • WebSocket handler endpoint를 등록한다.
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptor.kt
    • handshake에서 JWT 인증 정보를 추출한다.
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptorTest.kt

WebSocket protocol

  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketMessage.kt
    • request/response envelope와 message type enum을 둔다.
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketMessageTest.kt

WebSocket session/presence

  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketSessionRegistry.kt
    • local WebSocket session을 sessionId 기준으로 관리한다.
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatPresenceService.kt
    • Redis presence 등록, 갱신, 제거, 조회를 담당한다.
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBroker.kt
    • Redis pub/sub publish/subscribe와 local session 전송을 담당한다.
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketSessionRegistryTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatPresenceServiceTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBrokerTest.kt

WebSocket handler/application

  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandler.kt
    • WebSocket lifecycle과 JSON envelope dispatch를 담당한다.
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt
    • WebSocket 텍스트 메시지 저장/전달용 application method를 추가하고 SSE 의존성을 제거한다.
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandlerTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandshakeIntegrationTest.kt

푸시 payload

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt
    • 필요 시 chatType 또는 동등한 payload 값을 추가한다.
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt
    • chat_type data payload를 추가한다.
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/fcm/FcmServiceTest.kt

SSE 제거

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/controller/UserCreatorChatController.kt
    • events, events/disconnect, messages/text endpoint 제거 또는 WebSocket 전환에 맞춰 제거한다.
  • Delete: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatRealtimeService.kt
  • Modify: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt
  • Modify: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceIntegrationTest.kt
  • Modify: docs/plan-task/20260513_유저크리에이터채팅방개편.md
    • 클라이언트 연동 문서의 SSE 안내를 WebSocket 기준으로 갱신한다.

클라이언트 반영 문서

  • Modify: docs/20260618_유저크리에이터채팅_WebSocket전환/prd.md
    • iOS/Android 앱 변경 사항을 PRD에 유지한다.
  • Modify: docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
    • 서버 구현 task와 별도로 앱 반영 체크리스트를 유지한다.

2. WebSocket 메시지 계약 초안

구현 전 클라이언트와 공유할 JSON envelope 기준이다. 필드명 변경이 필요하면 PRD와 이 계획 문서를 먼저 갱신한다.

{
  "type": "SEND_TEXT",
  "requestId": "c7f1a263-0e4f-4f98-9bd4-70988575e9d7",
  "roomId": 10,
  "payload": {
    "textMessage": "hello"
  }
}

서버 응답 예시:

{
  "type": "MESSAGE",
  "requestId": null,
  "roomId": 10,
  "payload": {
    "messageId": 200,
    "messageType": "TEXT",
    "mine": false,
    "createdAt": 1781690400000,
    "textMessage": "hello",
    "voiceMessageUrl": null,
    "senderId": 2,
    "senderNickname": "creator",
    "senderProfileImageUrl": "https://cdn.test/profile/creator.png"
  }
}

서버 ack 예시:

{
  "type": "SEND_ACK",
  "requestId": "c7f1a263-0e4f-4f98-9bd4-70988575e9d7",
  "roomId": 10,
  "payload": {
    "messageId": 201,
    "messageType": "TEXT",
    "mine": true,
    "createdAt": 1781690401000,
    "textMessage": "hello",
    "voiceMessageUrl": null,
    "senderId": 1,
    "senderNickname": "user",
    "senderProfileImageUrl": "https://cdn.test/profile/user.png"
  }
}

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

앱은 서버 배포와 같은 릴리스 범위에서 아래 변경을 반영해야 한다. 서버 구현자가 직접 앱 코드를 수정하지 않더라도, API 계약과 검증 기준은 이 문서에 유지한다.

채팅방 진입

  • 기존 SSE 연결 생성 코드를 제거한다.
  • 채팅방 화면 진입 시 GET /api/v2/user-creator-chat/rooms/{roomId}/open을 먼저 호출한다.
  • openRoom 응답으로 기존 메시지 목록과 상대방 프로필/닉네임을 렌더링한다.
  • 이후 WebSocket /ws/v2/user-creator-chat에 연결한다.
  • handshake에는 Authorization: Bearer <accessToken> 헤더를 포함한다.
  • 연결 직후 아래 메시지를 보낸다.
{
  "type": "JOIN_ROOM",
  "requestId": "client-request-id",
  "roomId": 10,
  "payload": {}
}
  • JOINED 수신 전에는 텍스트 전송 버튼을 비활성화하거나 전송 대기 상태로 처리한다.

텍스트 메시지 전송

  • 기존 POST /api/v2/user-creator-chat/rooms/{roomId}/messages/text 호출을 제거한다.
  • 텍스트 메시지는 아래 WebSocket 메시지로 전송한다.
{
  "type": "SEND_TEXT",
  "requestId": "client-request-id",
  "roomId": 10,
  "payload": {
    "textMessage": "hello"
  }
}
  • 앱은 requestId를 pending 메시지와 매칭한다.
  • SEND_ACK를 수신하면 pending 메시지를 서버 응답의 messageId, createdAt, senderProfileImageUrl 기준으로 확정한다.
  • ERROR 또는 timeout이 발생하면 메시지를 실패 상태로 표시하고 재시도 UI를 제공한다.

메시지 수신

  • MESSAGE 이벤트 수신 시 현재 열려 있는 roomId와 일치하는지 확인한 뒤 메시지 목록에 append한다.
  • 현재 채팅방과 다른 roomIdMESSAGE를 받으면 버리거나 로그로 남긴다. 이번 서버 설계에서는 같은 WebSocket session이 하나의 roomId만 활성 방으로 가지므로 정상 상황에서는 발생하지 않아야 한다.

화면 이탈과 재연결

  • 채팅방 화면 이탈, 앱 백그라운드 진입, 로그아웃 시 아래 메시지를 보낸 뒤 WebSocket을 close한다.
{
  "type": "LEAVE_ROOM",
  "requestId": "client-request-id",
  "roomId": 10,
  "payload": {}
}
  • 현재 채팅방 화면에 남아 있는 동안 WebSocket이 끊기면 지수 백오프로 재연결한다.
  • 재연결 성공 후 JOIN_ROOM을 다시 보내고, 필요하면 GET /api/v2/user-creator-chat/rooms/{roomId}/messages로 누락 메시지를 동기화한다.
  • access token이 refresh되면 기존 WebSocket을 닫고 새 token으로 다시 연결한다.

푸시 이동

  • 푸시 payload의 chat_type == "USER_CREATOR"room_id를 확인한다.
  • 사용자가 푸시를 터치하면 room_id에 해당하는 채팅방 화면으로 이동한다.
  • 푸시 진입 후에도 일반 진입과 동일하게 openRoom 호출 후 WebSocket 연결과 JOIN_ROOM을 수행한다.
  • room_id가 없거나 숫자로 파싱할 수 없으면 채팅방 직접 이동을 하지 않고 앱 기본 알림 처리로 fallback 한다.

클라이언트 제거 대상

  • GET /api/v2/user-creator-chat/rooms/{roomId}/events 호출
  • POST /api/v2/user-creator-chat/rooms/{roomId}/events/disconnect 호출
  • POST /api/v2/user-creator-chat/rooms/{roomId}/messages/text 호출
  • SSE reconnect/retry 처리 코드

Phase 0: OSIV 비활성화 사전 점검

  • Task 0.1: 현재 OSIV 설정과 위험 패턴 조사

    • Files:
      • Modify: docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
    • TDD 예외 사유: 코드 변경 전 사전 조사 task로, 자동화 테스트보다 정적 검색과 결과 문서화가 목적이다.
    • 대체 검증 방법:
      • Run: rg -n "open-in-view|spring.jpa" src/main/resources src/test/resources
      • Expected: 현재 OSIV 명시 여부를 확인한다.
      • Run: rg -n "member\\?\\.auth|member\\.auth|member\\?\\.notification|member\\.notification|ApiResponse\\.ok\\([^\\n]*(repository|findBy|findAll)|ResponseEntity\\.ok\\([^\\n]*(repository|findBy|findAll)" src/main/kotlin/kr/co/vividnext/sodalive
      • Expected: controller 또는 응답 직렬화 단계에서 lazy loading이 일어날 수 있는 후보를 찾는다.
      • Run: rg -n "@Service|@Transactional|findByIdOrNull|findById\\(|findAll\\(|\\.member|\\.sender|\\.recipient|\\.auth|\\.chatRoom|\\.series|\\.audioContent" src/main/kotlin/kr/co/vividnext/sodalive
      • Expected: 트랜잭션 없는 service/facade/query method에서 LAZY 연관을 접근하는 후보를 찾는다.
    • GREEN: 발견 항목을 이 문서의 OSIV 점검 기록 섹션에 API/파일/위험/수정 방향 형식으로 기록한다.
    • 통과 확인:
      • Run: rg -n "OSIV 점검 기록|lazy loading|open-in-view" docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
      • Expected: 조사 결과와 후속 조치가 문서에 기록되어야 한다.
    • 검증 기록:
      • 무엇: 현재 OSIV 명시 여부와 lazy loading 위험 후보를 정적 검색으로 조사했다.
      • 왜: spring.jpa.open-in-view=false 전환 전에 controller 응답 직렬화나 인증 principal 접근에서 트랜잭션 밖 lazy 접근 가능성을 확인하기 위해서다.
      • 어떻게: rg -n "open-in-view|spring.jpa" src/main/resources src/test/resources, rg -n "member\\?\\.auth|member\\.auth|member\\?\\.notification|member\\.notification|ApiResponse\\.ok\\([^\\n]*(repository|findBy|findAll)|ResponseEntity\\.ok\\([^\\n]*(repository|findBy|findAll)" src/main/kotlin/kr/co/vividnext/sodalive, rg -n "@Service|@Transactional|findByIdOrNull|findById\\(|findAll\\(|\\.member|\\.sender|\\.recipient|\\.auth|\\.chatRoom|\\.series|\\.audioContent" src/main/kotlin/kr/co/vividnext/sodalive를 실행했다.
      • 결과: main/test application.yml 모두 spring.jpa는 있으나 open-in-view는 명시되어 있지 않았다. EventController, UserActionController, AuditionController, CreatorAdminContentSeriesGenreController, AudioContentCommentController 등에서 MemberAdapter.member.auth 직접 접근 후보가 확인되었다. QueryDSL repository의 member.auth, series.member, audioContent.member 접근은 쿼리 식 내부 경로가 대부분이라 즉시 lazy 직렬화 위험으로 분류하지 않았다.
  • Task 0.2: OSIV off 테스트 실행 범위 선정

    • Files:
      • Modify: docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
    • TDD 예외 사유: 구현 전 테스트 전략 정의 task다.
    • 대체 검증 방법:
      • Run: find src/test/kotlin/kr/co/vividnext/sodalive -name '*ControllerTest.kt' | sort
      • Expected: OSIV off 영향이 드러날 가능성이 높은 controller 테스트 목록을 선별한다.
    • GREEN: 인증 principal의 lazy 접근, entity 직접 반환, controller DTO 변환이 있는 API를 우선순위로 선정한다.
    • 통과 확인:
      • Run: rg -n "OSIV off 우선 테스트|ControllerTest|MockMvc" docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
      • Expected: 우선 실행할 테스트 목록 또는 기준이 문서에 기록되어야 한다.
    • OSIV off 우선 테스트:
      • kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest: @SpringBootTest, @AutoConfigureMockMvc, @Transactional, MemberAdapter를 함께 사용해 실제 JPA/MockMvc 경계와 인증 principal 전달 표면을 확인할 수 있다.
      • kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest: @SpringBootTest, @AutoConfigureMockMvc, @Transactional, MemberAdapter를 함께 사용해 인증 회원 id 기반 조회 표면을 확인할 수 있다.
      • kr.co.vividnext.sodalive.v2.api.creator.channel.home.adapter.in.web.CreatorChannelHomeControllerTest: @WebMvcTest라 JPA lazy loading 자체 검증은 제한적이지만 controller가 MemberAdapter.member를 facade로 전달하는 표면 회귀를 확인할 수 있다.
      • kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest: @WebMvcTest라 JPA lazy loading 자체 검증은 제한적이지만 인증 principal 전달 표면 회귀를 확인할 수 있다.
      • kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceIntegrationTest, kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest: 현재 유저-크리에이터 채팅 전용 ControllerTest가 없어, service 트랜잭션 안 DTO 변환과 lazy 접근 안전성을 보조 확인한다.
    • 검증 기록:
      • 무엇: OSIV off 영향이 드러날 가능성이 높은 controller/service 테스트 범위를 선정했다.
      • 왜: Phase 0.3에서 무작위 전체 테스트가 아니라 인증 principal lazy 접근, entity 직접 반환, DTO 변환 경계가 있는 테스트를 우선 실행하기 위해서다.
      • 어떻게: rg --files src/test/kotlin/kr/co/vividnext/sodalive | rg 'ControllerTest\\.kt$', rg -n "@SpringBootTest|@AutoConfigureMockMvc|@Transactional|MockMvc|MemberAdapter" src/test/kotlin/kr/co/vividnext/sodalive -g "*ControllerTest.kt", rg --files src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat를 실행했다.
      • 결과: HomeRecommendationControllerTest, CreatorRankingControllerTest를 우선 통합 테스트로 선정하고, CreatorChannelHomeControllerTest, CreatorChannelLiveControllerTest, user-creator-chat service 테스트를 보조 범위로 선정했다.
  • Task 0.3: OSIV off로 후보 테스트를 실행하고 실패를 분류

    • Files:
      • Modify: docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
    • TDD 예외 사유: 설정 전환 영향 조사 task다.
    • 대체 검증 방법:
      • Run: ./gradlew test -Dspring.jpa.open-in-view=false --tests '<선정한 ControllerTest 클래스명>'
      • Expected: 성공하면 해당 API는 우선 위험 낮음으로 기록한다. LazyInitializationException이 발생하면 파일/필드/API를 기록한다.
    • GREEN: 실패를 다음 유형으로 분류한다.
      • controller에서 인증 principal lazy 연관 접근
      • 응답 직렬화 중 entity lazy 연관 접근
      • service/facade 트랜잭션 밖 DTO 변환 중 lazy 연관 접근
      • 테스트 fixture가 @Transactional로 문제를 숨기는 경우
    • 통과 확인:
      • Run: rg -n "LazyInitializationException|OSIV off 실패|OSIV off 성공" docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
      • Expected: 테스트 결과와 분류가 문서에 기록되어야 한다.
    • OSIV off 성공:
      • Run: ./gradlew --no-daemon cleanTest test -Dkotlin.compiler.execution.strategy=in-process -Dspring.jpa.open-in-view=false --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceIntegrationTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest --tests '*CreatorChannelHomeControllerTest' --tests '*CreatorChannelLiveControllerTest' --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest
      • Result: BUILD SUCCESSFUL in 1m 24s
      • XML 확인: UserCreatorChatServiceIntegrationTest 2개, UserCreatorChatServiceTest 8개, CreatorChannelHomeControllerTest 2개, CreatorChannelLiveControllerTest 5개, HomeRecommendationControllerTest 21개, CreatorRankingControllerTest 3개 모두 failures=0, errors=0.
      • 분류: 선정한 user-creator-chat service/integration, 홈/랭킹 controller 통합 테스트, 보조 WebMvc controller 테스트 범위에서는 확인된 LazyInitializationException 없음. user-creator-chat DTO 변환은 현재 service 트랜잭션 안에서 수행되는 것으로 판단했다.
  • Task 0.4: lazy loading 의존 제거 후 OSIV off 명시

    • Files:
      • Modify: src/main/resources/application.yml
      • Modify: src/test/resources/application.yml
      • Modify: lazy loading 의존 API별 service/repository/controller/test 파일
      • Modify: docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
    • RED: Task 0.3에서 확인한 LazyInitializationException 재현 테스트를 먼저 고정한다.
    • 실패 확인:
      • Run: ./gradlew test -Dspring.jpa.open-in-view=false --tests '<실패 재현 테스트>'
      • Expected: 기존 코드에서는 LazyInitializationException 또는 동등한 실패가 발생한다.
    • GREEN: controller lazy 접근을 service/query 계층의 트랜잭션 안 DTO projection, fetch join, 명시 조회로 이동한다.
    • GREEN: application.yml과 필요한 경우 test application.yml에 아래 설정을 명시한다.
spring:
    jpa:
        open-in-view: false
  • 통과 확인:
    • Run: ./gradlew test -Dspring.jpa.open-in-view=false --tests '<수정한 테스트>'
    • Expected: BUILD SUCCESSFUL
  • REFACTOR: WebSocket 전환 작업과 관계없는 API 스키마 변경은 하지 않는다.
  • 검증 기록:
    • 무엇: main/test 설정에 spring.jpa.open-in-view=false를 명시했다.
    • 왜: Phase 0.3에서 user-creator-chat service/integration, 홈/랭킹 controller 통합 테스트, 보조 WebMvc controller 테스트 범위에 LazyInitializationException이 없었고, OSIV 정책을 명시해야 이후 WebSocket 전환 작업의 트랜잭션 경계를 안전하게 유지할 수 있기 때문이다.
    • 어떻게: src/main/resources/application.yml, src/test/resources/application.ymlspring.jpa 아래에 open-in-view: false를 추가했다.
    • 결과: 확인된 lazy loading 재현 실패가 없어 production code의 service/repository/controller 수정은 하지 않았다. 공개 API 스키마 변경도 없다.

Phase 1: WebSocket 의존성과 인증 handshake 기반 추가

  • Task 1.1: WebSocket 의존성 추가

    • Files:
      • Modify: build.gradle.kts
    • RED: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketConfigTest.kt를 추가해 /ws/v2/user-creator-chat handler bean 등록을 기대한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketConfigTest
      • Expected: WebSocket 관련 타입 또는 config bean 부재로 실패한다.
    • GREEN: implementation("org.springframework.boot:spring-boot-starter-websocket")를 추가한다.
    • 통과 확인:
      • Run: ./gradlew tasks --all
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: 의존성 추가 외 다른 dependency 정렬/버전 변경은 하지 않는다.
    • 검증 기록:
      • 무엇: spring-boot-starter-websocket 의존성을 추가했다.
      • 왜: raw WebSocket endpoint와 handshake interceptor 타입을 사용하기 위해서다.
      • 어떻게: WebSocket 타입 부재로 compileTestKotlin RED를 확인한 뒤 의존성을 추가하고 Phase 1 focused 테스트를 재실행했다.
      • 결과: ./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketConfigTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketAuthInterceptorTestBUILD SUCCESSFUL in 1m 11s로 통과했다.
  • Task 1.2: WebSocket config와 handler 등록

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketConfig.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandler.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketConfigTest.kt
    • RED: config 테스트에서 handler가 /ws/v2/user-creator-chat 경로로 등록되는지 검증한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketConfigTest
      • Expected: config class 부재로 실패한다.
    • GREEN: WebSocketConfigurer를 구현하고 TextWebSocketHandler 기반 handler를 등록한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketConfigTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: CORS origin은 기존 WebConfig의 허용 origin 정책과 어긋나지 않게 제한한다.
    • 검증 기록:
      • 무엇: /ws/v2/user-creator-chat endpoint를 WebSocketConfigurer로 등록하고 TextWebSocketHandler 기반 handler를 추가했다.
      • 왜: 채팅방 화면 진입 중 유지되는 raw WebSocket 연결 기반을 만들기 위해서다.
      • 어떻게: UserCreatorChatWebSocketConfigTest에서 endpoint path 등록을 검증했다.
      • 결과: Phase 1 focused 테스트가 BUILD SUCCESSFUL로 통과했다. allowed origin은 기존 WebConfig origin 목록과 동일하게 제한했다.
  • Task 1.3: WebSocket handshake JWT 인증 추가

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptor.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptorTest.kt
    • RED: Authorization: Bearer <token> 헤더가 없거나 유효하지 않으면 handshake가 실패하는 테스트를 작성한다.
    • RED: 유효한 토큰이면 attributes에 memberId와 인증 principal이 저장되는 테스트를 작성한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketAuthInterceptorTest
      • Expected: interceptor class 부재로 실패한다.
    • GREEN: 기존 TokenProvider.getAuthentication(token)을 사용해 인증하고, MemberAdapter.member.id를 session attributes에 저장한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketAuthInterceptorTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: JWT parsing 로직을 새로 만들지 않고 기존 TokenProvider를 재사용한다.
    • 검증 기록:
      • 무엇: Authorization: Bearer <token> handshake 인증 interceptor를 추가했다.
      • 왜: WebSocket session attributes에 인증 member id와 authentication을 저장해 이후 room join/message 처리에서 사용할 수 있게 하기 위해서다.
      • 어떻게: TokenProvider.validateToken(token)TokenProvider.getAuthentication(token)을 재사용하고, MemberAdapter.member.idmemberId attribute로 저장했다.
      • 결과: 유효 token 성공, Authorization header 누락 실패, invalid token 실패 테스트가 Phase 1 focused 테스트에서 통과했다.

Phase 2: 메시지 프로토콜과 local session registry 추가

  • Task 2.1: WebSocket message envelope 정의

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketMessage.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketMessageTest.kt
    • RED: JOIN_ROOM, SEND_TEXT, LEAVE_ROOM, PING, JOINED, MESSAGE, SEND_ACK, ERROR, PONG enum 값과 JSON deserialize 테스트를 작성한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketMessageTest
      • Expected: message class 부재로 실패한다.
    • GREEN: request/response envelope와 type enum을 추가한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketMessageTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: payload는 초기 구현에서 JsonNode로 받고, 타입별 request DTO 변환은 handler dispatch에서 수행한다.
    • 검증 기록:
      • 무엇: WebSocket request/response envelope와 message type enum을 추가했다.
      • 왜: Phase 4 handler dispatch에서 raw JSON 메시지를 공통 계약으로 역직렬화하기 위해서다.
      • 어떻게: UserCreatorChatWebSocketMessagetype, requestId, roomId, payload: JsonNode를 갖는 data class로 두고, UserCreatorChatWebSocketMessageTypeJOIN_ROOM, SEND_TEXT, LEAVE_ROOM, PING, JOINED, MESSAGE, SEND_ACK, ERROR, PONG을 정의했다.
      • RED: ./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketMessageTest 실행 시 UserCreatorChatWebSocketMessageType, UserCreatorChatWebSocketMessage unresolved reference로 compileTestKotlin 실패를 확인했다.
      • 결과: ./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketMessageTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketSessionRegistryTestBUILD SUCCESSFUL in 4m 30s로 통과했다.
  • Task 2.2: local WebSocket session registry 추가

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketSessionRegistry.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketSessionRegistryTest.kt
    • RED: roomId/memberId/sessionId 등록, 조회, 제거, 같은 session의 room 전환 시 기존 room 제거 테스트를 작성한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketSessionRegistryTest
      • Expected: registry class 부재로 실패한다.
    • GREEN: ConcurrentHashMap 기반 registry를 추가한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketSessionRegistryTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: registry는 local session만 관리하고 Redis를 직접 호출하지 않는다.
    • 검증 기록:
      • 무엇: local WebSocket session registry를 추가했다.
      • 왜: 다중 인스턴스 구조에서 현재 서버에 붙은 session만 roomId/memberId/sessionId 기준으로 관리하기 위해서다.
      • 어떻게: ConcurrentHashMap으로 room/member별 session map과 sessionId index를 관리하고, 같은 session이 다른 room으로 등록되면 기존 room mapping을 제거하도록 했다. 동시 같은 session room 전환은 sessionId hash 기반 고정 striped lock으로 register/remove를 직렬화했다. Redis 호출은 포함하지 않았다.
      • RED: ./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketSessionRegistryTest 실행 시 UserCreatorChatWebSocketSessionRegistry unresolved reference로 compileTestKotlin 실패를 확인했다.
      • RED: reviewer 지적 후 추가한 동시 같은 session room 전환 테스트가 기존 구현에서 Expected concurrent same-session room switch to leave exactly one active room mapping assertion으로 실패함을 확인했다.
      • RED: 운영 트래픽에서 sessionId별 lock map이 누적될 수 있다는 리뷰를 반영해 추가한 sessionId별 lock map을 유지하지 않는다 테스트가 기존 구현에서 실패함을 확인했다.
      • 결과: ./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketSessionRegistryTestBUILD SUCCESSFUL in 3m 41s로 통과했다.
      • 결과: 실제 실행한 Phase 2 focused 명령 ./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketMessageTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketSessionRegistryTestBUILD SUCCESSFUL in 1m 46s로 통과했다.

Phase 3: Redis presence와 Redis pub/sub 추가

  • Task 3.1: Redis presence service 추가

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatPresenceService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatPresenceServiceTest.kt
    • RED: markJoined, refresh, markLeft, hasPresence(roomId, memberId) 동작을 embedded Redis 또는 mock RedisTemplate으로 검증한다.
    • RED: TTL이 설정되는지 검증한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceServiceTest
      • Expected: presence service 부재로 실패한다.
    • GREEN: Redis key/value와 session set index를 저장하고 TTL 90초를 적용한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceServiceTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: key prefix는 companion object 상수로 모으고 기존 SSE presence key와 섞이지 않게 ws segment를 포함한다.
  • Task 3.2: Redis pub/sub room broker 추가

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBroker.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBrokerTest.kt
    • RED: publish 시 v2:user-creator-chat:ws:room:{roomId} channel로 메시지를 발행하는 테스트를 작성한다.
    • RED: subscribe callback이 local registry에서 대상 member session만 찾아 전송하는 테스트를 작성한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBrokerTest
      • Expected: broker class 부재로 실패한다.
    • GREEN: Redis pub/sub publisher와 listener를 추가한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBrokerTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: broker는 DB 저장을 하지 않고 이미 만들어진 message DTO만 전달한다.

Phase 4: WebSocket handler와 메시지 저장/전달

  • Task 4.1: JOIN_ROOM 처리

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandler.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandlerTest.kt
    • RED: 인증 member가 참여 중인 room에 JOIN_ROOM을 보내면 JOINED 응답과 local/Redis presence 등록이 수행되는 테스트를 작성한다.
    • RED: 참여자가 아닌 room이면 ERROR 후 close 되는 테스트를 작성한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketHandlerTest
      • Expected: JOIN_ROOM dispatch 부재로 실패한다.
    • GREEN: handler에서 JOIN_ROOM을 처리하고 service의 참여자 검증 method를 호출한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketHandlerTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: 기존 private requireParticipant 재사용이 필요하면 public/internal 검증 method로 최소 노출한다.
  • Task 4.2: SEND_TEXT 저장, sender ack, 수신자 WebSocket 전달

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandler.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandlerTest.kt
    • RED: 상대방 presence가 있으면 메시지를 저장하고 broker.publish를 호출하며 푸시 이벤트를 발행하지 않는 테스트를 작성한다.
    • RED: sender에게 SEND_ACK가 전송되는 handler 테스트를 작성한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketHandlerTest
      • Expected: WebSocket send method 부재로 실패한다.
    • GREEN: sendTextMessage의 저장 로직을 재사용하되 UserCreatorChatPresenceServiceUserCreatorChatRoomMessageBroker 기준으로 전달 여부를 판단한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketHandlerTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: 기존 REST text endpoint 제거 전까지 중복 저장 로직이 생기지 않도록 private save method로만 분리한다.
  • Task 4.3: 상대방 미접속 시 푸시 발송

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/fcm/FcmServiceTest.kt
    • RED: 상대방 presence가 없으면 FcmEventroomId, messageId, chatType=USER_CREATOR 정보를 포함하는 테스트를 작성한다.
    • RED: FcmService가 FCM data payload에 chat_type을 넣는 테스트를 작성한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest --tests kr.co.vividnext.sodalive.fcm.FcmServiceTest
      • Expected: chat_type payload 부재로 실패한다.
    • GREEN: FcmEvent 또는 FcmService.send에 chat type을 추가하고 user-creator chat push 발행 시 채운다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest --tests kr.co.vividnext.sodalive.fcm.FcmServiceTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: 기존 live/content/community deep link payload와 충돌하지 않게 chat_type은 message category에서만 채운다.
  • Task 4.4: LEAVE_ROOM, close, heartbeat 처리

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandler.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatPresenceService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandlerTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatPresenceServiceTest.kt
    • RED: LEAVE_ROOM과 WebSocket close 시 local registry와 Redis presence가 제거되는 테스트를 작성한다.
    • RED: PING 수신 시 presence TTL 갱신과 PONG 응답 테스트를 작성한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketHandlerTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceServiceTest
      • Expected: LEAVE_ROOM/PING 처리 부재로 실패한다.
    • GREEN: handler lifecycle callback과 message dispatch에 정리/heartbeat 처리를 추가한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketHandlerTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceServiceTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: close와 LEAVE_ROOM 정리 로직은 같은 private method를 사용한다.
  • Task 4.5: WebSocket client handshake 통합 테스트 추가

    • Files:
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandshakeIntegrationTest.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt (필요한 경우에만)
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptor.kt (필요한 경우에만)
    • RED: @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)에서 실제 서버 포트를 띄우고 StandardWebSocketClient/ws/v2/user-creator-chat에 접속하는 테스트를 작성한다.
    • RED: 유효한 Authorization: Bearer <accessToken> header가 있으면 handshake가 성공하고, header가 없거나 유효하지 않으면 handshake가 실패하는 테스트를 작성한다.
    • RED: 유효 token은 테스트 member를 저장한 뒤 기존 TokenProvider.createToken(...)으로 생성해, JwtFilter, SecurityConfig, UserCreatorChatWebSocketAuthInterceptor가 함께 동작하는 경계를 검증한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketHandshakeIntegrationTest
      • Expected: 통합 테스트 파일 부재 또는 실제 client handshake 경계 미구현으로 실패한다.
    • GREEN: 테스트가 실패하면 최소 수정만 적용한다. 예를 들어 security filter가 /ws/v2/user-creator-chat handshake를 interceptor까지 통과시키지 못하는 경우에만 해당 경로 정책을 조정한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketHandshakeIntegrationTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: 단위 테스트가 이미 검증하는 token parsing 세부 로직을 중복하지 않고, 실제 client handshake 성공/실패와 security/interceptor 연결 경계만 검증한다.

Phase 5: 기존 SSE 제거와 REST 경계 정리

  • Task 5.1: SSE controller endpoint 제거

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/controller/UserCreatorChatController.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt
    • RED: disconnectRealtime 관련 테스트를 제거하거나 WebSocket close/presence 테스트로 이동한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest
      • Expected: 제거 대상 method 참조가 남아 있으면 실패한다.
    • GREEN: /events, /events/disconnect, /messages/text REST endpoint를 제거한다. 텍스트 메시지는 WebSocket 전송만 허용한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: 사용하지 않는 MediaType.TEXT_EVENT_STREAM_VALUE import를 제거한다.
  • Task 5.2: UserCreatorChatRealtimeService 제거

    • Files:
      • Delete: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatRealtimeService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt
      • Modify: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt
    • RED: service 테스트에서 SSE realtime mock 의존성을 제거하고 WebSocket presence/broker mock을 주입하도록 변경한다.
    • 실패 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest
      • Expected: constructor signature 불일치 또는 미구현 mock으로 실패한다.
    • GREEN: UserCreatorChatService 생성자와 메시지 전달 로직에서 UserCreatorChatRealtimeService를 제거한다.
    • 통과 확인:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest
      • Expected: BUILD SUCCESSFUL
    • REFACTOR: rg -n "SseEmitter|connectEvents|disconnectRealtime|TEXT_EVENT_STREAM|UserCreatorChatRealtimeService" src/main src/test로 잔여 참조가 없는지 확인한다.
  • Task 5.3: 클라이언트 연동 문서 갱신

    • Files:
      • Modify: docs/plan-task/20260513_유저크리에이터채팅방개편.md
      • Modify: docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
    • TDD 예외 사유: 문서 갱신은 자동화 테스트 작성 대상이 아니다.
    • 대체 검증 방법:
      • Run: rg -n "GET /api/v2/user-creator-chat/rooms/\\{roomId\\}/events|events/disconnect|messages/text|JOIN_ROOM|SEND_TEXT|LEAVE_ROOM|chat_type" docs/20260618_유저크리에이터채팅_WebSocket전환
      • Expected: 제거 대상 API와 신규 WebSocket/푸시 계약이 모두 문서에 명시되어야 한다.
    • GREEN: 클라이언트 연동 순서를 openRoom REST 호출 후 WebSocket connect + JOIN_ROOM으로 갱신한다.
    • 통과 확인:
      • Run: ./gradlew tasks --all
      • Expected: BUILD SUCCESSFUL
  • Task 5.4: iOS/Android 앱 반영 체크리스트 확인

    • Files:
      • Modify: docs/20260618_유저크리에이터채팅_WebSocket전환/prd.md
      • Modify: docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
    • TDD 예외 사유: 앱 코드가 현재 서버 저장소에 없으므로 자동화 테스트를 작성할 수 없다.
    • 대체 검증 방법:
      • Run: rg -n "iOS|Android|JOIN_ROOM|SEND_ACK|MESSAGE|LEAVE_ROOM|푸시|room_id|chat_type" docs/20260618_유저크리에이터채팅_WebSocket전환/prd.md docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md
      • Expected: 클라이언트 변경 사항이 PRD와 plan-task 양쪽에 기록되어야 한다.
    • GREEN: 앱 담당자가 구현해야 할 진입, 전송, 수신, 이탈, 재연결, 푸시 이동, 제거 대상 API를 문서에 유지한다.
    • 통과 확인:
      • Run: ./gradlew tasks --all
      • Expected: BUILD SUCCESSFUL

4. 최종 회귀 검증

  • Run: rg -n "open-in-view" src/main/resources src/test/resources
    • Expected: OSIV 정책이 명시되어 있어야 한다.
  • Run: ./gradlew test -Dspring.jpa.open-in-view=false --tests '<OSIV off 우선 테스트 목록>'
    • Expected: BUILD SUCCESSFUL
  • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.*
    • Expected: BUILD SUCCESSFUL
  • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketHandshakeIntegrationTest
    • Expected: BUILD SUCCESSFUL
  • Run: ./gradlew test --tests kr.co.vividnext.sodalive.fcm.FcmServiceTest
    • Expected: BUILD SUCCESSFUL
  • Run: ./gradlew ktlintCheck
    • Expected: BUILD SUCCESSFUL
  • Run: ./gradlew test
    • Expected: BUILD SUCCESSFUL
  • Run: rg -n "SseEmitter|TEXT_EVENT_STREAM|connectEvents|disconnectRealtime|UserCreatorChatRealtimeService" src/main src/test
    • Expected: 검색 결과 없음
  • Run: rg -n "spring-boot-starter-websocket|/ws/v2/user-creator-chat|chat_type|USER_CREATOR" build.gradle.kts src/main src/test
    • Expected: WebSocket 의존성, endpoint, 푸시 payload 관련 구현이 확인됨

5. 구현 후 검증 기록

  • Phase 2:
    • Run: ./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketMessageTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketSessionRegistryTest
    • Result: BUILD SUCCESSFUL in 1m 46s; message envelope enum/JsonNode 역직렬화 테스트 3개와 local session registry 등록/조회/제거/room 전환/동시 같은 session 전환/session lock map 비유지 테스트 6개가 통과했다.
  • Phase 0:
    • Run: rg -n "open-in-view|spring.jpa" src/main/resources src/test/resources
    • Result: main/test 설정의 spring.jpa.open-in-view=false 명시를 확인했다.
    • Run: ./gradlew --no-daemon cleanTest test -Dkotlin.compiler.execution.strategy=in-process -Dspring.jpa.open-in-view=false --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceIntegrationTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest --tests '*CreatorChannelHomeControllerTest' --tests '*CreatorChannelLiveControllerTest' --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest
    • Result: BUILD SUCCESSFUL in 1m 24s; XML 기준 UserCreatorChatServiceIntegrationTest 2개, UserCreatorChatServiceTest 8개, CreatorChannelHomeControllerTest 2개, CreatorChannelLiveControllerTest 5개, HomeRecommendationControllerTest 21개, CreatorRankingControllerTest 3개 모두 failures=0, errors=0.

6. OSIV 점검 기록

  • API/기능:
    • 파일: src/main/resources/application.yml, src/test/resources/application.yml
    • 위험 유형: OSIV 정책 미명시
    • lazy 접근 대상: 해당 없음
    • OSIV off 테스트: rg -n "open-in-view|spring.jpa" src/main/resources src/test/resources
    • 수정 방향: main/test spring.jpa.open-in-view=false 명시
    • 처리 상태: 완료
  • API/기능: 유저-크리에이터 채팅 room open/messages/voice service 경계
    • 파일: src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceIntegrationTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt
    • 위험 유형: service/facade 트랜잭션 밖 DTO 변환 중 lazy 연관 접근 가능성
    • lazy 접근 대상: UserCreatorChatParticipant.member, UserCreatorChatMessage.participant
    • OSIV off 테스트: Phase 0 묶음 검증 명령에 포함
    • 수정 방향: 현재 범위에서는 DTO 변환이 service 트랜잭션 안에서 수행되어 추가 수정 없음
    • 처리 상태: XML 기준 UserCreatorChatServiceIntegrationTest 2개, UserCreatorChatServiceTest 8개 모두 failures=0, errors=0
  • API/기능: 홈 추천/크리에이터 랭킹 controller 통합 테스트
    • 파일: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt
    • 위험 유형: controller 요청 처리 또는 응답 직렬화 중 lazy 연관 접근 가능성
    • lazy 접근 대상: MemberAdapter.member.auth, 홈/랭킹 조회 응답 DTO 관련 entity 연관
    • OSIV off 테스트: Phase 0 묶음 검증 명령에 포함
    • 수정 방향: 확인된 LazyInitializationException 없음. 추가 lazy 수정 없음
    • 처리 상태: XML 기준 HomeRecommendationControllerTest 21개, CreatorRankingControllerTest 3개 모두 failures=0, errors=0
  • API/기능: 크리에이터 채널 home/live WebMvc controller 표면
    • 파일: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt
    • 위험 유형: controller에서 인증 principal을 facade로 전달하는 표면 회귀 가능성
    • lazy 접근 대상: MemberAdapter.member
    • OSIV off 테스트: Phase 0 묶음 검증 명령에 포함
    • 수정 방향: JPA lazy loading 직접 검증은 아니며, WebMvc 표면 회귀만 확인
    • 처리 상태: XML 기준 CreatorChannelHomeControllerTest 2개, CreatorChannelLiveControllerTest 5개 모두 failures=0, errors=0