Files

10 KiB

Phase 1 리뷰 보고서

1. 리뷰 정보

항목 내용
대상 Phase 1 P1-T1, P1-T2, P1-R1~R4, P1-GATE
기준 HEAD 50409e41c0c469529572f5d77033c3cb23d67d22 + 현재 작업 트리
작업 트리 지문 3bbb1d90869484073c3af681118a13f5f03c3ccecbe84de16f1568338acd66b7
일자 / 리뷰어 2026-09-10 / Codex 및 목표·품질·보안·QA 증거 리뷰어
기준 문서 PRD, 구현 계획, docs/sample/sample-review.md
상태 P1-R4 수정·자동 회귀·후속 리뷰 완료 · 실제 MySQL/Papago/HTTP 수동 검증 대기

2. 목적과 범위

§2~7의 정적 근거와 지문은 최초 리뷰 당시 기록이다. 수정 후 코드를 같은 지문으로 검증했다고 보지 않는다. 현재 판정은 §8과 2026-09-10 후속 재판정에 기록하며, 아래 최초 리뷰의 실행 제약은 그대로 보존한다.

원문 언어 감지, 번역 저장·재사용, 본문 개정 정합성, 동시 요청의 목표 언어 보존을 기준 문서와 대조한다. 코드·테스트 소스·기존 검증 기록만 확인한다. 사용자 지시에 따라 테스트/컴파일/서버/HTTP/DB 실행은 하지 않는다. 현재 테스트 통과는 사용자 보고 및 기존 기록으로 구분하며 신규 실행 성공으로 표현하지 않는다.

3. 판정 기준

확정 요구사항과 실제 코드 경로가 일치하면 충족으로 판정한다. 결함 후보는 발생 순서·예외 전달·기존 테스트의 경계를 대조한 후 확정/오탐/보류로 분리한다. 이번 확정 항목은 제한된 동시 요청 조건에서 발생하고 재조회로 복구할 수 있어 Medium으로 판정한다. 단순 외부 감지 호출 중복 자체는 계획에서 허용한 범위이므로 결함으로 세지 않는다.

4. 검토 근거와 충족 항목

아래 source 경로는 src/main/kotlin/kr/co/vividnext/sodalive/ 기준이다.

요구사항 / 경계 코드 근거 판정
CCT-001 원문과 번역 분리 v2/creator/channel/community/translation/adapter/out/persistence/CreatorCommunityTranslation.kt:17, schema.sql 충족
CCT-007 현재 개정·언어·해시 일치 v2/creator/channel/community/translation/application/CreatorCommunityTranslationService.kt:95, i18n/translation/TranslationReadModelMaterializer.kt:194 충족
이전 감지 결과 배제 content/LanguageDetectEvent.kt:383, 감지 전 확인 및 잠금/refresh 후 재검사 충족
CCT-010 A→B→A 메모리 복원 CreatorCommunityTranslationService.requestTranslations에서 materialize 후 누락 예약 충족
같은 게시물 작업/번역 행 저장 직렬화 요청 진입점의 post lock, materializer의 post/translation lock 충족
언어 미확정 상태의 실제 캐시 INSERT 경합 최초 근거: content/LanguageDetectionCacheService.kt:21, :29 당시 수정 필요, P1-R4 자동 검증으로 수정 확인 (§9)

읽은 테스트에는 CreatorCommunityTranslationServiceTest의 shouldReturnOnlyCurrentTranslationFromBatchedRead, shouldFallbackToOriginalWhenTranslationIsMissingOrDoesNotMatchCurrentSource, shouldRematerializeAtoBtoAIntoExistingTranslationRow, shouldSerializeConcurrentMissingMemoryTranslationJobScheduling, shouldUpdateExistingTranslationAfterRepeatableReadSnapshotWaitsForFirstWriter가 포함된다. 기존 P1 리뷰의 수정 사항을 신규 결함으로 중복 등록하지 않았다.

