feat(home): 크리에이터 채널 홈 정책을 보강한다

This commit is contained in:
2026-07-31 01:46:39 +09:00
parent f1c2e6c50b
commit 13e0b42375
20 changed files with 1811 additions and 51 deletions

View File

@@ -0,0 +1,172 @@
# Phase 7 코드 리뷰 보고서
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 7 / Task 7.1~7.2 / P7-GATE |
| 기준 commit 또는 working tree | `f1c2e6c5` + 2026-07-30 working tree |
| 리뷰 일자 | 2026-07-30 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `plan-task.md`, `docs/agent-guides/*.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 목적과 범위
- 활성 고정 9개 상한, 10번째 고정의 최고령 교체, 비활성 재활성화, 홈 오디오 고정 우선 정렬, 최신 오디오 제외을 코드·테스트·문서와 정적 대조했다.
- 사용자 지시에 따라 컴파일·테스트는 실행하지 않았고, Phase 7의 기존 Gate 성공 기록은 참고 증거로만 사용했다.
## 3. 검토 근거
- `AudioContentService.kt:1230-1261`은 transaction 안에서 현재 고정→활성 목록 순으로 읽고 추가·재활성화하지만 크리에이터 단위 lock을 취하지 않는다.
- `PinContent.kt:11-19`에는 `(member_id, content_id)` unique constraint가 없고, `PinContentRepository.kt:15-43`의 조회에도 pessimistic lock이 없다.
- `MemberRepository.kt:35-37`에는 이미 회원 행을 잠그는 `findByIdForUpdate` 패턴이 있다.
- `AudioContentServiceTest.kt:454-530`은 8/9개, 10번째 교체, 비활성 재활성화를 순차 mock 요청으로 검증하며 동시 요청은 다루지 않는다.
## 4. 발견 사항
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P7-001` | Medium | 완료 | 동시 고정 요청이 활성 9개 상한을 넘거나 중복 행을 만들 수 있다 | Task 7.3 | `P7-R1` |
### REV-P7-001 — 동시 고정 요청이 활성 9개 상한을 넘거나 중복 행을 만들 수 있다
- **심각도:** Medium
- **상태:** 확정
- **관련 요구사항:** PRD Feature D/H, `DEC-001`, Task 7.1
- **소유 Task:** Task 7.3 / `P7-R1`
**관찰 내용**
활성 고정이 8개일 때 같은 크리에이터의 두 transaction이 동시에 목록을 읽으면 둘 다 `size < 9`로 판단해 서로 다른 행을 추가할 수 있다. 같은 콘텐츠에 대한 두 요청도 둘 다 `findByContentIdAndMemberId == null`을 관찰한 후 중복 행을 추가할 수 있다.
**정적 재현 절차**
1. 활성 고정 8개를 준비한다.
2. transaction A/B가 각각 서로 다른 콘텐츠에 대해 기존 고정이 없음과 활성 목록 8개를 읽는다.
3. A/B가 각각 새 `PinContent`를 저장하면 최종 활성 수는 10개가 된다.
4. 현재 코드·lock·constraint 중 이 interleaving을 차단하는 장치가 없다.
**영향**
동시 요청이라는 제한 조건에서 크리에이터별 활성 고정 9개 계약이 깨지고, 홈 정렬 join에 중복 행이 생기면 응답 개수·순서도 오염될 수 있다.
**권장 조치**
고정 상태를 읽기 전 크리에이터 행을 pessimistic write lock으로 직렬화하고, 새 DDL 없이 동시성 통합 테스트로 9개 상한과 콘텐츠 유일성을 고정한다. 홈 repository 테스트에는 9개 초과 fixture에서 반환 개수가 9임을 명시적으로 추가한다.
**판정 기록**
- 2026-07-30 — transaction 내 읽기-판단-쓰기 순서, lock 부재, unique constraint 부재를 정적 대조해 확정.
- 2026-07-30 — `AudioContentService.pinToTheTop`에 creator member row lock을 추가하고 `AudioContentPinConcurrencyTest`/`AudioContentServiceTest` 통과로 완료. Reviewer gate에서 요구한 홈 목록 9개 상한 assertion은 `DefaultCreatorChannelHomeQueryRepositoryTest`에 추가해 통과 확인.
## 5. plan·goal 전환
- `plan-task.md` Phase 7에 Task 7.3 / `P7-R1`을 추가했다.
- 기존 Task 7.1~7.2과 P7-GATE의 완료 이력은 유지하고 리뷰 후속 goal만 추가했다.
## 6. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | 고정 service·entity·repository·테스트 정적 대조 |
| 후보 항목 판정 완료 | 충족 | `REV-P7-001` 확정 |
| 확정 항목 plan 반영 | 충족 | Task 7.3 / `P7-R1` |
| 검증 명령과 결과 기록 | 충족 | 정적 검토만 실행, 테스트 미실행 사유 기록 |
**최종 결론:** 수정 goal 완료.
**남은 항목:** 없음.
## 7. 2차 리뷰 — 2026-07-30
### 7.1 리뷰 범위와 방법
- Task 7.3의 member row lock 호출 순서와 동시성 테스트가 MySQL 운영 격리수준에서도 직렬화를 보장하는지 정적 검토했다.
- MySQL 공식 InnoDB `REPEATABLE READ`의 consistent read/locking read 의미를 근거로 대조했다.
- 사용자 지시에 따라 테스트·컴파일·ktlint은 실행하지 않았다.
### 7.2 발견 사항
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P7-002` | Medium | 완료 | 크리에이터 lock 전에 일반 조회가 snapshot을 만들 수 있다 | Task 7.4 | `P7-R2` |
#### REV-P7-002 — 크리에이터 lock 전에 일반 조회가 snapshot을 만들 수 있다
- **관련 요구사항:** PRD Feature D/H, Task 7.3
- **관찰:** `pinToTheTop``repository.findByIdAndCreatorId` 일반 조회 후 `memberRepository.findByIdForUpdate`를 호출하고, 그 뒤 `PinContent`를 일반 조회한다. MySQL InnoDB 기본 `REPEATABLE READ`에서는 첫 consistent read가 snapshot을 정하고 locking read는 최신 행을 읽으므로 두 방식을 섞으면 후속 일반 조회가 lock 대기 전 snapshot을 계속 사용할 수 있다. [MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html)
- **영향:** 두 번째 transaction이 member lock을 기다린 뒤에도 첫 transaction의 최신 고정 변경을 보지 못해 9개 상한이나 콘텐츠 중복 방지 판단이 stale 상태를 기준으로 수행될 수 있다.
- **검증 공백:** 현재 단위 테스트는 member lock이 `PinContent` 조회보다 빠른지만 확인하고 콘텐츠 일반 조회는 순서 검증에 포함하지 않는다. 동시성 테스트의 시작 latch도 두 transaction의 고정 목록 읽기 시점을 강제하지 않아 이 interleaving을 보장하지 않는다.
- **권장 조치:** transaction의 첫 DB 접근에서 member row lock을 잡고 모든 일반 조회를 그 뒤로 옮기며, 호출 순서를 단위 테스트로 고정한다.
- **완료 기록:** 2026-07-30 — `AudioContentServiceTest``inOrder` RED/GREEN으로 lock이 콘텐츠/고정 조회보다 먼저 호출됨을 고정했다.
### 7.3 plan·goal 전환과 종료 판정
- `plan-task.md` Phase 7에 Task 7.4 / `P7-R2`를 추가했다.
- **최종 결론:** Medium 1건 수정 완료.
- **남은 항목:** 없음.
## 8. 3차 리뷰 — 2026-07-31
### 8.1 리뷰 범위와 방법
- Task 7.3~7.4 후속 구현이 같은 크리에이터의 모든 상단 고정 변경을 실제로 직렬화하는지 `pinToTheTop`, `unpinAtTheTop`, member lock, `PinContent` 행 재사용 흐름을 정적 검토했다.
- 기존 단위·동시성 테스트가 고정과 해제의 경쟁을 포함하는지도 함께 확인했다.
- 사용자 지시에 따라 테스트·컴파일·ktlint은 실행하지 않았다.
### 8.2 발견 사항
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P7-003` | Medium | 수정 완료 | 고정 해제가 creator lock을 공유하지 않아 새 고정을 소실할 수 있다 | Task 7.5 | `P7-R3` |
#### REV-P7-003 — 고정 해제가 creator lock을 공유하지 않아 새 고정을 소실할 수 있다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD Feature H, Task 7.1·7.3·7.4
- **소유 Task:** Task 7.5 / `P7-R3`
**관찰 내용**
`pinToTheTop`은 첫 DB 접근에서 `MemberRepository.findByIdForUpdate`로 크리에이터를 잠근 뒤, 활성 고정이 9개이면 가장 오래된 `PinContent` 행의 `content`를 새 콘텐츠로 바꿔 재사용한다. 반면 `unpinAtTheTop`은 같은 member lock 없이 이전 콘텐츠로 `PinContent`를 조회하고 `isActive = false`로 변경한다.
**정적 재현 절차**
1. 활성 고정 9개에서 가장 오래된 고정 콘텐츠를 A, 새 고정 콘텐츠를 B로 둔다.
2. 해제 transaction이 A의 `PinContent`를 먼저 읽은 뒤 commit 전 대기한다.
3. 고정 transaction이 member lock을 얻고 같은 행을 B의 활성 고정으로 재사용해 commit한다.
4. 해제 transaction이 늦게 commit하면 이전 A를 가리키던 stale entity update가 재사용된 행을 다시 비활성화하거나 이전 상태로 덮을 수 있다.
**근거**
- 코드: `AudioContentService.pinToTheTop`은 member lock과 최고령 `PinContent` 행 재사용을 수행한다.
- 코드: `AudioContentService.unpinAtTheTop`은 member lock 없이 `PinContent`를 조회·비활성화한다.
- 테스트: `AudioContentPinConcurrencyTest`는 고정 요청 2개의 경쟁만 검증하고 고정/해제 경쟁은 다루지 않는다.
**영향**
겹친 두 요청의 순서에 따라 성공한 새 고정 B가 홈 `audioContents`에서 사라지거나 재사용 행 상태가 요청 완료 순서와 다르게 남을 수 있다. 활성 9개 상한 자체를 초과하지는 않지만 상단 고정 상태의 일관성이 깨진다.
**권장 조치**
`unpinAtTheTop`도 첫 DB 접근에서 `pinToTheTop`과 같은 member row lock을 획득한 뒤 해제 대상을 조회한다. 최소 회귀 테스트로 member lock이 `PinContent` 조회보다 먼저 호출되는 순서를 고정하고 기존 동시 고정·10번째 교체 테스트를 함께 유지한다.
**판정 기록**
- 2026-07-31 — 고정 행 재사용, 해제의 lock 부재, 현재 동시성 테스트 범위를 정적 대조해 확정했다.
- 2026-07-31 — `unpinAtTheTop`의 creator lock 선행 획득을 RED/GREEN으로 보정하고, Phase 7 직접 영향 단위 테스트와 `ktlintCheck`, `git diff --check` 통과를 확인했다.
### 8.3 plan·goal 전환과 종료 판정
- `plan-task.md` Phase 7에 Task 7.5 / `P7-R3`을 추가했다.
- **최종 결론:** Medium 1건 수정 완료.
- **남은 항목:** 없음.
## 9. 4차 리뷰 — 2026-07-31
- **대상:** 활성 고정 9개 상한, 10번째·비활성 재고정, 홈 고정 우선 정렬, 최신 오디오 제외와 고정/해제 creator lock 직렬화.
- **방법:** `AudioContentService`, member pessimistic lock, `PinContent` 조회 순서, 홈 repository 정렬과 단위·동시성·repository 테스트를 Task 7.1~7.5 및 기존 `REV-P7-001`~`REV-P7-003`과 정적 대조했다. 사용자 지시에 따라 테스트·컴파일·ktlint은 실행하지 않았다.
- **결과:** 기존 동시성 보정이 유지되며 신규 확정 발견 사항 없음. Phase 7 후속 Task를 추가하지 않는다.
- **남은 항목:** 없음.