feat(home): 팔로잉 탭 최근 소식 정책을 보강한다
This commit is contained in:
203
docs/20260625_메인_홈_팔로잉_탭_API/reviews/phase-4-review.md
Normal file
203
docs/20260625_메인_홈_팔로잉_탭_API/reviews/phase-4-review.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# 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 완료 판정을 유지한다.
|
||||
Reference in New Issue
Block a user