docs(creator-community): 1단계 검토 결과를 기록한다

This commit is contained in:
2026-09-10 15:09:36 +09:00
parent 0e96e233aa
commit 19b068b775
2 changed files with 252 additions and 0 deletions
@@ -0,0 +1,118 @@
# P1-T2 리뷰 보고서
## 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 1 / `P1-T2`, `P1-GATE` |
| 기준 working tree | P1-T2 구현 및 실제 Spring regression test 추가 상태 |
| 리뷰 일자 | 2026-09-10 |
| 리뷰어 | automated review |
| 기준 문서 | `prd.md`, `plan-task.md` |
| 리뷰 상태 | 추가 수정 및 회귀 검증 완료 |
## 범위
- 코드: `LanguageDetectEvent.kt`, `CreatorCommunityTranslationService.kt`
- 테스트: `CreatorCommunityLanguageDetectTest.kt`
- 제외: 다른 리소스 감지, 공개 API, Docker·HTTP·실제 Papago·MySQL 수동 검증
## 확정 발견 사항
| ID | 심각도 | 상태 | 제목 | 후속 goal |
|---|---|---|---|---|
| `REV-P1-T2-001` | Blocker | 수정 완료 | 커밋 후 번역 예약이 독립 트랜잭션으로 커밋되지 않는다 | `P1-R1` |
| `REV-P1-T2-002` | High | 수정 완료 | 잠금 조회가 stale managed 게시물 상태를 다시 읽지 않는다 | `P1-R1` |
| `REV-P1-T2-003` | High | 수정 완료 | 이미 언어가 있는 이벤트가 요청 target을 버린다 | `P1-R1` |
| `REV-P1-T2-004` | High | 수정 완료 | 이전 mock 검증은 실제 `translation_job` 저장을 증명하지 않는다 | `P1-R1` |
| `REV-P1-T2-005` | High | 수정 완료 | 동시 언어 미확정 감지의 두 번째 target이 lock 뒤 유실된다 | `P1-R2` |
| `REV-P1-T2-006` | Blocker | 수정 완료 | 동시 missing-memory 예약이 translation job unique key에 경합한다 | `P1-R3` |
## 근거와 판정
### REV-P1-T2-001
- 코드: `CreatorCommunityTranslationService.requestTranslations`는 기본 `@Transactional`이고, listener는 after-commit callback에서 이를 호출한다.
- 재현: `shouldExposeProxiedRequiresNewTranslationRequestEntryPoint`는 `Propagation.REQUIRES_NEW`가 아닌 annotation으로 실패했다.
- 영향: 감지 언어 저장 뒤에도 job 예약이 실제로 커밋되지 않아 번역 흐름이 진행되지 않는다.
- 조치: 공유 서비스의 public 요청 진입점을 `REQUIRES_NEW`로 선언하고 실제 job 저장 test로 검증한다.
### REV-P1-T2-002
- 코드: listener는 `findByIdAndIsActiveTrueForUpdate` 뒤 persistence context의 entity를 refresh하지 않는다.
- 재현: `shouldRejectStalePostAfterBlockedDetection`는 감지 동안 본문을 변경한 뒤에도 이전 언어가 저장되어 실패했다.
- 영향: 오래된 감지 결과가 새 본문 언어로 저장될 수 있다.
- 조치: 잠금 획득 뒤 최신 entity 상태를 refresh하고 revision/content를 재검사한다.
### REV-P1-T2-003
- 코드: listener는 이미 languageCode가 설정된 커뮤니티 이벤트를 감지 전 return한다.
- 재현: `shouldContinueBothRequestedTargetsWhenLanguageIsAlreadyKnown`는 `ja`, `en` target job이 저장되지 않아 실패했다.
- 영향: 상세 요청별 번역 예약 범위가 사라진다.
- 조치: 현재 본문 검증 뒤 known-language 이벤트도 after-commit에서 원 target으로 shared service를 호출한다.
### REV-P1-T2-004
- 코드/테스트: 기존 테스트는 `CreatorCommunityTranslationService` mock의 호출만 검증했다.
- 재현: 실제 Spring listener와 scheduler를 연결한 `CreatorCommunityLanguageDetectTest`에서 저장된 `translation_job` target을 확인하자 job이 없었다.
- 영향: mock interaction만 통과해도 사용자 흐름의 persistence 실패를 놓친다.
- 조치: 감지 provider만 제어하고 scheduler/repository는 실제 bean으로 사용한다.
### REV-P1-T2-005
- 코드: lock과 refresh 뒤 languageCode가 이미 있으면 revision/content 재검증 전에 return한다.
- 재현: 같은 언어 미확정 본문으로 두 감지를 시작해 첫 감지를 먼저 커밋한 뒤 두 번째 감지를 진행하면 두 번째 target job이 없다.
- 영향: 동시 상세 요청이 서로 다른 언어를 요청할 때 한 요청의 번역 예약이 유실된다.
- 조치: 현재 revision/content가 유효하면 이미 저장된 언어를 보존하고, 두 번째 이벤트의 원 target을 after-commit에 등록한다.
### REV-P1-T2-006
- 코드: `requestTranslations`는 post lock 없이 materializer의 missing-memory 반환 뒤 scheduler의 job 존재 조회와 insert를 실행한다.
- 재현: 같은 언어 확정 post와 target으로 두 request transaction을 동시에 시작하면 두 transaction이 missing job을 보고 insert를 시도할 수 있다.
- 영향: job unique key 예외가 호출자 transaction까지 전파돼 상세 예약 흐름이 실패할 수 있다.
- 조치: request transaction 시작에서 active post write lock과 refresh를 수행해 source extraction, memory lookup, job 존재 조회와 insert를 직렬화한다.
## 실행 증거
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest'` | 실패 | 초기 test bean 보정 후 7개 중 5개 assertion 실패 |
## 결론
`REV-P1-T2-001~006`은 `plan-task.md`의 `P1-R1~R3` 회귀 수정 goal로 반영했고 모두 수정·검증을 완료했다.
## 수정 후 검증 기록
### 1차 수정 검증 — 2026-09-10
- 무엇을: `REQUIRES_NEW` 번역 요청, stale entity refresh, known-language target continuation과 실제 `translation_job` 검증을 추가했다.
- 어떻게:
- `./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest'` — exit 0, `BUILD SUCCESSFUL`.
- `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*' ktlintCheck bootJar` — exit 0, `BUILD SUCCESSFUL`.
- 남은 항목: 사용자 정책상 Docker, HTTP, 실제 Papago, MySQL 수동 동시성 검증은 실행하지 않았다.
### 2차 수정 검증 — 2026-09-10
- 대상: `REV-P1-T2-005`.
- RED: `./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest'` — exit 1, 8개 중 1개 실패.
두 감지가 언어 NULL에서 시작한 뒤 첫 감지의 `ja` job 저장 후 두 번째 `en` job이 누락됐다.
- GREEN: lock과 refresh 뒤 revision/content가 유효하면 이미 저장된 languageCode를 유지하고,
원 target을 after-commit에 등록하도록 수정했다.
- 검증:
- `./gradlew test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest'` — exit 0, `BUILD SUCCESSFUL`.
- `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.*' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.*' ktlintCheck` — exit 0, `BUILD SUCCESSFUL`.
- 남은 항목: Docker, HTTP, 실제 Papago, MySQL 수동 동시성 검증은 사용자 정책상 실행하지 않았다.
### 3차 수정 검증 — 2026-09-10
- 대상: `REV-P1-T2-006`.
- RED: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest'`
— exit 1. 실제 `translation_job` insert가 H2 `SQLState 23505`,
`uk_translation_job_resource_field_target_hash` unique index 충돌로 실패했다.
- GREEN: `requestTranslations` transaction 시작에서 active post write lock과 `PESSIMISTIC_WRITE` refresh를 수행하도록 수정했다.
- 검증:
- 같은 focused test — exit 0, `BUILD SUCCESSFUL`; 두 호출 정상 종료와 job 1개 저장을 확인했다.
- P1 직접 영향 회귀와 P1 Gate — exit 0, `BUILD SUCCESSFUL`.
- `./gradlew ktlintCheck` — exit 0, `BUILD SUCCESSFUL`.
- 남은 항목: Docker, HTTP, 실제 Papago, MySQL 수동 동시성 검증은 사용자 정책상 실행하지 않았다.
@@ -0,0 +1,134 @@
# 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 소비자가 통과했고 별도의 미해결 공통 경계가 없다는 영향 범위 판단에 따른다.