실행한 검증은 git diff, rg, sed, nl을 통한 정적 대조와 파일 SHA256 계산이다. 기존 테스트 XML과 리뷰 레인 출처는 증거 기록에 있다. 새 테스트나 런타임 재현은 실행하지 않았다.

5. 발견 사항 요약

ID 심각도 상태 제목 소유 Task 후속 Goal
REV-P1-007 Medium 수정 완료 · P1-R4 자동 증거 (§9), 최초 정적 확정 이력 유지 최초 감지 캐시 저장 경합으로 후발 목표 언어의 예약 유실 P1-T2, P1-R2 P1-R4 완료, 실제 MySQL 수동 확인

6. REV-P1-007 상세

  • 관련 요구사항: CCT-005, CCT-010, 계획 P1-R2의 각 요청 target 보존 목표.
  • 관련 계약: PRD §4.2 상세 요청 언어만 예약, §6 기존 감지 캐시 재사용.
  • 근거 파일:
    • src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectionCacheService.kt:21: 캐시를 먼저 조회한다.
    • 같은 파일 :28, :29: 외부 감지 후 같은 키에 무조건 save한다.
    • src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectionResult.kt:15: 해시/provider/version 유일 키.
    • src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt:19: IDENTITY 저장 식별자.
    • src/main/kotlin/kr/co/vividnext/sodalive/content/LanguageDetectEvent.kt:398: 캐시 예외가 후속 post lock보다 먼저 전파된다.
    • 같은 파일 :411, :417: 목표 언어 후속 예약은 감지 성공 이후에만 등록된다.

정적 검증 순서

이 순서는 코드를 따라 확인한 결함 조건이며, 이번 턴에서 실제로 실행한 재현 결과가 아니다.

  1. 원문 언어 NULL이고 감지 캐시가 없는 동일 한국어 게시물에 일본어·영어 상세 요청을 보낸다.
  2. 두 감지 트랜잭션이 모두 언어 NULL과 캐시 miss를 읽은 뒤 각 detector 호출을 진행한다.
  3. 첫 detector가 반환하면 캐시 INSERT → 게시물 언어 저장 → 커밋 → 일본어 작업 예약이 진행된다.
  4. 뒤늦게 반환한 detector는 캐시를 다시 읽거나 원자적으로 합류하지 않고 같은 유일 키를 INSERT한다.
  5. 중복 키 예외로 두 번째 감지 트랜잭션이 종료되어 영어 목표를 예약하는 콜백에 도달하지 못한다.

기대 결과는 양쪽 목표 언어의 작업 보존이다. 코드 경로상 결과는 후발 목표의 작업 누락이다. 다음 영어 상세 조회로 다시 예약할 수 있으므로 영구적인 데이터 손실이라고 주장하지 않는다. 서로 다른 게시물을 같은 본문으로 동시에 신규 작성할 때도 동일한 전역 캐시 키 경합이 발생할 수 있다.

기존 테스트가 통과하는 이유

src/test/kotlin/kr/co/vividnext/sodalive/content/CreatorCommunityLanguageDetectTest.kt:335의 shouldPreserveSecondTargetAfterConcurrentDetectionSetsLanguage는 ControlledLanguageDetectionCacheService를 사용한다. 같은 파일 :743의 detectWithCache override는 실제 캐시 SELECT/INSERT 없이 제어된 언어를 반환한다. 따라서 이 테스트의 목표 보존 성공은 실제 캐시 유일 키 저장 경합까지 보장하지 않는다.

권장 조치와 판정

동일 캐시 키 저장을 원자적으로 처리해 이미 저장한 결과로 수렴시키고, 후발 감지 트랜잭션이 rollback-only가 되지 않게 한다. 이미 실패한 트랜잭션 내부에서 예외만 잡고 계속 진행하는 방식은 사용하지 않는다. 외부 API 호출 동안 게시물 행 잠금을 잡지 않는 현재 기준을 유지한다. 실제 캐시 service/repository를 사용하고 detector만 통제하는 두 cache miss 경합 회귀 테스트를 추가한다.

