12 KiB
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로 바꾼다.
정적 재현 절차
- publish 트랜잭션 A가 active follower인 회원을 읽고 insert 전에 멈춘다.
- 트랜잭션 B가 같은 회원의 언팔로우와 현재 inbox 비활성화를 완료한다.
- A가 기존 follower snapshot으로 active inbox를 뒤늦게 insert한다.
- 언팔로우 중에는 조회의 active-following 조건으로 숨겨지지만, 회원이 다시 팔로우하면 해당 언팔로우 이전 이벤트가 노출 가능해진다.
영향
재팔로우 시 기존 비활성 row를 복구하지 않고 재팔로우 이후의 새 이벤트만 제공한다는 정책을 우회해, 언팔로우 이전 이벤트가 최근 소식으로 나타날 수 있다.
권장 조치
follower 판정부터 inbox insert까지를 하나의 트랜잭션 경계로 묶고, 해당 creator_following row를 팔로우·언팔로우 상태 변경과 같은 순서로 잠근다. publish가 먼저 끝나면 뒤이은 언팔로우가 새 row까지 비활성화하고, 언팔로우가 먼저 끝나면 publish가 row를 생성하지 않는 두 순서를 동시성 테스트로 고정한다.
판정 기록
- 2026-07-30 — publish/follow/unfollow 호출 흐름과 트랜잭션 경계를 대조해 확정했다. 런타임 테스트는 실행하지 않았다.
plan·goal 전환
plan-task.mdPhase 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_followingrow를 잠그도록 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(...)는 nullablerequest.isActive를 기본값과 함께 service에 전달한다.CreatorFollowRequest는isActive를 공개 request 필드로 정의한다.MemberService.creatorFollow(...)의 신규 관계 분기는 기본값이isActive=true인CreatorFollowing()을 저장하고, 기존 관계 분기는isNotify와isActive만 변경한다.MemberService.creatorUnFollow(...)만homeFollowingNewsInboxPort.deactivateByMemberIdAndCreatorId(...)를 호출한다.MemberServiceTest는 전용creatorUnFollow(...)경로만 검증하며creatorFollow(..., isActive=false)경로는 검증하지 않는다.
정적 재현 절차
- 관계가 없는 회원이
POST /member/creator/follow에isActive=false를 보내면 기본 active 관계가 새로 생성된다. - 별도로 active 팔로우와 active inbox row가 있는 회원이 같은 요청을 보내면 관계 row만 inactive가 되고 inbox는 active로 남는다.
- 같은 API에
isActive=true를 보내 재팔로우한다. - 기존 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.mdPhase 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 완료 판정을 유지한다.