32 KiB
메인 홈 추천 AI 캐릭터 스냅샷 산식 변경 Plan/Task
시나리오 계약
- Happy path: KST 전날 하루 데이터를 UTC 범위로 변환해 AI 캐릭터 점수를 계산하고, 점수순 상위 20개 스냅샷을 저장한다. Real surface:
DefaultHomeRecommendationQueryRepositoryTest,RecommendationSnapshotRefreshServiceTest. - Edge:
ChatCharacter.createdAt이 null이거나 미래인 캐릭터, 비활성 캐릭터, 비활성 메시지/participant/user, 비활성 재팔로우 row는 AI 캐릭터 스냅샷 후보에서 제외된다. Real surface:DefaultHomeRecommendationQueryRepositoryTest. - Fallback: 최신
AI_CHARACTER스냅샷이 없으면 fallback refresh를 같은 application service 경로로 실행하고, 홈 API는 최대 1,500ms만 기다린다. timeout 이후에도 refresh는 가능한 경우 백그라운드에서 완료되어 다음 요청부터 저장된 스냅샷을 사용한다. Real surface:HomeRecommendationQueryServiceTest, fallback service unit test. - Adjacent regression: 메인 홈 추천 API URL과 AI 캐릭터 응답 필드는 변경하지 않는다. 최근 응원/인기 커뮤니티 스냅샷 산식과 조회 동작은 이번 변경으로 바꾸지 않는다. Real surface: 기존
HomeRecommendationControllerTest,RecommendationSnapshotRefreshServiceTest.
범위와 전제
- 기존
docs/20260529_메인_홈_추천_API/prd.md는 과거 구현 기준으로 유지한다. - 이번 문서는
docs/20260709_메인_홈_추천_AI캐릭터_스냅샷/prd.md의 후속 구현 계획이다. - 신규 공개 API, 신규 응답 필드, 운영 DDL 추가는 기본 범위에 포함하지 않는다.
- fallback refresh는 AI 캐릭터 섹션만 대상으로 한다. 스케줄러의 전체 일 스냅샷 갱신은 유지하되, AI 캐릭터 갱신 로직은 fallback과 같은 application service 경로를 사용하도록 분리한다.
- Redisson
RLock은 thread-bound이므로, 백그라운드 refresh를 사용할 때 lock 획득과 해제는 refresh를 실제 수행하는 같은 worker thread 안에서 처리한다.
기존 AI_CHARACTER 로직 유지/변경 경계
- 유지:
RecommendedSectionType.AI_CHARACTERenum 값과 code는 변경하지 않는다. - 유지:
RecommendationSnapshot저장 구조와RecommendationSnapshotPort.findLatestSnapshots(...)조회 계약은 변경하지 않는다. - 유지:
HomeRecommendationQueryService.findAiCharacterRecommendations(...)의 정상 경로는 최신 스냅샷 조회 후 상세 조립이다. fallback은 스냅샷이 없을 때만 추가된다. - 유지:
DefaultHomeRecommendationQueryRepository.findAiCharacterRecommendationDetails(...)의 상세 조립,characterId,creatorId, 원작명, 전체 채팅 수, 활성creatorMember필터는 변경하지 않는다. - 유지:
HomeRecommendationFacade,HomeRecommendationController,HomeRecommendationResponse.HomeAiCharacterItem의 공개 API URL과 응답 필드는 변경하지 않는다. - 유지: 최근 응원/인기 커뮤니티 스냅샷 산식, window, 저장 limit, random tie-breaker 정렬은 변경하지 않는다.
- 변경:
findAiCharacterSnapshots(...)의 입력 window, AI 캐릭터 점수 산식, 신규 부스트 값, 팔로우 증가 수 포함, 후보 제외 조건, top 20 동점 정렬 기준만 변경한다. - 변경:
RecommendationSnapshotRefreshService에는 AI 캐릭터 단일 refresh 경로를 추가하되, 기존 전체 일 refresh가 최근 응원/인기 커뮤니티까지 함께 갱신하는 동작은 유지한다. - 추가: 최신 AI 캐릭터 스냅샷이 없을 때만 fallback refresh orchestration을 추가한다.
실행 명령
- 문서 명령 확인:
./gradlew tasks --all - 산식/기간 단위 테스트:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicyTest - 스냅샷 refresh 서비스 테스트:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest - AI 캐릭터 query 통합 테스트:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest - 홈 추천 조회/fallback 테스트:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest - 홈 API 회귀 테스트:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest - 포맷 검증:
./gradlew ktlintCheck - 전체 회귀:
./gradlew test
Phase 1: 문서와 기준 고정
- Task 1.1: PRD 기반 구현 계획 문서 작성
- 파일 경로:
- Modify:
docs/20260709_메인_홈_추천_AI캐릭터_스냅샷/prd.md - Create:
docs/20260709_메인_홈_추천_AI캐릭터_스냅샷/plan-task.md
- Modify:
- RED: 문서 작업은 TDD 예외. TDD 예외 사유: 코드 동작 변경 전 요구사항과 구현 순서를 고정하는 작업이다.
- GREEN: PRD의 산식, KST→UTC 전날 범위, top 20, 동점 정렬, fallback timeout/lock 정책을 task로 분해한다.
- REFACTOR: 기존 홈 추천 구현 파일과 테스트 파일 기준으로 task별 수정/검증 경로를 맞춘다.
- 기대 결과: 구현 시작 전에 PRD와 plan-task가 같은 디렉터리에 준비된다.
- 파일 경로:
Phase 2: 산식과 시간 범위 정책
-
Task 2.1: AI 캐릭터 점수 계산식 계약 테스트 보강
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScoreSpec.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicy.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicyTest.kt
- Modify:
- RED: AI 캐릭터 점수가
aiChatCount * 0.45 + activeUserCount * 0.35 + followIncreaseCount * 0.20에 신규 부스트를 곱하도록 실패 테스트를 추가한다. 계산을 DB에서 하든 Kotlin에서 하든 동일한 기대값을 검증할 수 있도록 계산식 자체를 독립 단위 테스트로 고정한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest - GREEN: 기존 크리에이터 부스트
1.5/1.3/1.2는 유지하고, AI 캐릭터 전용 부스트 상수와followIncreaseCount를 받는 AI 점수 함수를 추가/수정한다. - REFACTOR: 산식 상수명은 PRD 용어가 드러나도록
AI_CHAT_WEIGHT,AI_ACTIVE_USER_WEIGHT,AI_FOLLOW_INCREASE_WEIGHT,AI_NEW_BOOST_*형태로 정리한다. - 계산식 테스트 케이스:
aiChatCount=0,activeUserCount=0,followIncreaseCount=0,newBoost=1.0이면0.0aiChatCount=10,activeUserCount=0,followIncreaseCount=0,newBoost=1.0이면4.5aiChatCount=0,activeUserCount=10,followIncreaseCount=0,newBoost=1.0이면3.5aiChatCount=0,activeUserCount=0,followIncreaseCount=10,newBoost=1.0이면2.0aiChatCount=10,activeUserCount=10,followIncreaseCount=10,newBoost=1.0이면10.0aiChatCount=10,activeUserCount=10,followIncreaseCount=10,newBoost=1.15이면11.5- 소수 오차는
0.0001이내로 검증한다.
- 기대 결과: AI 캐릭터 산식만 변경되고 최근 응원/인기 커뮤니티/최근 데뷔 산식은 변경되지 않는다.
- 파일 경로:
-
Task 2.2: AI 캐릭터 신규 부스트 경계값 계약 테스트 보강
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScoreSpec.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicy.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicyTest.kt
- Modify:
- RED: AI 캐릭터 신규 부스트가 생성일 기준 0일/10일
1.15, 11일/20일1.10, 21일/30일1.05, 31일1.0으로 계산되는 실패 테스트를 작성한다. 같은 테스트에서 기존 크리에이터 신규 부스트 값이 변경되지 않았는지도 확인한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest - GREEN: AI 캐릭터 전용 부스트 계산 함수를 PRD 값으로 변경한다.
- REFACTOR: 날짜 차이 계산 방식은 기존
ChronoUnit.DAYS.between(baseAt.toLocalDate(), 기준일.toLocalDate())와 일치하도록 유지한다. - 기대 결과: 생성일 경계값에 대한 산식 근거가 Kotlin 단위 테스트로 고정된다.
- 파일 경로:
-
Task 2.3: KST 전날 하루를 UTC 조회 범위로 변환하는 정책 추가
- 파일 경로:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationSnapshotWindowPolicy.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationSnapshotWindowPolicyTest.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
- Create:
- RED:
nowUtc = 2026-07-09T21:00:00이면 KST 기준 전날인2026-07-09 00:00:00 <= t < 2026-07-10 00:00:00이 UTC2026-07-08T15:00:00 <= t < 2026-07-09T15:00:00half-open 범위로 변환되고, 저장snapshotAt은2026-07-09T14:59:59가 되는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicyTest - GREEN:
previousKstDayUtcWindow(nowUtc)정책을 추가하고, AI 캐릭터 스냅샷 계산에는windowStartUtc,windowEndExclusiveUtc를 전달한다. - REFACTOR: 기존 최근 응원/인기 커뮤니티 7일 window 계산과 AI 캐릭터 전날 window 계산이 섞이지 않도록 함수명을 분리한다.
- 기대 결과: 운영 DB 시간이 UTC여도 AI 캐릭터 집계 기준은 KST 전날 하루로 고정된다.
- 파일 경로:
Phase 3: AI 캐릭터 스냅샷 Query 변경
-
Task 3.1: 전날 AI 답변 수/활성 사용자 수/팔로우 증가 수 집계
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
- Modify:
- RED:
findAiCharacterSnapshots(windowStartUtc, windowEndExclusiveUtc, 20)가 전날 범위 안의 AI 답변 수, 중복 없는 활성 사용자 수,ChatCharacter.creatorMember.id대상 신규 활성creator_followingrow 수를 반영하도록 실패 테스트를 작성한다. 범위 밖 메시지/팔로우, 비활성 메시지/participant/user/following은 제외되는 fixture를 포함한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest - GREEN: AI 캐릭터 snapshot native query 또는 QueryDSL 집계에
creator_following전날 신규 row 집계를 추가하고,score = ((chat * 0.45) + (activeUser * 0.35) + (followIncrease * 0.20)) * aiNewBoost로 계산한다. - REFACTOR: 기존 AI 발화 수/활성 사용자 수 테스트는 전날 window 기준으로 이름과 fixture를 갱신한다. 상세 조회 로직
findAiCharacterRecommendationDetails(...)는 변경하지 않는다. - 기대 결과: 팔로우 증가 수가 AI 캐릭터 점수에 포함되고, 재활성화 row는
created_at이 전날 신규 row가 아니면 집계되지 않는다.
- 파일 경로:
-
Task 3.2: AI 신규 부스트와 후보 제외 조건 적용
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
- Modify:
- RED:
ChatCharacter.createdAt기준 AI 전용 부스트가 적용되는 테스트,createdAtnull 또는 기준 시각보다 미래인 캐릭터가 후보에서 제외되는 테스트,creatorMember가 없거나 비활성/비CREATOR/비AI_CHARACTER인 캐릭터가 제외되는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest - GREEN: AI 스냅샷 후보 query에
ChatCharacter.createdAt is not null,createdAt < windowEndExclusiveUtc, 활성creatorMember조건을 추가하고 AI 전용 신규 부스트 값을 적용한다. - REFACTOR: 신규 부스트 계산 기준과 query의 날짜 비교 기준이
RecommendationSnapshotWindowPolicy의 UTC end-exclusive와 일치하는지 정리한다. - 기대 결과: PRD의 null/future 생성일 제외와 AI 전용 신규 부스트 경계가 DB 집계 테스트로 고정된다.
- 파일 경로:
-
Task 3.3: top 20 저장 후보와 동점 생성일 정렬 적용
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
- Modify:
- RED: AI 캐릭터 후보가 21개 이상일 때 최종 점수 기준 20개만 반환되고, 점수가 동일하면
ChatCharacter.createdAt이 더 늦은 캐릭터가 먼저 반환되는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest - GREEN: AI 스냅샷 query의 정렬을
score desc,chat_character.created_at desc기준으로 바꾸고 최종 limit 20을 적용한다. 기존RecommendationSnapshotRecord.randomTieBreaker필드는 schema 유지를 위해 값을 채우되 AI 정렬 기준으로 사용하지 않는다. - REFACTOR: 최근 응원/인기 커뮤니티의 random tie-breaker 정렬은 변경하지 않는다.
- 기대 결과: AI 캐릭터 스냅샷만 동점 생성일 최신순 정책을 사용한다.
- 파일 경로:
Phase 4: Refresh Service와 Scheduler 정리
-
Task 4.1: AI 캐릭터 단일 refresh 경로 분리
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
- Modify:
- RED:
refreshAiCharacterSnapshots(nowUtc)가 KST 전날 하루 UTC window로queryPort.findAiCharacterSnapshots(windowStartUtc, windowEndExclusiveUtc, 20)을 호출하고AI_CHARACTER스냅샷만 replace하는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest - GREEN: 전체 일 refresh 내부에서 AI 캐릭터 부분을 public 또는 internal method로 분리하고, fallback도 같은 method를 호출할 수 있게 한다.
- REFACTOR: 최근 응원/인기 커뮤니티 refresh는 기존 7일 window와 저장 limit을 유지한다.
- 기대 결과: scheduler와 fallback이 같은 AI 스냅샷 생성 로직을 재사용할 수 있다.
- 파일 경로:
-
Task 4.2: Scheduler가 AI section lock 경로를 재사용하도록 보강
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/scheduler/RecommendationSnapshotScheduler.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
- Modify:
- RED: 스케줄러 또는 refresh service 테스트에서 AI 캐릭터 refresh가 section lock key를 사용하고, lock 획득 실패 시 AI 중복 refresh를 실행하지 않는 실패 테스트를 작성한다.
- 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest - GREEN: 기존 전체 scheduler lock과 최근 응원/인기 커뮤니티 갱신 흐름은 유지하되, AI 캐릭터 refresh만 fallback과 충돌하지 않도록 공통 section lock key를 사용한다. 권장 key는
lock:recommendation-snapshot-refresh:AI_CHARACTER다. - REFACTOR: Redisson
RLock획득/해제는 같은 thread에서 수행되도록 lock 처리 위치를 정리한다. - 기대 결과: 스케줄러 실행 중 fallback이 동시에 AI 캐릭터 스냅샷을 중복 생성하지 않는다.
- 파일 경로:
Phase 5: 스냅샷 없음 fallback refresh
-
Task 5.1: AI 캐릭터 fallback refresh service 추가
- 파일 경로:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/AiCharacterSnapshotFallbackPort.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackServiceTest.kt
- Create:
- RED: 최신
AI_CHARACTER스냅샷이 없을 때 fallback service가 AI refresh를 요청하고, refresh 완료 후 저장된 스냅샷을 다시 조회하도록 실패 테스트를 작성한다. 이미 스냅샷이 있으면 refresh를 요청하지 않는 테스트도 추가한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest - GREEN: fallback service를 추가해 double-check 조회, AI 단일 refresh 호출, refresh 후 재조회 흐름을 구현한다.
- REFACTOR: fallback service는 점수 계산/상세 조립을 직접 하지 않고 snapshot 조회와 refresh orchestration만 담당한다.
- 기대 결과: 스냅샷 없음 fallback이 scheduler와 같은 저장 로직을 재사용한다.
- 파일 경로:
-
Task 5.2: fallback lock, timeout, 백그라운드 완료 정책 구현
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackServiceTest.kt
- Modify:
- RED: lock 대기 최대 300ms, 홈 API refresh 완료 대기 최대 1,500ms, timeout 시 현재 요청 빈 결과 반환, timeout 후 background refresh 계속 진행, lock이 잡혀 있는 동안 후속 요청은 중복 refresh를 시작하지 않는 테스트를 작성한다.
- 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest - GREEN: refresh 작업을 worker thread에서 실행하고 해당 thread 안에서 Redisson lock을 획득/해제한다. 홈 요청 thread는 future 완료를 최대 1,500ms만 기다리고, timeout 시 future를 cancel하지 않는다.
- REFACTOR: 실제 구현에서
TaskExecutor,CompletableFuture, 또는 기존 async 인프라 중 하나만 사용한다. 테스트에서는 deterministic executor/fake clock을 사용해 sleep 기반 테스트를 피한다. - 기대 결과: 첫 요청 timeout 후에도 refresh가 완료되면 다음 요청은 저장된 스냅샷을 사용하고, 동시 요청은 중복 refresh를 만들지 않는다.
- 파일 경로:
-
Task 5.3: 홈 AI 캐릭터 조회에 fallback 연결
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
- Modify:
- RED:
findAiCharacterRecommendations가 최신 스냅샷 없음이면 fallback service를 호출한 뒤 다시 스냅샷을 조회하고 상세를 조립하는 실패 테스트를 작성한다. fallback 후에도 스냅샷이 없으면 빈 리스트를 반환하는 테스트를 추가한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest - GREEN: AI 캐릭터 조회에서 snapshotPort 1차 조회가 비어 있을 때만 fallback service를 호출하고, 재조회 결과를 기존 상세 조립 로직에 전달한다. 스냅샷이 있는 기존 정상 경로의 상세 조회 순서와 응답 매핑은 유지한다.
- REFACTOR: 스냅샷이 이미 있는 정상 경로에서는 fallback service가 호출되지 않도록 분기와 테스트를 명확히 유지한다.
- 기대 결과: 홈 통합 조회와 AI 캐릭터 전체보기 모두 기존 응답 스키마를 유지한 채 fallback을 사용할 수 있다.
- 파일 경로:
Phase 6: API 회귀와 관측 로그
-
Task 6.1: 홈 API 응답 스키마 회귀 검증
- 파일 경로:
- Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponseTest.kt
- Test:
- RED: 홈 통합 조회와 AI 캐릭터 전체보기 응답이 기존
characterId,creatorId, 이름, 소개, 이미지, 작품명, 전체 채팅 수 필드를 그대로 내려주는 회귀 테스트를 확인/보강한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest --tests kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.HomeRecommendationResponseTest - GREEN: DTO 변경 없이 fallback이 연결된 application service 결과를 기존 facade/controller가 그대로 매핑하게 한다.
- REFACTOR: 공개 API URL과 JSON field name 변경이 없음을 테스트 assertion으로 유지한다.
- 기대 결과: 클라이언트 공개 스키마는 변경되지 않는다.
- 파일 경로:
-
Task 6.2: fallback/refresh 관측 로그 추가
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
- Modify:
- RED: fallback refresh 실행/성공/실패/timeout, lock 획득 성공/실패, AI 스냅샷 저장 수 로그 이벤트가 남는지 실패 테스트를 작성한다.
- 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest - GREEN: 기존 프로젝트 관례대로 신규 metric dependency 없이 구조화 로그를 남긴다.
- REFACTOR: 로그 event key는 검색 가능한 snake_case로 고정하고, 예외 로그는 홈 전체 실패를 유발하지 않는 fallback 실패와 실제 refresh 실패를 구분한다.
- 기대 결과: 운영에서 fallback 빈 배열 원인을 timeout/lock/실패/데이터 없음으로 구분할 수 있다.
- 파일 경로:
Phase 7: 최종 검증과 문서 갱신
-
Task 7.1: focused regression 실행
- 파일 경로:
- Modify:
docs/20260709_메인_홈_추천_AI캐릭터_스냅샷/plan-task.md
- Modify:
- RED: 구현 task 완료 후 계획 문서에 기록할 focused command 목록을 확정한다.
- 실패 확인: 해당 없음. TDD 예외 사유: 검증 기록 문서화 task다.
- GREEN: 아래 명령을 실행하고 결과를 이 문서 하단 검증 기록에 누적한다.
- REFACTOR: 실패한 명령이 있으면 원인과 재실행 결과를 같은 task 아래에 기록한다.
- 기대 결과: 산식/집계/fallback/API 회귀가 최소 명령으로 검증된다.
- 실행 명령:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicyTest./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest
- 파일 경로:
-
Task 7.2: 전체 회귀와 문서 검증
- 파일 경로:
- Modify:
docs/20260709_메인_홈_추천_AI캐릭터_스냅샷/plan-task.md
- Modify:
- RED: 구현 완료 후 전체 회귀 명령 실행 전에는 검증 기록이 비어 있어야 한다.
- 실패 확인: 해당 없음. TDD 예외 사유: 검증 기록 문서화 task다.
- GREEN:
./gradlew ktlintCheck,./gradlew test,./gradlew tasks --all,git diff --check를 실행하고 결과를 검증 기록에 남긴다. - REFACTOR: 문서와 코드의 산식/timeout 값이 다르면 구현 또는 문서를 수정한 뒤 재검증한다.
- 기대 결과: 전체 테스트, 포맷, 문서 명령 유효성, diff 공백 검사가 모두 통과한다.
- 파일 경로:
Coverage Check
- Feature A: Task 2.1, Task 3.1에서 AI 채팅 수 45%, 활성 사용자 수 35%, 팔로우 증가 수 20% 산식을 검증한다.
- Feature B: Task 2.3, Task 4.1에서 KST 전날 하루를 UTC 조회 범위로 변환하는 정책을 검증한다.
- Feature C: Task 2.2, Task 3.2에서
ChatCharacter.createdAt기준 AI 전용 신규 부스트와 null/future 제외를 검증한다. - Feature D: Task 3.3, Task 5.3, Task 6.1에서 top 20, 동점 생성일 최신순, 기존 응답 스키마 유지를 검증한다.
- Feature E: Task 5.1, Task 5.2, Task 5.3에서 fallback refresh 재사용, double-check, 300ms lock 대기, 1,500ms 홈 API 대기, timeout 후 background 완료, 중복 refresh 방지를 검증한다.
- Non-Goals: Task 6.1과 Task 7.2에서 공개 API URL/응답 필드 변경 없음, AI 캐릭터 팔로우 생성/취소 동작 변경 없음, 관리자/ML/A-B 제외를 확인한다.
전체 검증 기록
-
2026-07-09: PRD 기반으로
plan-task.md를 생성했다. 구현 전 계획 문서 작성 작업이므로 코드 테스트는 아직 실행하지 않았고, 문서 형식/명령 유효성 검증을 진행한다. -
2026-07-09: 사용자 피드백에 따라 계산을 DB에서 하든 Kotlin 단에서 하든 산식 근거를 촘촘히 남기도록 Task 2.1을 AI 캐릭터 점수 계산식 계약 테스트로 보강했다.
0, 단일 항목 가중치, 세 항목 합산, 신규 부스트 적용, 소수 오차 허용 범위를 구체 테스트 케이스로 문서화했다. -
2026-07-09: 문서 검증으로
git diff --check를 실행해 통과했다../gradlew tasks --all은 일반 샌드박스에서~/.gradlewrapper lock 파일 접근 제한으로 실패했고, 권한 상승 재실행 결과BUILD SUCCESSFUL로 통과했다. -
2026-07-10: 구현 검증으로 산식/윈도우 focused 테스트
RecommendationScorePolicyTest,RecommendationSnapshotWindowPolicyTest를 실행해BUILD SUCCESSFUL을 확인했다. -
2026-07-10: AI 캐릭터 스냅샷 집계 focused 테스트
DefaultHomeRecommendationQueryRepositoryTest를 실행해 전날 UTC window, 팔로우 증가량, 후보 제외, top 20/동점 정렬이BUILD SUCCESSFUL임을 확인했다. -
2026-07-10: refresh/fallback/query focused 테스트
RecommendationSnapshotRefreshServiceTest,RecommendationSnapshotFallbackServiceTest,HomeRecommendationQueryServiceTest를 실행해 section lock, 300ms lock wait, 1,500ms home wait, timeout 후 background 완료, double-check, fallback 연결이BUILD SUCCESSFUL임을 확인했다. -
2026-07-10: API 회귀 focused 테스트
HomeRecommendationControllerTest,HomeRecommendationResponseTest를 실행해 공개 응답 스키마 유지가BUILD SUCCESSFUL임을 확인했다. -
2026-07-10: 전체 검증으로
./gradlew ktlintCheck,./gradlew test,./gradlew tasks --all,git diff --check를 실행했다../gradlew test는 120초 제한에서 1회 timeout되어 600초 제한으로 재실행했고, 모든 명령이 최종BUILD SUCCESSFUL또는 무출력 통과했다. -
2026-07-10: 리뷰 게이트에서 발견된 fallback 조건/단일 실행/section lock 해제 시점 이슈를 수정했다. 최신 스냅샷 존재 여부는 요청 page가 아니라
offset=0, limit=1존재 확인으로 분리했고, fallback refresh는 단일 in-flight future로 제한했으며, scheduler AI section lock은 트랜잭션 완료 후 해제되도록 보강했다. -
2026-07-10: 수정 후
HomeRecommendationQueryServiceTest,RecommendationSnapshotFallbackServiceTest,RecommendationSnapshotRefreshServiceTestfocused 테스트를 실행해BUILD SUCCESSFUL을 확인했다. 이어서 domain/repository/API focused 테스트,./gradlew ktlintCheck,git diff --check,./gradlew test,./gradlew tasks --all을 재실행했고 모두 최종 통과했다. -
2026-07-10: 후속 리뷰 응답으로 AI 캐릭터 팔로우 증가량 집계에서 비활성 follower 계정을 제외하도록
creator_following.member_id의member.is_active = true조건을 추가했다.DefaultHomeRecommendationQueryRepositoryTest에 활성 follow row이지만 follower 계정이 비활성인 fixture를 추가했고,./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest,./gradlew ktlintCheck,git diff --check가 모두 통과했다. -
2026-07-10: 후속 리뷰 응답으로
HomeRecommendationQueryService.findAiCharacterRecommendations의Propagation.NOT_SUPPORTED를 제거해 홈 API 트랜잭션 경계를 유지하도록 되돌렸다. 또한 AI 캐릭터 스냅샷 window를endExclusiveUtc기반 half-open 범위로 정리해cm.created_at < :windowEndExclusive,cf.created_at < :windowEndExclusive,cc.created_at < :windowEndExclusive로 집계되도록 수정했다. -
2026-07-10: half-open 경계 회귀 테스트
shouldFindAiCharacterSnapshotsWithExclusiveWindowEnd를 추가했다. 첫 focused 재검증은executeSnapshotQuery가 AI SQL에 없는:snapshotAt파라미터를 항상 바인딩해 실패했고, SQL에 존재하는 경우에만snapshotAt을 바인딩하도록 수정했다. 이후./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest --tests 'kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest.shouldReturnTwentyAiCharactersOnHomeRecommendations'를 재실행해BUILD SUCCESSFUL을 확인했다. -
2026-07-10: 전체 회귀 재검증 중
HomeRecommendationControllerTest.shouldExposeCreatorIdOnAiCharacterRecommendations가 전체 실행에서 최신 AI 스냅샷 충돌로 1회 실패했다. 테스트 fixture의 AI 스냅샷 시각을 fallback window보다 최신인2026-12-31T23:59:59로 고정해 테스트 독립성을 보강했고,./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest.shouldExposeCreatorIdOnAiCharacterRecommendations' --tests 'kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest.shouldReturnTwentyAiCharactersOnHomeRecommendations'가BUILD SUCCESSFUL임을 확인했다. -
2026-07-10: 최종 검증으로
./gradlew test,./gradlew ktlintCheck,git diff --check를 재실행했다. 전체 테스트 1125건 포함 모든 명령이 최종 통과했다. -
2026-07-10: 리뷰 게이트에서 PRD inclusive window 문구와 fallback post-refresh 재조회 트랜잭션 가시성 이슈가 차단으로 지적됐다. PRD/plan-task의 기간 표현을
windowStartUtc <= t < windowEndExclusiveUtchalf-open 기준으로 정렬했고, active 공통RecommendationSnapshotFallbackService의 snapshot read를PROPAGATION_REQUIRES_NEWread-onlyTransactionTemplate으로 분리해 caller read transaction과 독립된 DB read가 되도록 보강했다. -
2026-07-10: 후속 정리로 삭제된
AiCharacterSnapshotFallbackServiceTest참조를 activeRecommendationSnapshotFallbackServiceTest로 갱신했고, 비활성 legacyAiCharacterSnapshotFallbackService구현을 제거한 뒤AiCharacterSnapshotFallbackPort만 별도 파일로 유지했다.