2026-09-10 — 품질 리뷰, 목표 리뷰, 루트 리뷰에서 위 정적 경로를 교차 확인하여 확정했다. 실행 재현은 사용자 지시에 따라 수행하지 않았으며 P1-R4의 RED 단계로 남긴다.

7. 계획 전환

plan-task.md의 Phase 1에 신규 P1-R4를 추가한다. 기존 P1-R2와 P1-GATE 완료 체크 및 검증 기록은 유지한다. 새 목표: 실제 최초 감지 캐시 경합에서도 두 요청 target을 보존하고 해당 회귀를 방지한다. 상세 검증 명령·파일·RED/GREEN/REFACTOR·재검토 조건은 신규 Task에 기록한다.

8. 리뷰 종료 판정

항목 결과
Phase 범위와 기존 수정 확인 충족
후보 판정 확정 1건. afterCommit 후 재잠금 교착 후보는 DB 커밋 후 잠금 해제로 오탐 제외
확정 항목 계획 반영 최초 P1-R4 신규 등록, 이후 구현·자동 검증·후속 리뷰 완료
실행 범위 명시 최초 리뷰는 테스트 미실행. 이후 P1-R4 로컬 자동 검증 증거를 §9에 별도 누적

최초 결론 기록: 수정 Goal 필요. 당시 최초 감지 캐시 경합 수정과 수정 후 검증이 필요했다. 현재 최종 결론: REV-P1-007은 P1-R4 로컬 자동 검증 범위에서 수정 완료. 실제 MySQL/Papago/HTTP 검증은 기존 배포 후 체크리스트에서 미완료로 유지한다.

9. P1-R4 수정 후 재판정 · 2026-09-10

  • RED는 실제 캐시 service/repository와 제어된 provider를 사용했다. 두 provider 진입 후 focused 명령이 exit 1로 실패했고 ExecutionException → DataIntegrityViolationException → H2 23505 캐시 유일 키 충돌을 확인했다.
  • GREEN은 native MySQL ON DUPLICATE KEY UPDATE id=id와 scalar SELECT ... FOR UPDATE로 승자 결과에 수렴시킨다. audit 시각을 명시하고 DB 예외를 잡아 같은 트랜잭션에서 계속하는 방식은 사용하지 않는다.
  • 같은 게시물 ja/en 이중 miss 및 다른 게시물의 동일 본문 경합에서 캐시 1행과 각 target 작업 보존을 확인했다. focused fresh 재실행은 --rerun-tasks --no-parallel로 BUILD SUCCESSFUL (5분 19초)이었다.
  • P1 listener/scheduler/materializer 회귀 (1분 09초), P2 상세/API 회귀 (1분 42초), ktlintCheck (44초)는 모두 BUILD SUCCESSFUL이다. 정확한 명령은 후속 증거에 기록한다.
  • spec PASS: ses_f7657d350ffe3YUYhEHvHPuikK. quality APPROVED: ses_f7656994dffeR2odTLswaw6RTo.
  • REV-P1-007의 실제 캐시 경합 테스트 공백은 위 자동 증거로 보완됐다. H2 MODE=MySQL은 MySQL 8/InnoDB의 실제 격리·잠금·DDL 증거가 아니다. 실제 MySQL의 순서 제어 경합, audit 컬럼, 감지 중 수정 가능 여부와 stale 차단, 실제 Papago/HTTP는 수동 체크리스트에서 확인해야 한다.
  • 전체 ./gradlew test는 생략했다. 공통 캐시 계약·실제 repository 경합·모든 직접 listener/scheduler/materializer 테스트·P2 상세/API 소비자가 통과했고 별도의 미해결 공통 경계가 없다는 영향 범위 판단에 따른다.