docs(user-creator-chat): WebSocket Phase 3 기록을 갱신한다

This commit is contained in:
2026-06-18 19:09:42 +09:00
parent 282bc078e5
commit 2d13f8dee7
2 changed files with 55 additions and 2 deletions

View File

@@ -450,7 +450,7 @@ spring:
### Phase 3: Redis presence와 Redis pub/sub 추가 ### Phase 3: Redis presence와 Redis pub/sub 추가
- [ ] **Task 3.1: Redis presence service 추가** - [x] **Task 3.1: Redis presence service 추가**
- Files: - Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatPresenceService.kt` - 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` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatPresenceServiceTest.kt`
@@ -464,8 +464,16 @@ spring:
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceServiceTest` - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceServiceTest`
- Expected: `BUILD SUCCESSFUL` - Expected: `BUILD SUCCESSFUL`
- REFACTOR: key prefix는 companion object 상수로 모으고 기존 SSE presence key와 섞이지 않게 `ws` segment를 포함한다. - REFACTOR: key prefix는 companion object 상수로 모으고 기존 SSE presence key와 섞이지 않게 `ws` segment를 포함한다.
- 검증 기록:
- 무엇: WebSocket session 단위 Redis presence service를 추가했다.
- 왜: 다중 서버에서 상대방이 같은 `roomId`에 접속 중인지 판단하고, session별 join/refresh/leave 상태를 TTL 기반으로 관리하기 위해서다.
- 어떻게: `StringRedisTemplate`으로 `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}` 키를 저장하고 90초 TTL을 적용했다.
- RED: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceServiceTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBrokerTest` 실행 시 `UserCreatorChatPresenceService` unresolved reference로 `compileTestKotlin` 실패를 확인했다.
- Reviewer RED: 남은 session id가 TTL 만료로 stale 상태일 때 마지막 live session이 leave 해도 member presence가 즉시 제거되지 않는다는 지적을 받고, stale session 테스트가 `ArgumentsAreDifferent`로 실패함을 확인했다.
- 결과: stale session id를 presence key 기준으로 정리하고 live session이 없을 때 member/room presence를 제거하도록 수정했다. `UserCreatorChatPresenceServiceTest``BUILD SUCCESSFUL in 1m 22s`로 통과했고, Phase 3 WebSocket focused 테스트가 `BUILD SUCCESSFUL in 31s`로 통과했다.
- Reviewer 보강: PRD의 Redis presence value 계약에 맞춰 value를 문자열 `1`에서 `serverId`, `memberId`, `roomId`, `sessionId`, `lastSeenAt` JSON으로 변경했다. `user-creator-chat.websocket.server-id`가 비어 있으면 애플리케이션 시작 시 UUID를 serverId로 사용한다.
- [ ] **Task 3.2: Redis pub/sub room broker 추가** - [x] **Task 3.2: Redis pub/sub room broker 추가**
- Files: - Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBroker.kt` - 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` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBrokerTest.kt`
@@ -479,6 +487,39 @@ spring:
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBrokerTest` - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBrokerTest`
- Expected: `BUILD SUCCESSFUL` - Expected: `BUILD SUCCESSFUL`
- REFACTOR: broker는 DB 저장을 하지 않고 이미 만들어진 message DTO만 전달한다. - REFACTOR: broker는 DB 저장을 하지 않고 이미 만들어진 message DTO만 전달한다.
- 검증 기록:
- 무엇: Redis pub/sub room broker와 `RedisMessageListenerContainer` bean을 추가했다.
- 왜: 서버 인스턴스 간 room 메시지를 Redis channel로 전달하고, 수신 인스턴스가 local registry에서 대상 member session만 찾아 WebSocket으로 전송하기 위해서다.
- 어떻게: publish는 `v2:user-creator-chat:ws:room:{roomId}` channel에 `roomId`, `memberId`, `payload` JSON을 발행하고, listener는 `v2:user-creator-chat:ws:room:*` pattern topic을 구독해 대상 local session에 `TextMessage(payload)`를 전송한다. broker는 DB 저장을 하지 않는다.
- RED: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceServiceTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBrokerTest` 실행 시 `UserCreatorChatRoomMessageBroker`, `UserCreatorChatRoomPublishedMessage` unresolved reference로 `compileTestKotlin` 실패를 확인했다.
- Reviewer RED: 같은 member의 local session 중 하나가 `IOException`으로 실패하면 이후 정상 session 전송이 중단되는 테스트가 기존 구현에서 실패함을 확인했다.
- 결과: session별 전송 실패를 격리하고 실패 session만 local registry에서 제거하도록 수정했다. `UserCreatorChatRoomMessageBrokerTest``BUILD SUCCESSFUL in 21s`로 통과했고, Phase 3 WebSocket focused 테스트가 `BUILD SUCCESSFUL in 21s`로 통과했다.
- [x] **Task 3.3: Redis presence/pub-sub embedded Redis 통합 테스트 추가**
- Files:
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRedisIntegrationTest.kt`
- RED: `@SpringBootTest``@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])`로 실제 Redis에 `markJoined`를 호출하면 presence key, member session set, room set이 저장되고 presence key TTL이 0보다 큰지 검증한다.
- RED: `markLeft` 호출 시 실제 Redis에서 session presence key와 마지막 member session set, room member entry가 정리되는지 검증한다.
- RED: stale session id가 member session set에 남아 있고 presence key가 없으면 `hasPresence(roomId, memberId)`가 false를 반환하고 stale session id를 set에서 제거하는지 검증한다.
- RED: `UserCreatorChatRoomMessageBroker.publish`가 실제 Redis pub/sub을 통해 listener까지 도달하고, local registry의 대상 member session에 payload를 전달하는지 검증한다.
- 실패 확인:
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRedisIntegrationTest`
- Expected: 통합 테스트 파일 부재 또는 실제 Redis 경계 미검증으로 실패한다.
- GREEN: production code 변경 없이 기존 `UserCreatorChatPresenceService`, `UserCreatorChatRoomMessageBroker`, `RedisMessageListenerContainer`가 embedded Redis에서 동작하도록 테스트를 추가한다. 실패가 있으면 Redis serialization/listener wiring에 필요한 최소 수정만 적용한다.
- 통과 확인:
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRedisIntegrationTest`
- Expected: `BUILD SUCCESSFUL`
- REFACTOR: Redis 통합 테스트는 `src/test/resources/META-INF/spring.factories` 전역 등록 없이 `EmbeddedRedisInitializer`를 명시적으로 opt-in 한다.
- 범위 한계:
- Phase 3에서는 Redis presence/pub-sub 인프라를 실제 Redis 기준으로 검증한다.
- 실제 WebSocket `JOIN_ROOM`, `SEND_TEXT`, `LEAVE_ROOM`, `PING`, 메시지 저장/ack/푸시 분기까지 포함한 end-to-end 흐름은 Phase 4에서 검증한다.
- 검증 기록:
- 무엇: embedded Redis 기반 `UserCreatorChatRedisIntegrationTest`를 추가했다.
- 왜: mock 단위 테스트가 아니라 실제 Redis key/TTL/set 저장, stale session pruning, Redis pub/sub listener 전달 경계를 확인하기 위해서다.
- 어떻게: `@SpringBootTest``@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])`로 opt-in embedded Redis를 사용하고, `UserCreatorChatPresenceService`, `UserCreatorChatRoomMessageBroker`, `UserCreatorChatWebSocketSessionRegistry`, `StringRedisTemplate`을 실제 Spring context에서 주입받아 검증했다.
- RED: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRedisIntegrationTest` 실행 시 테스트 파일 부재로 `No tests found for given includes` 실패를 확인했다.
- GREEN: 같은 focused 명령을 `cleanTest`와 함께 순차 재실행해 `BUILD SUCCESSFUL in 33s`로 통과했다. join presence key/member session set/room set/TTL, last session leave 정리, stale session pruning, Redis pub/sub listener를 통한 target local session payload 전달을 확인했다.
- Reviewer 보강 GREEN: embedded Redis 테스트에서 `user-creator-chat.websocket.server-id=redis-test-server`를 주입하고 실제 Redis presence value JSON의 `serverId`, `memberId`, `roomId`, `sessionId`, `lastSeenAt`과 TTL을 함께 검증하도록 갱신했다.
--- ---
@@ -655,6 +696,17 @@ spring:
## 5. 구현 후 검증 기록 ## 5. 구현 후 검증 기록
- Phase 3:
- Run: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRedisIntegrationTest`
- RED Result: 테스트 파일 부재 상태에서 `No tests found for given includes`로 실패했다.
- GREEN Result: `BUILD SUCCESSFUL in 33s`; embedded Redis 기준 presence key/member session set/room set/TTL, markLeft 정리, stale session pruning, Redis pub/sub listener delivery 테스트 4개가 통과했다.
- Run: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceServiceTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBrokerTest`
- RED Result: `UserCreatorChatPresenceService`, `UserCreatorChatRoomMessageBroker`, `UserCreatorChatRoomPublishedMessage` unresolved reference로 `compileTestKotlin` 실패를 확인했다.
- GREEN Result: `BUILD SUCCESSFUL in 3m 22s`; presence join/refresh/leave/hasPresence 테스트 4개와 broker publish/pattern subscribe/local target delivery 테스트 3개가 통과했다.
- Reviewer 보강 RED: stale session id가 남은 상태에서 마지막 live session이 leave 하는 테스트가 기존 구현에서 `ArgumentsAreDifferent`로 실패했다.
- Reviewer 보강 GREEN: `UserCreatorChatPresenceServiceTest``BUILD SUCCESSFUL in 1m 22s`, WebSocket focused 테스트 묶음이 `BUILD SUCCESSFUL in 31s`, `./gradlew --no-daemon ktlintCheck``BUILD SUCCESSFUL in 50s`로 통과했다.
- Broker 보강 RED: broken local session의 `sendMessage``IOException`을 던질 때 같은 member의 healthy session 전송이 중단되어 `UserCreatorChatRoomMessageBrokerTest`가 실패했다.
- Broker 보강 GREEN: `UserCreatorChatRoomMessageBrokerTest``BUILD SUCCESSFUL in 21s`, WebSocket focused 테스트 묶음이 `BUILD SUCCESSFUL in 21s`, `./gradlew --no-daemon ktlintCheck``BUILD SUCCESSFUL in 37s`로 통과했다.
- Phase 2: - 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` - 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개가 통과했다. - Result: `BUILD SUCCESSFUL in 1m 46s`; message envelope enum/JsonNode 역직렬화 테스트 3개와 local session registry 등록/조회/제거/room 전환/동시 같은 session 전환/session lock map 비유지 테스트 6개가 통과했다.

View File

@@ -254,6 +254,7 @@
- STOMP는 도입하지 않고 raw WebSocket JSON protocol을 사용한다. - STOMP는 도입하지 않고 raw WebSocket JSON protocol을 사용한다.
- Redis는 현재 연결된 인프라를 사용한다. - Redis는 현재 연결된 인프라를 사용한다.
- RedisTemplate 또는 Redisson 중 기존 코드 패턴과 테스트 용이성을 기준으로 선택하되, presence TTL과 pub/sub을 모두 구현해야 한다. - 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 점검과 수정이 끝난 뒤 명시한다. - `spring.jpa.open-in-view=false`는 lazy loading 의존 API 점검과 수정이 끝난 뒤 명시한다.
- 공개 REST API 스키마는 필요한 범위 외에는 변경하지 않는다. - 공개 REST API 스키마는 필요한 범위 외에는 변경하지 않는다.
- 텍스트 메시지 저장은 기존 `UserCreatorChatMessage` 엔티티와 repository를 사용한다. - 텍스트 메시지 저장은 기존 `UserCreatorChatMessage` 엔티티와 repository를 사용한다.