# Phase 4 코드 리뷰 보고서 ## 1. 리뷰 정보 | 항목 | 내용 | |---|---| | 리뷰 대상 | Phase 4 / Task 4.1~4.5 | | 기준 commit 또는 working tree | `e6f56f24` + 2026-07-30 working tree | | 리뷰 일자 | 2026-07-30 | | 리뷰어 | Codex | | 기준 문서 | `prd.md` Feature F, `plan-task.md` Phase 4 | | 리뷰 상태 | 판정 완료 | ## 2. 목적과 범위 - source key, follower fan-out, 언팔로우 비활성화, 랭킹/오디오/무료 커뮤니티 발행 연결을 정적으로 추적했다. - 원 트랜잭션 commit 이후 발행, 발행 실패 격리, 공개 시각과 중복 방지 키를 확인했다. - 사용자의 지시에 따라 컴파일과 테스트는 실행하지 않았다. ## 3. 검토 근거 - source key는 타입과 원천 id/기간으로 안정적으로 생성된다. - publish service는 활성 follower에게만 record를 만들고 DB 컬럼 길이에 맞춰 title/body를 제한한다. - 언팔로우는 기존 inbox를 비활성화하며 재팔로우가 이를 복구하지 않는다. - 랭킹, 즉시/예약 오디오, 무료 커뮤니티 생성 경로는 commit 이후 publish service를 호출하고 실패를 원 처리와 격리한다. - 관련 단위·서비스 테스트는 발행 성공, 예약 공개, 유료 미발행, 발행 실패 격리를 포함한다. ## 4. 발견 사항 확정 발견 사항 없음. ## 5. plan·goal 전환 전환 항목 없음. ## 6. 리뷰 종료 판정 | 판정 항목 | 결과 | 근거 | |---|---|---| | 리뷰 범위 전체 확인 | 충족 | 발행 경로와 호출부 정적 추적 | | 후보 항목 판정 완료 | 충족 | 후보 없음 | | 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 | | 검증 명령과 결과 기록 | 충족 | 테스트 미실행 사유 기록 | **최종 결론:** 확정 발견 사항 없음. **남은 항목:** 없음. ## 7. 2차 리뷰 — 2026-07-30 ### 리뷰 범위 - active follower 조회, inbox insert의 트랜잭션 경계와 언팔로우·재팔로우 상태 변경을 함께 정적으로 추적했다. - 컴파일과 테스트는 사용자 지시에 따라 실행하지 않았다. ### REV-P4-001 — follower 판정 후 언팔로우가 완료되면 stale active inbox가 생성될 수 있음 **심각도:** Medium **상태:** 확정 **근거** - `HomeFollowingNewsPublishService.publishToFollowers(...)`는 active follower id 목록을 먼저 읽고 이후 별도 호출로 inbox row를 insert한다. - `HomeFollowingNewsInboxJpaRepository.findActiveFollowerIds(...)`는 `creator_following`을 잠그지 않는 조회다. - `HomeFollowingNewsInboxPersistenceAdapter.insertIgnoreAll(...)`의 실제 insert는 `REQUIRES_NEW` 경로에서 실행될 수 있어 follower 조회와 하나의 직렬화 경계를 공유하지 않는다. - `MemberService.creatorUnFollow(...)`는 언팔로우 시점에 존재하는 active inbox만 비활성화하고, `creatorFollow(...)`의 재팔로우는 기존 관계 row를 다시 active로 바꾼다. **정적 재현 절차** 1. publish 트랜잭션 A가 active follower인 회원을 읽고 insert 전에 멈춘다. 2. 트랜잭션 B가 같은 회원의 언팔로우와 현재 inbox 비활성화를 완료한다. 3. A가 기존 follower snapshot으로 active inbox를 뒤늦게 insert한다. 4. 언팔로우 중에는 조회의 active-following 조건으로 숨겨지지만, 회원이 다시 팔로우하면 해당 언팔로우 이전 이벤트가 노출 가능해진다. **영향** 재팔로우 시 기존 비활성 row를 복구하지 않고 재팔로우 이후의 새 이벤트만 제공한다는 정책을 우회해, 언팔로우 이전 이벤트가 최근 소식으로 나타날 수 있다. **권장 조치** follower 판정부터 inbox insert까지를 하나의 트랜잭션 경계로 묶고, 해당 `creator_following` row를 팔로우·언팔로우 상태 변경과 같은 순서로 잠근다. publish가 먼저 끝나면 뒤이은 언팔로우가 새 row까지 비활성화하고, 언팔로우가 먼저 끝나면 publish가 row를 생성하지 않는 두 순서를 동시성 테스트로 고정한다. **판정 기록** - 2026-07-30 — publish/follow/unfollow 호출 흐름과 트랜잭션 경계를 대조해 확정했다. 런타임 테스트는 실행하지 않았다. ### plan·goal 전환 - `plan-task.md` Phase 4에 Task 4.6, `P4-R1`, `P4-R-GATE`를 추가했다. - 기존 Task 4.1~4.5의 완료 체크와 검증 기록은 유지했다. ### 2차 리뷰 종료 판정 | 판정 항목 | 결과 | 근거 | |---|---|---| | 리뷰 범위 전체 확인 | 충족 | publish/follow/unfollow 트랜잭션 경계 정적 추적 | | 후보 항목 판정 완료 | 충족 | `REV-P4-001` 확정 | | 확정 항목 plan 반영 | 충족 | Task 4.6 / `P4-R1` | | 검증 명령과 결과 기록 | 충족 | 테스트 미실행 사유 기록 | **최종 결론:** 수정 goal 필요. **남은 항목:** 없음. `P4-R1`과 `P4-R-GATE` 수정 후 검증은 아래 기록에 누적했다. ## 8. 수정 후 검증 — 2026-07-30 - RED 확인: `HomeFollowingNewsInboxPersistenceAdapterTest.shouldNotExposeNewsCreatedBeforeUnfollowAfterRefollowWhenPublishIsDelayed` 실행 결과 stale active inbox assertion 실패로 `BUILD FAILED`. - follower 조회와 inbox insert 경계에서 `creator_following` row를 잠그도록 active follower 조회에 `for update`를 적용했고, `creatorFollow(...)`/`creatorUnFollow(...)`도 같은 row를 `PESSIMISTIC_WRITE`로 조회하도록 변경했다. - GREEN 확인: 동시성 단일 테스트 재실행 결과 `BUILD SUCCESSFUL`. - P4-R1 focused test `HomeFollowingNewsInboxPersistenceAdapterTest`, `HomeFollowingNewsPublishServiceTest`, `MemberServiceTest` 실행 결과 `BUILD SUCCESSFUL`. - P4-R-GATE E2E `HomeFollowingEndToEndTest` 실행 결과 `BUILD SUCCESSFUL`. - P4-R-GATE lint `ktlintCheck` 최초 실행은 테스트 import 정렬 위반으로 `BUILD FAILED`; import 정렬 수정 후 재실행한다. - P4-R-GATE lint `ktlintCheck` 재실행 결과 `BUILD SUCCESSFUL`. - 리뷰 게이트에서 Critical/Important/Minor blocker 없음으로 승인됐다. **수정 후 결론:** `REV-P4-001` 보완 완료. ## 9. 3차 정적 리뷰 — 2026-07-30 ### 리뷰 범위 - 공개 팔로우·언팔로우 controller 경로, `MemberService` 상태 전이, inbox 비활성화와 재팔로우 조회 결과를 함께 정적으로 추적했다. - 컴파일과 테스트는 사용자 지시에 따라 실행하지 않았다. ### REV-P4-002 — 통합 팔로우 API의 `isActive=false` 경로가 기존 inbox를 비활성화하지 않음 **심각도:** High **상태:** 확정 **관련 요구사항:** PRD Feature F의 언팔로우 시 기존 inbox 비활성화, 재팔로우 시 기존 inbox 미복구 **소유 Task:** Task 4.7 / `P4-R2` **관찰 내용** `POST /member/creator/follow`는 request의 `isActive=false`를 `MemberService.creatorFollow(...)`에 전달해 팔로우·언팔로우를 함께 처리한다. 관계 row가 없으면 전달된 `isActive`를 반영하지 않은 기본 active 관계를 만들고, 기존 관계 row가 있으면 `creatorFollowing.isActive=false`만 반영한 채 inbox 비활성화를 호출하지 않는다. 이후 같은 API로 다시 활성화하면 기존 inbox row가 계속 active이므로 active-following 조회 조건을 다시 만족해 언팔로우 이전 소식이 노출될 수 있다. **근거** - `MemberController.creatorFollow(...)`는 nullable `request.isActive`를 기본값과 함께 service에 전달한다. - `CreatorFollowRequest`는 `isActive`를 공개 request 필드로 정의한다. - `MemberService.creatorFollow(...)`의 신규 관계 분기는 기본값이 `isActive=true`인 `CreatorFollowing()`을 저장하고, 기존 관계 분기는 `isNotify`와 `isActive`만 변경한다. - `MemberService.creatorUnFollow(...)`만 `homeFollowingNewsInboxPort.deactivateByMemberIdAndCreatorId(...)`를 호출한다. - `MemberServiceTest`는 전용 `creatorUnFollow(...)` 경로만 검증하며 `creatorFollow(..., isActive=false)` 경로는 검증하지 않는다. **정적 재현 절차** 1. 관계가 없는 회원이 `POST /member/creator/follow`에 `isActive=false`를 보내면 기본 active 관계가 새로 생성된다. 2. 별도로 active 팔로우와 active inbox row가 있는 회원이 같은 요청을 보내면 관계 row만 inactive가 되고 inbox는 active로 남는다. 3. 같은 API에 `isActive=true`를 보내 재팔로우한다. 4. 기존 active inbox가 다시 active-following 조건을 만족해 최근 소식 조회 후보가 된다. **영향** 관계가 없는 회원은 언팔로우 요청으로 오히려 active follower가 될 수 있다. 기존 follower는 언팔로우 경로에 따라 과거 소식 보존 상태가 달라지고, 통합 언팔로우 경로에서 재팔로우 이후 과거 소식 미복구 계약을 위반한다. **권장 조치** 관계가 없고 `isActive=false`면 전용 언팔로우 경로와 동일하게 새 active 관계를 만들지 않는다. 기존 관계를 `isActive=false`로 반영하는 같은 트랜잭션에서는 inbox를 비활성화한다. `isActive=true`인 알림 변경·재팔로우는 기존 비활성 inbox를 복구하지 않도록 유지하고, 두 경우를 `MemberServiceTest`로 고정한다. **판정 기록** - 2026-07-30 — request/controller/service의 공개 `isActive=false` 흐름과 PRD 재팔로우 정책을 정적으로 대조해 확정했다. 런타임 테스트는 실행하지 않았다. ### plan·goal 전환 - `plan-task.md` Phase 4에 Task 4.7, `P4-R2`, `P4-R2-GATE`를 추가했다. - 기존 Task 4.1~4.6의 완료 체크와 검증 기록은 유지했다. ### 3차 리뷰 종료 판정 | 판정 항목 | 결과 | 근거 | |---|---|---| | 리뷰 범위 전체 확인 | 충족 | 두 공개 상태 변경 경로와 조회 조건 정적 추적 | | 후보 항목 판정 완료 | 충족 | `REV-P4-002` 확정 | | 확정 항목 plan 반영 | 충족 | Task 4.7 / `P4-R2` | | 검증 명령과 결과 기록 | 충족 | 테스트 미실행 사유와 정적 근거 기록 | **최종 결론:** 수정 goal 필요. **남은 항목:** 없음. `P4-R2`와 `P4-R2-GATE` 수정 후 검증은 아래 기록에 누적했다. ## 10. 3차 수정 후 검증 — 2026-07-30 - RED 확인: `./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.member.MemberServiceTest"` 실행 결과 `creatorFollow(..., isActive=false)` 신규 회귀 2건이 assertion 실패로 `BUILD FAILED`. - 관계가 없고 `isActive=false`인 통합 팔로우 요청은 새 active 관계를 만들지 않고, 기존 관계를 inactive로 바꾸는 경로에서는 `homeFollowingNewsInboxPort.deactivateByMemberIdAndCreatorId(...)`를 호출하도록 `MemberService.creatorFollow(...)`만 최소 수정했다. - GREEN 확인: 같은 `MemberServiceTest` 명령 재실행 결과 `BUILD SUCCESSFUL`. - P4-R2-GATE 확인: `./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest" --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest"` 실행 결과 `BUILD SUCCESSFUL`. - P4-R2-GATE lint `./gradlew --no-daemon ktlintCheck` 최초 실행은 신규 테스트 줄 길이 위반으로 `BUILD FAILED`; 포맷 정리 후 `MemberServiceTest`와 `ktlintCheck` 재실행 결과 모두 `BUILD SUCCESSFUL`. - 추가 전체 회귀 확인: `./gradlew --no-daemon test` 실행 결과 `BUILD SUCCESSFUL`. - 리뷰 게이트에서 Critical/Important/Minor blocker 없음으로 승인됐다. **수정 후 결론:** `REV-P4-002` 보완 완료. ## 11. 4차 정적 리뷰 — 2026-07-30 - source key, after-commit 발행, follower fan-out, 랭킹·오디오·무료 커뮤니티 호출부와 발행 실패 격리를 다시 추적했다. - `P4-R1`의 `creator_following` 잠금 순서와 `P4-R2`의 통합 `isActive=false` 경로가 전용 언팔로우와 같은 inbox 최종 상태를 만드는지 확인했다. - persistence·publish·`MemberService` 테스트가 중복 방지, 동시 publish/unfollow, 신규 inactive 요청과 재팔로우 미복구를 고정하는지 정적으로 확인했다. - 컴파일과 테스트는 사용자 지시에 따라 실행하지 않았다. - 신규 확정 발견 사항 없음. `REV-P4-001`, `REV-P4-002` 수정 완료와 기존 Phase 4 완료 판정을 유지한다.