135 lines
10 KiB
Markdown
135 lines
10 KiB
Markdown
# Phase 1 리뷰 보고서
|
|
|
|
## 1. 리뷰 정보
|
|
|
|
| 항목 | 내용 |
|
|
|---|---|
|
|
| 대상 | Phase 1 `P1-T1`, `P1-T2`, `P1-R1~R4`, `P1-GATE` |
|
|
| 기준 | HEAD `50409e41c0c469529572f5d77033c3cb23d67d22` + 현재 작업 트리 |
|
|
| 작업 트리 지문 | `3bbb1d90869484073c3af681118a13f5f03c3ccecbe84de16f1568338acd66b7` |
|
|
| 일자 / 리뷰어 | 2026-09-10 / Codex 및 목표·품질·보안·QA 증거 리뷰어 |
|
|
| 기준 문서 | [PRD](../prd.md), [구현 계획](../plan-task.md), `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과 리뷰 레인 출처는 [증거 기록](review-evidence.md)에 있다. 새 테스트나 런타임 재현은 실행하지 않았다.
|
|
|
|
## 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`이다. 정확한 명령은 [후속 증거](review-evidence.md)에 기록한다.
|
|
- spec `PASS`: `ses_f7657d350ffe3YUYhEHvHPuikK`. quality `APPROVED`: `ses_f7656994dffeR2odTLswaw6RTo`.
|
|
- `REV-P1-007`의 실제 캐시 경합 테스트 공백은 위 자동 증거로 보완됐다. H2 `MODE=MySQL`은 MySQL 8/InnoDB의
|
|
실제 격리·잠금·DDL 증거가 아니다. 실제 MySQL의 순서 제어 경합, audit 컬럼, 감지 중 수정 가능 여부와 stale 차단,
|
|
실제 Papago/HTTP는 [수동 체크리스트](../plan-task.md)에서 확인해야 한다.
|
|
- 전체 `./gradlew test`는 생략했다. 공통 캐시 계약·실제 repository 경합·모든 직접 listener/scheduler/materializer
|
|
테스트·P2 상세/API 소비자가 통과했고 별도의 미해결 공통 경계가 없다는 영향 범위 판단에 따른다.
|