Files
sodalive-backend-spring-boot/docs/20260612_크리에이터_채널_홈_API/reviews/phase-7-review.md

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을 관찰한 후 중복 행을 추가할 수 있다.

정적 재현 절차

  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
  • 관찰: pinToTheToprepository.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 — AudioContentServiceTestinOrder 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.17.5 및 기존 REV-P7-001REV-P7-003과 정적 대조했다. 사용자 지시에 따라 테스트·컴파일·ktlint은 실행하지 않았다.
  • 결과: 기존 동시성 보정이 유지되며 신규 확정 발견 사항 없음. Phase 7 후속 Task를 추가하지 않는다.
  • 남은 항목: 없음.