11 KiB
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을 관찰한 후 중복 행을 추가할 수 있다.
정적 재현 절차
- 활성 고정 8개를 준비한다.
- transaction A/B가 각각 서로 다른 콘텐츠에 대해 기존 고정이 없음과 활성 목록 8개를 읽는다.
- A/B가 각각 새
PinContent를 저장하면 최종 활성 수는 10개가 된다. - 현재 코드·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.mdPhase 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 - 영향: 두 번째 transaction이 member lock을 기다린 뒤에도 첫 transaction의 최신 고정 변경을 보지 못해 9개 상한이나 콘텐츠 중복 방지 판단이 stale 상태를 기준으로 수행될 수 있다.
- 검증 공백: 현재 단위 테스트는 member lock이
PinContent조회보다 빠른지만 확인하고 콘텐츠 일반 조회는 순서 검증에 포함하지 않는다. 동시성 테스트의 시작 latch도 두 transaction의 고정 목록 읽기 시점을 강제하지 않아 이 interleaving을 보장하지 않는다. - 권장 조치: transaction의 첫 DB 접근에서 member row lock을 잡고 모든 일반 조회를 그 뒤로 옮기며, 호출 순서를 단위 테스트로 고정한다.
- 완료 기록: 2026-07-30 —
AudioContentServiceTest의inOrderRED/GREEN으로 lock이 콘텐츠/고정 조회보다 먼저 호출됨을 고정했다.
7.3 plan·goal 전환과 종료 판정
plan-task.mdPhase 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로 변경한다.
정적 재현 절차
- 활성 고정 9개에서 가장 오래된 고정 콘텐츠를 A, 새 고정 콘텐츠를 B로 둔다.
- 해제 transaction이 A의
PinContent를 먼저 읽은 뒤 commit 전 대기한다. - 고정 transaction이 member lock을 얻고 같은 행을 B의 활성 고정으로 재사용해 commit한다.
- 해제 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.mdPhase 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.17.5 및 기존REV-P7-001REV-P7-003과 정적 대조했다. 사용자 지시에 따라 테스트·컴파일·ktlint은 실행하지 않았다. - 결과: 기존 동시성 보정이 유지되며 신규 확정 발견 사항 없음. Phase 7 후속 Task를 추가하지 않는다.
- 남은 항목: 없음.