From f3a558f659d66bf88237fb0069d4364f28b5e9a4 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 9 Nov 2026 15:43:23 +0900 Subject: [PATCH] =?UTF-8?q?docs(i18n):=20=EA=B3=B5=EA=B8=89=EC=9E=90=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5=20=EB=B0=A9=EB=B2=95=EA=B3=BC=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20=EA=B8=B0=EB=A1=9D?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 341 ++++++++++++++++++ docs/20261109_감지번역공급자공통화/prd.md | 81 +++++ .../provider-guide.md | 106 ++++++ .../reviews/phase-1-review.md | 45 +++ 4 files changed, 573 insertions(+) create mode 100644 docs/20261109_감지번역공급자공통화/plan-task.md create mode 100644 docs/20261109_감지번역공급자공통화/prd.md create mode 100644 docs/20261109_감지번역공급자공통화/provider-guide.md create mode 100644 docs/20261109_감지번역공급자공통화/reviews/phase-1-review.md diff --git a/docs/20261109_감지번역공급자공통화/plan-task.md b/docs/20261109_감지번역공급자공통화/plan-task.md new file mode 100644 index 00000000..dcfbb5de --- /dev/null +++ b/docs/20261109_감지번역공급자공통화/plan-task.md @@ -0,0 +1,341 @@ +# 언어 감지·번역 공급자 공통화 구현 계획 + +| 항목 | 내용 | +|---|---| +| 작성일 | 2026-11-09 | +| 상태 | 구현·검증·독립 리뷰·리소스 정리 완료 | +| 요구사항 | [prd.md](prd.md), LP-001~010 및 확정 구현 계약 | +| 활성 Goal | 없음 | + +## 목표와 제약 + +별도 감지·번역 인터페이스와 독립 공급자 순서 설정으로 새 구현체를 도메인 변경 없이 추가할 수 있게 한다. +기존 Kotlin 1.6.21 / Java 17 / Spring Boot 2.7.14 / RestTemplate / JPA를 사용한다. +AWS SDK·실제 adapter·새 의존성·DDL·공개 API·워커 스케줄/재시도·커뮤니티 revision 정책은 변경하지 않는다. +새 공급자 selector를 vendor처럼 등록하거나 실제 성공 결과를 selector 이름으로 저장하지 않는다. +별도 worktree 없이 현재 작업 트리에서 진행한다. 구현 위임 범위에서는 commit/staging/push하지 않는다. +부모 세션은 ULW의 명시적 커밋 지시에 따라 검증 완료 후 원자적 커밋을 만들며 push하지 않는다. + +## 실행 순서 + +공통 계약·설정 → 감지/번역 연결과 fixture 갱신 → 기능별 회귀·HTTP/DB QA → 독립 리뷰 순서다. +생산 코드/fixture 간 공유 의존성이 있어 구현 에이전트 한 명이 순차 진행한다. 부모는 별도 QA·최종 리뷰를 맡는다. + +### Phase 1: 공급자 독립 실행과 캐시 + +- [x] **Task 1.1: 공통 공급자 구조 구현 (P1-T1)** + +**Objective:** 두 기능의 독립 우선순위, 실패 전환, 실제 provider 저장과 공통 cache 선택을 완성한다. +**시작 조건:** PRD 확정, 기존 64건 baseline 통과, Oracle 계약 검토 및 Plan 순서 검토 완료. +**완료 증거:** 새 테스트의 의미 있는 RED/GREEN, 기존 fixture 회귀, 기능 범위 빌드/정적 검사. +**범위 밖:** 실 AWS 연동, 실행 중 설정 변경, 범용 plugin framework, 새로운 DB 경합 해결 정책. + +**Files (src/main/kotlin/kr/co/vividnext/sodalive 기준):** +- Create: `i18n/translation/LanguageProviderProperties.kt`, `LanguageDetectionProvider.kt`, `DetectedLanguage.kt`, + `ProviderTranslationResult.kt`, `TranslationLanguages.kt`, `PapagoLanguageDetectionProvider.kt`, `TranslationCoordinator.kt`. +- Modify: `content/LanguageDetectionCacheService.kt`, `content/LanguageDetectEvent.kt`. +- Modify: `i18n/translation/TranslationJobWorker.kt`, `TranslationReadModelMaterializer.kt`, `PapagoTranslationService.kt`, + `ResourceTranslationJobScheduler.kt`, `TranslationJobScheduler.kt`. +- Modify: `v2/creator/channel/community/translation/application/CreatorCommunityTranslationService.kt` (언어 helper 참조만). +- Modify: `src/main/resources/application.yml` (독립 순서 설정과 환경변수). + +**Tests (src/test/kotlin/kr/co/vividnext/sodalive 기준):** +- Add focused tests in `i18n/translation/`: 공급자 순서 설정, `TranslationCoordinator`, `PapagoLanguageDetectionProvider`, + 독립 순서 Spring wiring 및 보조 공급자 캐시 저장/materialization 통합 시나리오. +- Update: `content/LanguageDetectionCacheServiceTest.kt`, `CreatorCommunityLanguageDetectTest.kt`, + `CreatorCommunityLanguageDetectionCacheConcurrencyTest.kt`. +- Update: `i18n/translation/TranslationJobWorkerTest.kt` 및 생성자 변경의 직접 영향 fixture. +- Update where required: `v2/creator/channel/community/translation/CreatorCommunityTranslationServiceTest.kt`, + `explorer/profile/creatorCommunity/CreatorCommunityTranslationEndToEndTest.kt`. +- Preserve: `i18n/translation/PapagoTranslationServiceTest.kt`의 17개 HTTP/chunk 회귀 assertion. + +**Interfaces:** PRD의 확정 구현 계약을 그대로 적용한다. 설정 validation은 각 capability의 등록 구현체 목록과 대조한다. +후보 구현체는 설정 순서로 정렬하고, 첫 성공 뒤 중단한다. catch 밖의 repository 오류는 그대로 전파한다. +DB 스키마를 유지하고 cache key의 provider 값에는 실제 성공 adapter 이름을 사용한다. + +- [x] **RED:** 기존 behavior를 보존하는 fixture 갱신 후, 두 가짜 공급자의 순서·실패 전환·저장 귀속 테스트를 작성한다. +- [x] **RED 확인:** 신규 타입의 unresolved symbol 오류는 RED로 계산하지 않는다. 필요한 최소 선언으로 컴파일한 뒤 + 누락된 순서/전환/캐시 동작 때문에 실패하는 assertion을 실제로 확인한다. +- [x] **GREEN:** PRD 계약을 충족하는 최소 두 coordinator 및 설정/adapter 연결을 구현한다. +- [x] **GREEN 확인:** 새 focused 테스트에서 독립 순서·캐시·폴백·모두 실패·설정 오류가 통과한다. +- [x] **REFACTOR:** 이번 변경의 중복만 정리하고 기존 focused·DB 경합·revision·chunk 테스트를 회귀 실행한다. + +**P1-T1 결과:** 새 테스트 21건과 기존 64건을 포함한 85건이 기능별 실행에서 모두 통과했다. +`LanguageDetectionCacheService`의 insertIfAbsent → locking read와 커뮤니티 경합·revision assertion은 유지했다. +번역 worker/materializer는 같은 coordinator cache lookup을 사용하며 저장 provider/version은 실제 성공 adapter 값이다. +기본 production bean은 Papago뿐이며 테스트 secondary는 `@Primary` 없이 list 주입으로 등록된다. +전체 테스트·운영 MySQL·실 API 호출은 수행하지 않았다. 명령/RED·GREEN/한계는 아래 실행 증거에 누적했다. + +**구현상 보존 항목:** +1. 감지 cache 트랜잭션, insertIfAbsent 이후 locking read 승자 반환, cache hit 무호출. +2. listener의 AFTER_COMMIT/REQUIRES_NEW, domain별 languageCode 가드, 커뮤니티 lock/refresh/revision 및 afterCommit 요청. +3. worker는 외부 번역을 DB 저장 트랜잭션 밖에서 실행하고, 성공 vendor 기준 저장 직전 재조회·저장한다. +4. materializer의 조회에는 외부 호출이 없고, 현재 원문 검증·read model 저장 방식은 유지한다. +5. 테스트의 latch·동시 경합·stale revision assertions는 삭제/완화하지 않는다. 새 seam으로 fixture만 옮긴다. + +**검증 명령:** +```bash +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.i18n.translation.*' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectionCacheConcurrencyTest' +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest' --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityTranslationEndToEndTest' +./gradlew --no-daemon --offline ktlintCheck bootJar +``` + +- [x] **Task 1.2: 확장 방법 문서화 (P1-T2)** + +**Objective:** 새 공급자 추가에 필요한 구현체·빈·설정·테스트 절차를 개발자가 바로 적용할 수 있게 한다. +**시작 조건:** 실제 구현 계약과 설정 key 확정. +**Files:** `docs/20261109_감지번역공급자공통화/provider-guide.md`. +**완료 증거:** 현재 설정과 일치하는 예시, 감지/번역별 순서 변경, 미등록 vendor 시작 실패, AWS 미구현 명시. +**TDD 예외:** 문서 변경이며 실제 코드/config/테스트와 대조한다. +**범위 밖:** 미구현 AWS 코드 예제를 작동 코드처럼 제공하거나 가짜 SDK 계약 작성. + +**P1-T2 결과:** `provider-guide.md`를 작성하고 실제 interface/생성자/config/test와 대조했다. +구현체+빈+해당 기능 순서 설정만으로 확장하며 기존 business 호출자는 수정하지 않는 경로를 문서화했다. +독립 환경변수, AWS/secondary의 현재 미구현·테스트 전용 경계, cache-first 및 오류·검증 한계를 명시했다. +문서 추가 후 별도 Gradle 실행은 하지 않았고 코드 검증은 P1-T1의 fresh 결과를 사용했다. + +- [x] **Phase Gate: 통합 검증·리뷰 (P1-GATE)** + +**Objective:** 가짜 공급자만 추가해 실패 전환·순서 교체·저장·재사용이 관측됨을 확인한다. +**시작 조건:** P1-T1/P1-T2 완료. +**완료 증거:** 아래 시나리오 통과, 독립 리뷰의 차단 이슈 없음, 임시 리소스 정리 영수증. +**TDD 예외:** 독립 실행 확인·리뷰로, 새 구현은 추가하지 않는다. + +| 시나리오 | 이진 통과 조건 | 표면 | +|---|---|---| +| S1 기존 동작 | 파파고 단독 및 기존 64건 회귀 유지 | 실제 서비스 HTTP mock + 기존 H2 integration | +| S2 독립 순서·전환 | 기능별 설정 교체가 다른 기능에 영향 없음, 첫 성공 이후 호출 0회 | Spring context와 fake adapters, 부모 driver | +| S3 저장·재사용 | 보조 공급자 실제 이름으로 저장, 이후 모든 외부 호출 0회, materializer 결과 일치 | 실제 H2 repository 재조회 | +| S4 실패·설정 | 모두 실패 시 안전한 결과, 빈/중복/unknown 설정 시작 실패, DB 오류를 공급자 실패로 숨기지 않음 | focused tests 및 부모 context driver | +| S5 파파고 감지 분리 | 감지 HTTP 형식과 결과를 유지하고 listener는 vendor를 모름 | 실제 RestTemplate loopback 또는 controlled HTTP 경계 | + +## 검증 기록 + +- 변경 전 baseline: 번역 package, 감지 cache, 커뮤니티 감지/경합 및 번역 service 60건 통과, exit 0, 1m38s. +- 추가 baseline: `CreatorCommunityTranslationEndToEndTest` 4건 통과, exit 0, 2m38s. 합계 64건. +- Oracle는 두 coordinator, cache-first, 실제 provider 귀속, 기존 DB/트랜잭션 보존 설계를 검토했다. +- LSP 도구 미제공. Kotlin 컴파일/ktlint로 보완하며 성공한 LSP 진단으로 표현하지 않는다. +- 전체 테스트는 실행하지 않는다. 사용자 명령: `./gradlew test`. +- RUN_TMP: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/language-providers.06uAbV`. + 모든 실행의 임시 자원은 이 작업 소유 root 아래에 두고 종료 시 삭제한다. 공유 Gradle 캐시나 다른 daemon은 건드리지 않는다. +- 구현 child root: `implementation.F9mSBa` (위 RUN_TMP 하위, parent 존재 확인 후 `mktemp -d` 생성). + 모든 shell 실행은 이 child로 TMPDIR/NODE_COMPILE_CACHE/BUN_RUNTIME_TRANSPILER_CACHE_PATH를 지정한다. + 정리 완료: 검증 프로세스 종료 확인 후 이 child만 삭제했다. 아래 영수증 참조. parent/shared cache는 유지한다. +- 실행 판정: 기존 승인 문서를 재사용하며 새 planning/worktree/리뷰 dispatch는 하지 않는다. P1-GATE는 부모가 판정한다. + +### P1-T1 실행 증거 (2026-11-09) + +모든 Gradle 실행은 `--no-daemon --offline`, tool timeout `600000ms`로 시작했다. 무필터 test/check/build는 실행하지 않았다. +아래 명령 전에 다음 환경변수를 child root로 지정했다. 로그는 child 안에서만 보관하며 정리 시 삭제한다. + +```bash +export TMPDIR=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/language-providers.06uAbV/implementation.F9mSBa +export NODE_COMPILE_CACHE="$TMPDIR" BUN_RUNTIME_TRANSPILER_CACHE_PATH="$TMPDIR" +``` + +**RED-1 (9건, 9실패, exit 1, 7m40s):** 컴파일 가능한 최소 계약과 첫 adapter만 호출하는 coordinator로 실행했다. +설정 순서·잘못된 설정 거부·폴백·안전한 전체 실패·guard·캐시 조회 누락 assertion이 실패했다. +unresolved symbol이나 컴파일 실패는 없었다. + +```bash +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.i18n.translation.TranslationCoordinatorTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderPropertiesTest' +``` + +**RED-2 / 부분 GREEN (19건, 7실패, exit 1, 9m01s):** RED-1의 9건은 GREEN. +감지의 다음 공급자 미호출·예외 전파·보조 cache-first 누락 3건이 행동 RED였다. +raw 감지 stub의 HTTP 미호출 3건과 Spring context의 쉼표 목록 변환 문제 1건도 관측했다. +설정은 `@Value String` → split/trim으로 고쳐 별도 변환 서비스 의존을 제거했다. + +```bash +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageDetectionProviderTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.PapagoLanguageDetectionProviderTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderSpringConfigurationTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.TranslationCoordinatorTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderPropertiesTest' +``` + +**H2 첫 실행 (4건, 2실패, exit 1, 3m27s):** Spring 독립 순서 2건 GREEN, 감지 fallback 누락은 행동 RED. +번역 테스트는 H2의 기존 MySQL `FOR UPDATE SKIP LOCKED` syntax 오류로 worker 결과까지 도달하지 못했으므로 +이 SQL 오류는 행동 RED로 계산하지 않았다. production query는 바꾸지 않고 test spy로 claim만 대체했다. + +```bash +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderCacheEndToEndTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderSpringConfigurationTest' +``` + +**H2 RED / 부분 GREEN (21건, 2실패, exit 1, 7m20s):** 감지 fallback과 H2 secondary 감지 귀속은 GREEN. +기존 worker가 Papago 실패 뒤 secondary를 호출하지 않아 secondary memory 재조회가 null인 행동 RED를 확인했다. +다른 1건은 HTTP form Content-Type에 실제 UTF-8 charset이 붙는 테스트 기대값 오류로, 행동 RED로 계산하지 않았다. +실제 요청의 charset을 정확히 기대하도록 수정했으며 공급자 요청 형식은 기존과 같다. + +```bash +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderCacheEndToEndTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageDetectionProviderTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.PapagoLanguageDetectionProviderTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderSpringConfigurationTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.TranslationCoordinatorTest' --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderPropertiesTest' +``` + +**GREEN 및 최종 회귀:** 아래 각 실행은 XML의 tests/failures/errors/skipped를 확인했다. 총 85건 = 기존 64 + 신규 21, +최종 각 실행 모두 failure/error/skipped 0, exit 0이다. 최초 package GREEN도 51건, 7m27s였다. + +| 실행 | 결과 | 소요 | +|---|---|---| +| 번역 package + 감지 cache (아래 첫 명령, 최종 재실행) | 51/51 | 45s | +| 커뮤니티 감지 + 최초 cache 경합 | 14/14 (12 + 2) | 2m23s | +| 커뮤니티 번역 service + 레거시 end-to-end | 20/20 (16 + 4) | 52s | +| ktlintCheck + bootJar | 성공, exit 0 | 1m20s | + +```bash +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.i18n.translation.*' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectionCacheConcurrencyTest' +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest' --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityTranslationEndToEndTest' +./gradlew --no-daemon --offline ktlintCheck bootJar +``` + +신규 21건의 구성은 TranslationCoordinator 6, 설정 unit 3, 감지 coordinator 5, Papago 감지 HTTP 3, +실제 Spring 설정 2, 실제 H2 저장/worker/materializer 2다. 기존 Papago chunk 17건은 수정 없이 통과했다. +`ktlintCheck` 초기 실행은 import/들여쓰기/줄 길이로 exit 1 (1m44s)이었으며 모든 수정을 apply_patch로 적용했다. +중간 ktlintCheck는 exit 0 (38s), 최종 ktlintCheck+bootJar도 exit 0이다. 자동 formatter는 사용하지 않았다. +기존 unrelated 테스트의 Kotlin unchecked cast/불필요한 `!!` 경고와 Gradle 9 deprecation 경고는 유지했다. + +**검증 한계/판정:** 제공된 LSP/diagnostics 도구는 없다. Kotlin 컴파일과 ktlint로 보완했다. +H2 integration은 실제 DB round-trip, worker 결과, materializer 표시와 무호출 cache 재사용을 검증하지만, +H2 미지원 MySQL claim SQL 한 곳은 test spy이며 운영 MySQL claim 동시성을 새로 증명하지 않는다. +실제 유료 API·운영 DB·전체 테스트는 실행하지 않았다. 사용자 전체 검증 명령은 `./gradlew test`다. +부모의 독립 Java driver와 ONE reviewer는 P1-GATE에서 수행하며 이 구현자가 대신 완료 표시하지 않는다. + +### 부모 Java QA용 실제 생성자/바인딩 + +`javap -classpath build/classes/kotlin/main`으로 아래 생성자와 메서드가 존재함을 확인했다. +클래스 패키지는 content service/listener를 제외하면 `kr.co.vividnext.sodalive.i18n.translation`이다. + +```text +LanguageProviderProperties(String detectionOrder, String translationOrder) +LanguageProviderProperties() // 두 기능 모두 papago +getDetectionOrder(): List +getTranslationOrder(): List +TranslationCoordinator(List, LanguageProviderProperties, TranslationMemoryRepository) +findCached(String sourceHash, String sourceLanguage, String targetLanguage): TranslationMemory? +translate(TranslateRequest): ProviderTranslationResult +LanguageDetectionCacheService(LanguageDetectionResultRepository, List, LanguageProviderProperties) +detectWithCache(String): DetectedLanguage? +PapagoLanguageDetectionProvider(String papagoClientId, String papagoClientSecret) +ProviderTranslationResult(TranslateResult, String providerName, String providerVersion) +DetectedLanguage(String detectedLanguage, String providerName) +TranslationJobWorker(TranslationJobRepository, TranslationMemoryRepository, TranslationCoordinator, + TranslationReadModelMaterializer, PlatformTransactionManager) +TranslationReadModelMaterializer(TranslationSourceExtractor, TranslationCoordinator, + ContentTranslationRepository, AiCharacterTranslationRepository, ContentThemeTranslationRepository, + SeriesTranslationRepository, SeriesGenreTranslationRepository, OriginalWorkTranslationRepository, + CategoryTranslationRepository, CreatorCommunityRepository, CreatorCommunityTranslationRepository, EntityManager) +``` + +`LanguageDetectListener`는 기존 repository/service/EntityManager/publisher 주입을 유지하고 마지막 Papago 인증 String 2개만 제거했다. +HTTP 테스트 reflection 대상은 listener가 아니라 `PapagoLanguageDetectionProvider.restTemplate`이다. +설정은 component의 두 생성자 인자에 각각 `@Value("\${sodalive.language-providers.detection-order:papago}")`, +`@Value("\${sodalive.language-providers.translation-order:papago}")`가 붙는다. +application.yml의 독립 환경변수는 `LANGUAGE_PROVIDERS_DETECTION_ORDER`, `LANGUAGE_PROVIDERS_TRANSLATION_ORDER`다. +Spring context 테스트는 두 가짜 bean의 실제 순서 변경과 빈/중복/unknown 설정 시작 실패를 확인한다. + +### 이 구현자가 변경한 전체 파일 + +생산 Kotlin 경로 prefix: `src/main/kotlin/kr/co/vividnext/sodalive/`. + +```text +A i18n/translation/DetectedLanguage.kt +A i18n/translation/LanguageDetectionProvider.kt +A i18n/translation/LanguageProviderProperties.kt +A i18n/translation/PapagoLanguageDetectionProvider.kt +A i18n/translation/ProviderTranslationResult.kt +A i18n/translation/TranslationCoordinator.kt +A i18n/translation/TranslationLanguages.kt +M content/LanguageDetectEvent.kt +M content/LanguageDetectionCacheService.kt +M i18n/translation/PapagoTranslationService.kt +M i18n/translation/ResourceTranslationJobScheduler.kt +M i18n/translation/TranslationJobScheduler.kt +M i18n/translation/TranslationJobWorker.kt +M i18n/translation/TranslationReadModelMaterializer.kt +M v2/creator/channel/community/translation/application/CreatorCommunityTranslationService.kt +``` + +테스트 Kotlin 경로 prefix: `src/test/kotlin/kr/co/vividnext/sodalive/`. + +```text +A i18n/translation/LanguageDetectionProviderTest.kt +A i18n/translation/LanguageProviderCacheEndToEndTest.kt +A i18n/translation/LanguageProviderPropertiesTest.kt +A i18n/translation/LanguageProviderSpringConfigurationTest.kt +A i18n/translation/PapagoLanguageDetectionProviderTest.kt +A i18n/translation/TranslationCoordinatorTest.kt +M content/CreatorCommunityLanguageDetectTest.kt +M content/CreatorCommunityLanguageDetectionCacheConcurrencyTest.kt +M content/LanguageDetectionCacheServiceTest.kt +M i18n/translation/TranslationJobWorkerTest.kt +M v2/creator/channel/community/translation/CreatorCommunityTranslationServiceTest.kt +``` + +나머지 3개: `src/main/resources/application.yml` 수정, +`docs/20261109_감지번역공급자공통화/plan-task.md` 갱신, +`docs/20261109_감지번역공급자공통화/provider-guide.md` 추가. 합계 29개다. +부모가 만든 `prd.md`는 읽기만 했으며 이 구현자는 변경하지 않았다. +`CreatorCommunityTranslationEndToEndTest.kt`는 기존 생성자 영향이 없어 변경하지 않고 4건을 그대로 실행했다. +의존성·DDL·AWS 코드·git index/commit/branch/worktree는 변경하지 않았다. + +### 소유 임시 자원 정리 영수증 + +- 삭제 대상: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/language-providers.06uAbV/implementation.F9mSBa`. +- 모든 Gradle 명령이 종료된 뒤 `lsof -nP +D "$TMPDIR"`에서 열린 파일이 없음을 확인했다 (출력 없음, exit 1). + process 목록에서도 이 child를 사용하는 Gradle/test/compiler 프로세스가 없었다. 다른 프로젝트의 daemon은 종료하지 않았다. +- 정확한 child 경로를 `test`로 대조한 후 해당 child만 `rm -rf -- "$TMPDIR"`로 삭제했다. + 이어진 `test ! -e "$TMPDIR"`와 parent `ls -ld`가 exit 0이었다 (2026-11-09 15:22 KST). +- child의 일회성 로그/임시 파일만 제거했다. parent, 부모 QA 소유 형제 경로, 공유 Gradle/Kotlin cache는 유지했다. + 이 문서의 명령/횟수/실패 원인/최종 결과는 로그 삭제 전에 기록했다. + +### P1-GATE 부모 독립 실행 검증 + +- 부모가 최종 production/test diff, 신규 interface/coordinator/config 및 fixture 변환을 직접 읽었다. + 기존 concurrency/revision assertion은 삭제·완화되지 않았다. +- fresh 명령: 아래 실행에서 `:test`가 실제 재실행됐고 exit 0, `BUILD SUCCESSFUL in 2m`이었다. + ktlintCheck/bootJar는 직전 성공 산출물과 일치하여 UP-TO-DATE였다. + +```bash +./gradlew --no-daemon --offline test --rerun \ + --tests 'kr.co.vividnext.sodalive.i18n.translation.*' \ + --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' \ + --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectTest' \ + --tests 'kr.co.vividnext.sodalive.content.CreatorCommunityLanguageDetectionCacheConcurrencyTest' \ + --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest' \ + --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityTranslationEndToEndTest' \ + ktlintCheck bootJar +``` + +- 부모가 XML 15개를 직접 확인했다. 총 85건, failures/errors/skipped 모두 0. +- 별도 임시 Java `ProviderQa`를 Java 17로 컴파일하고 실제 Spring AnnotationConfigApplicationContext에서 실행했다. + 실제 공통 설정·coordinator와 테스트 전용 `papago`/`aws` adapter, 메모리 repository 대역을 사용했다. + 이 driver는 DB 검증이 아니며 DB 저장/재조회는 위 H2 테스트가 담당한다. + +| 부모 runtime QA | 관측 결과 | +|---|---| +| 감지 aws,papago / 번역 papago,aws | 감지 aws, 번역 papago만 호출 | +| 감지 papago,aws / 번역 aws,papago | 감지 papago, 번역 aws만 호출 | +| 파파고 예외 후 보조 성공 | detect:papago → detect:aws, translate:papago → translate:aws; 성공 provider는 aws | +| 보조 cache 재사용 | 감지·번역 provider 호출 0회 | +| 모든 provider 실패 | 감지 null, 번역 cause 없는 고정 실패 메시지 | +| unknown/duplicate/empty 설정 | 실제 Spring context 초기화 실패 | + +- driver 출력: `QA PASS: independent Spring-configured provider chains; all contexts closed; no external requests` (exit 0). +- 별도 `PapagoDetectionQa`에서 실제 추출된 Papago adapter와 RestTemplate을 loopback HTTP 서버에 연결했다. + test-only interceptor로 목적지를 127.0.0.1로 바꿨으며 운영 endpoint/설정은 변경하지 않았다. + 원문 공백·한글·&의 form 직렬화, POST, 가짜 인증 헤더, 성공 언어 반환, blank 결과 null, + HTTP 500 전파의 3개 응답 시나리오를 검증했다. + 출력: `QA PASS: 3 loopback HTTP scenarios; server stopped; no paid API requests` (exit 0). +- 모든 Spring context와 loopback 서버는 finally/try-with-resources에서 종료됐다. + 부모 driver·추출 라이브러리·로그는 최종 정리에서 소유 RUN_TMP만 제거한다. +- `git diff --check` exit 0. LSP·전체 테스트·실제 유료 API·운영 MySQL 미검증 범위는 기존 기록과 같다. +- 독립 최종 reviewer 판정: **APPROVE**, 확정 발견 사항 없음. 미추적 파일을 포함한 전체 변경과 + XML 15개(85건 통과)를 직접 대조했다. [리뷰 보고서](reviews/phase-1-review.md). +- 원자적 커밋은 (1) 공통 계약·순서 설정·raw 감지/coordinator와 해당 unit/HTTP 테스트, + (2) 기존 감지/worker/materializer 연결과 fixture·통합 테스트, (3) 요구사항·계획·가이드·리뷰 문서로 나눈다. + 첫 단위는 기존 호출자와 독립적으로 컴파일 가능하며, 두 번째 단위는 생성자/fixture 변경을 함께 보존한다. +- 코드 커밋: `320adb03` (공통 계약·설정·기반 테스트), `f7fadf7a` (기존 흐름 연결·캐시·통합 테스트). + 두 커밋의 변경 Kotlin pre-commit ktlint와 메시지 규칙 사전/사후 검사 모두 통과했다. + 코드 커밋 후 production/test 미커밋 diff가 없고 문서 디렉터리만 남음을 확인했다. push하지 않는다. +- 부모 소유 리소스 정리: 모든 driver/context/server/Gradle 실행이 종료됐고 `lsof -nP +D "$RUN_TMP"` 출력이 없었다. + 사전 생성·내용 확인한 `language-providers.06uAbV`만 삭제하고 `test ! -e` exit 0을 확인했다. + 출력: `CLEANUP PASS: owned language-providers.06uAbV removed; shared caches untouched`. + 임시 Java/class/추출 라이브러리/로그가 함께 제거됐으며 필요한 증거는 이 문서에 보존했다. + 작업 임시 노트도 문서에 이관 후 삭제한다. 다른 작업의 파일·프로세스·캐시에는 조치하지 않았다. diff --git a/docs/20261109_감지번역공급자공통화/prd.md b/docs/20261109_감지번역공급자공통화/prd.md new file mode 100644 index 00000000..b153495a --- /dev/null +++ b/docs/20261109_감지번역공급자공통화/prd.md @@ -0,0 +1,81 @@ +# 언어 감지·번역 공급자 공통화 PRD + +| 항목 | 내용 | +|---|---| +| 작성일 | 2026-11-09 | +| 상태 | 구현·기능 범위 검증·독립 리뷰 완료 | +| 승인 범위 | 감지·번역 별도 인터페이스, 공통 공급자 선택·실패 전환, 기능별 순서 설정 | +| 계획 | [plan-task.md](plan-task.md) | + +## 목적 + +파파고에 결합된 언어 감지·번역 호출을 분리한다. 이후 공급자는 해당 인터페이스 구현체와 빈·인증·순서 설정을 +추가하면 사용할 수 있어야 하며, 콘텐츠·댓글·커뮤니티 등 도메인 코드에 공급자 분기를 추가하지 않는다. + +## 범위와 제외 + +- 현재 실행되는 실제 구현체는 파파고뿐이다. AWS 실제 API·SDK 구현은 다음 작업이며 이번에는 추가하지 않는다. +- AWS 구현체가 등록된 뒤 기본 운영 의도는 파파고 → AWS이다. 감지와 번역 순서는 각각 독립 설정으로 바꾼다. +- 기본 실행 설정은 존재하는 파파고만 사용한다. 미구현 AWS를 등록한 것처럼 처리하거나 네트워크 호출하지 않는다. +- 기존 파파고 5,000 UTF-16 단위 분할, 지원 번역 언어 ko/en/ja, 공개 API, DB 스키마, job 재시도 정책은 유지한다. +- 감지 결과 저장 대상별 차이, 커뮤니티 원문 revision 검증, 이벤트 after-commit 처리 및 DB 경합 규칙을 유지한다. +- 실행 중 설정 변경, 관리 화면, 임의 플러그인 로딩, 새 번역 품질 정책, 기존 FAILED 작업 재처리는 제외한다. + +## 요구사항 + +| ID | 수용 기준 | +|---|---| +| LP-001 | 감지와 번역은 독립된 공급자 인터페이스이며, 둘 중 하나만 구현한 공급자도 해당 기능에 등록 가능하다. | +| LP-002 | 감지·번역의 공급자 순서를 서로 다른 설정으로 지정한다. 테스트 공급자 2개로 한쪽만 순서를 바꿔도 다른 쪽에 영향이 없음을 증명한다. | +| LP-003 | 공급자별 예외 또는 유효 결과 없음이면 다음 공급자를 순서대로 한 번 시도한다. 첫 성공 뒤 후속 공급자를 호출하지 않는다. | +| LP-004 | 모든 공급자 실패 시 감지는 기존처럼 결과 없음으로 처리하고 번역은 기존 워커 실패·재시도 경로를 따른다. | +| LP-005 | 캐시는 실제 성공 공급자 이름으로 저장한다. AWS 역할의 가짜 공급자 성공을 papago 이름으로 저장하지 않는다. | +| LP-006 | 설정된 공급자들의 유효 캐시를 우선 재사용한다. 새 외부 호출의 순서는 별도 설정 순서를 따른다. 워커와 결과 조립의 캐시 선택 정책이 같다. | +| LP-007 | 빈 원문·동일 언어·서비스 미지원 번역 언어는 불필요한 공급자 전환을 일으키지 않는다. 유효한 미지원 언어 감지 결과는 감지 장애와 구분한다. | +| LP-008 | 빈/중복/알 수 없는 공급자 순서 설정은 초기화 단계에서 명확히 거부한다. 새 구현체 추가 시 기존 선택 코드나 공급자 enum 수정이 필요 없다. | +| LP-009 | 기존 파파고 캐시·번역 데이터 및 분할 번역 동작과 감지 캐시 동시성 승자 반환을 보존한다. | +| LP-010 | 추가 방법과 기능별 순서 설정 예시, 실제 AWS 미구현 상태를 문서화한다. | + +## 보안·오류 처리 + +외부 HTTP 응답과 실행 설정이 신뢰 경계다. 부분 번역·빈 응답은 정상 결과로 저장하지 않는다. +원문, 인증정보, 공급자 응답 본문이 신규 로그나 워커 오류 문자열에 노출되지 않도록 한다. +공급자 식별자·순서 외에 임의 URL을 사용자 요청으로 받지 않는다. 유료 API 테스트는 하지 않는다. + +## 검증 시나리오 + +1. 파파고 단독 기본 설정에서 기존 감지·번역·캐시·분할·커뮤니티 동작이 유지된다. +2. 두 가짜 공급자로 감지/번역 순서 독립 변경, 첫 성공 중단, 실패 전환·전체 실패를 확인한다. +3. 가짜 보조 공급자의 감지/번역 결과가 실제 provider로 DB에 저장되고 다음 요청과 materializer가 재사용한다. +4. 잘못된 설정의 초기화 실패, 원문 정책 가드, 캐시 동시성·revision 회귀를 검증한다. + +## 검증 범위 + +변경 전 관련 focused 테스트 60건과 추가 end-to-end 4건 통과를 확인했다. +구현 후 새 계약 테스트를 포함한 85건 및 부모 런타임·HTTP 검증을 통과했다. 상세 증거는 계획 문서에 기록한다. +전체 테스트는 저장소 정책에 따라 사용자 실행(`./gradlew test`)이며, 운영 DDL·실제 AWS/Papago 계정 호출은 제외한다. +LSP 도구가 제공되지 않으면 Kotlin 컴파일과 ktlint로 보완하고 그 한계를 기록한다. + +## 확정 구현 계약 + +- 기존 `TranslationProvider`와 raw `TranslateResult`는 유지한다. +- `LanguageDetectionProvider`: `providerName: String`, `detect(query: String): String?`. +- `DetectedLanguage`: `detectedLanguage: String`, `providerName: String`. +- `ProviderTranslationResult`: `result: TranslateResult`, `providerName: String`, `providerVersion: String`. +- 기존 `LanguageDetectionCacheService.detectWithCache(query: String): DetectedLanguage?`가 감지 선택·캐시·전환을 담당한다. + 기존 callback overload는 남기지 않고 모든 테스트 fixture도 새 계약에 맞춘다. +- `TranslationCoordinator.findCached(sourceHash, sourceLanguage, targetLanguage): TranslationMemory?`를 + worker와 materializer가 함께 사용한다. 캐시 선택만 통일하며 DB 저장 트랜잭션은 기존 worker에 남긴다. +- `TranslationCoordinator.translate(request: TranslateRequest): ProviderTranslationResult`가 실제 성공 공급자를 반환한다. + 결과 개수 불일치·blank 결과·공급자 예외는 다음 공급자로 전환한다. 부분 결과는 섞지 않고 전체 요청을 넘긴다. +- 새 번역 coordinator의 빈 입력/blank 텍스트/동일 언어/미지원 언어 요청은 고정 메시지의 `IllegalArgumentException`으로 + 외부 호출 전에 거부한다. 기존 Papago raw adapter 가드는 그대로 유지한다. +- 전체 번역 공급자 실패는 원문·원시 예외를 포함하지 않는 `IllegalStateException("all translation providers failed")`이다. + 감지 전체 실패는 null이다. catch는 adapter 호출에만 적용하고 DB 실패를 공급자 실패로 처리하지 않는다. +- `sodalive.language-providers.detection-order`와 `translation-order`는 각각 기본 `papago`이다. + 순서는 재시작/재배포 시 반영한다. 미등록 공급자는 설정 오류로 거부하며 AWS 구현 전에는 `aws`를 활성화하지 않는다. +- 감지 캐시의 정규화/hash 규칙과 `insertIfAbsent` 다음 `findDetectedLanguageForUpdate` 승자 반환을 유지한다. + adapter에는 기존처럼 원래 query를 전달한다. 유효한 ko/en/ja 밖 감지 결과는 공급자 실패로 재해석하지 않는다. +- 공통 `TranslationLanguages`로 서비스 언어 정책만 옮긴다. Papago 분할은 Papago adapter에 유지한다. +- 현재 entity에는 공급자별 cache key가 있으므로 DDL 변경 없이 실제 공급자를 기록한다. + 캐시 두 개가 있으면 설정상 앞선 결과를 선택하며 특정 job의 공급자를 영구 고정하는 새 모델은 만들지 않는다. diff --git a/docs/20261109_감지번역공급자공통화/provider-guide.md b/docs/20261109_감지번역공급자공통화/provider-guide.md new file mode 100644 index 00000000..4acfe4b0 --- /dev/null +++ b/docs/20261109_감지번역공급자공통화/provider-guide.md @@ -0,0 +1,106 @@ +# 언어 감지·번역 공급자 확장 가이드 + +## 현재 실행 구조 + +운영 구현체는 `PapagoLanguageDetectionProvider`와 `PapagoTranslationService`뿐이다. +AWS 감지·번역 adapter, 해당 SDK 및 전용 인증 설정은 추가하지 않았다. 지금 `aws`를 순서에 넣으면 시작에 실패한다. + +감지와 번역은 별도 capability다. 감지만 또는 번역만 제공하는 공급자도 등록할 수 있다. +두 capability에서 같은 `providerName`을 쓰는 것은 가능하지만, 한 capability 안의 이름 중복은 시작 시 거부한다. + +| 경계 | 계약 | +|---|---| +| `LanguageDetectionProvider` | `providerName: String`, `detect(query: String): String?` | +| `LanguageDetectionCacheService` | `detectWithCache(query: String): DetectedLanguage?` | +| `DetectedLanguage` | `detectedLanguage: String`, `providerName: String` | +| `TranslationProvider` | `providerName: String`, `providerVersion: String`, `translate(TranslateRequest): TranslateResult` | +| `TranslationCoordinator` | `findCached(sourceHash, sourceLanguage, targetLanguage): TranslationMemory?`, `translate(TranslateRequest): ProviderTranslationResult` | +| `ProviderTranslationResult` | `result: TranslateResult`, `providerName: String`, `providerVersion: String` | + +인터페이스와 DTO는 `src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/`에 있다. +감지 coordinator는 기존 `content/LanguageDetectionCacheService.kt`이며 별도 vendor가 아니다. +`TranslationCoordinator`도 `TranslationProvider`를 구현하지 않는다. + +## 새 공급자 추가 순서 + +1. 필요한 capability 인터페이스를 구현하고 Spring `@Component`/`@Service` 또는 `@Bean`으로 등록한다. + `providerName`은 capability 내 유일하고 재배포 후에도 안정적인 값이어야 한다. +2. 공급자의 HTTP/SDK 및 인증은 adapter 안에 둔다. 외부 응답 형식과 빈 결과를 검증하고, + 원문·인증정보·응답 본문·예외 cause를 로그로 출력하지 않는다. 번역은 입력별 결과를 같은 순서로 반환한다. +3. 구현한 capability의 설정 순서에만 이름을 넣고 재시작/재배포한다. `@Primary`, selector 분기, enum 등록은 필요 없다. +4. 아래 테스트 경계로 순서·실패 전환·실제 provider 저장·무호출 캐시 재사용·materializer 표시를 검증한다. + +콘텐츠·댓글·커뮤니티 호출자, worker, materializer, scheduler에 공급자 분기를 추가하지 않는다. +기존 두 coordinator는 주입된 구현체 목록과 설정만 사용한다. 공급자마다 다른 분할·요청 크기 제한은 adapter 책임이다. +Papago의 5,000 UTF-16 단위 분할과 서로게이트 쌍 보존은 `PapagoTranslationService`에 남아 있다. +ko/en/ja 서비스 정책은 `TranslationLanguages`에 있으며 공급자를 추가해도 자동 확장하지 않는다. + +## 기능별 독립 설정 + +`LanguageProviderProperties`는 component scan으로 등록되며 생성자 `@Value`로 문자열을 주입받는다. +별도 `@EnableConfigurationProperties` 등록은 필요 없다. 쉼표로 나누고 각 이름의 앞뒤 공백을 제거한다. +기본값은 두 기능 모두 `papago`다. + +```yaml +sodalive: + language-providers: + detection-order: ${LANGUAGE_PROVIDERS_DETECTION_ORDER:papago} + translation-order: ${LANGUAGE_PROVIDERS_TRANSLATION_ORDER:papago} +``` + +다음은 **secondary 구현체를 두 capability에 등록한 뒤에만** 적용할 수 있는 예시다. +현재 `secondary` 구현체는 테스트에만 있다. + +```bash +export LANGUAGE_PROVIDERS_DETECTION_ORDER=secondary,papago +export LANGUAGE_PROVIDERS_TRANSLATION_ORDER=papago,secondary +``` + +감지만 순서를 바꾸어도 번역 순서는 바뀌지 않는다. 반대로 번역만 바꿀 수도 있다. +Spring property를 직접 지정할 때는 `sodalive.language-providers.detection-order`와 +`sodalive.language-providers.translation-order`를 각각 설정한다. +빈 값·빈 항목·중복 이름·미등록 이름·capability 안의 중복 bean 이름표(`providerName`)는 초기화에서 거부한다. +공급자가 번역에만 등록됐다면 그 이름을 감지 순서에 넣을 수 없다. +실행 중 설정 변경은 지원하지 않는다. AWS 구현체 등록 전 `papago,aws`는 사용할 수 없다. + +## 캐시와 오류 계약 + +감지는 정규화/hash/normalization version을 기존과 같이 사용하고 adapter에는 원래 query를 전달한다. +설정된 모든 공급자의 캐시를 순서대로 조회한 뒤, 유효 캐시가 하나도 없을 때만 외부 호출을 시작한다. +저장은 실제 성공 공급자로 `insertIfAbsent`하고 이어지는 locking read의 승자를 반환한다. +ko/en/ja 밖의 유효 감지 결과도 성공이다. 커뮤니티의 기존 지원 언어 gate는 별도로 유지된다. + +번역 worker와 materializer는 동일한 `TranslationCoordinator.findCached`를 사용한다. +앞선 공급자의 캐시가 없어도 뒤 공급자의 유효 캐시가 있으면 외부 호출을 하지 않는다. +여러 공급자 캐시가 있으면 설정 순서상 첫 유효 결과를 선택한다. job을 특정 vendor에 영구 고정하지 않는다. +cache key와 DDL은 바꾸지 않았으며 `providerVersion`은 기록 필드이지 cache key가 아니다. +외부 번역은 worker의 저장 트랜잭션 밖에서 실행하고, 실제 성공 공급자로 저장 직전 재조회한다. +materializer는 외부 번역을 호출하지 않는다. + +adapter 예외·null 감지·빈 감지·번역 결과 개수 불일치·blank 번역 결과는 다음 공급자로 전환한다. +번역은 부분 결과를 섞지 않고 원래 요청 전체를 다음 공급자에게 한 번 넘긴다. 첫 성공 이후에는 중단한다. +저장소 조회·저장 오류는 adapter 실패로 catch하지 않는다. +감지가 모두 실패하면 `null`, 번역이 모두 실패하면 cause 없는 +`IllegalStateException("all translation providers failed")`를 반환하며 기존 job 재시도 경로를 따른다. +빈 목록·blank 텍스트·동일 언어(대소문자 무시)·미지원 번역 언어는 외부 호출 전에 +`IllegalArgumentException("invalid translation request")`로 거부한다. raw Papago의 기존 가드는 유지한다. + +## 검증 경계 + +| 테스트 | 검증 내용 | +|---|---| +| `LanguageProviderSpringConfigurationTest` | 실제 `@Value` binding, 두 test bean, 기능별 독립 순서, 기본값과 잘못된 설정의 시작 실패 | +| `LanguageProviderPropertiesTest` | 순서 검증 및 선택되지 않은 bean을 포함한 capability 내 중복 이름 거부 | +| `LanguageDetectionProviderTest` / `TranslationCoordinatorTest` | 전체 요청 폴백, 첫 성공 중단, cache-first, 안전한 전체 실패, DB 오류 전파 | +| `PapagoLanguageDetectionProviderTest` / `PapagoTranslationServiceTest` | 제어된 HTTP form/header/response와 기존 17개 chunk 회귀 | +| `LanguageProviderCacheEndToEndTest` | 실제 H2의 secondary 감지/번역 귀속, providerVersion, 두 번째 요청 무호출, worker와 materializer의 실제 표시 | + +H2 버전은 MySQL `FOR UPDATE SKIP LOCKED`를 지원하지 않으므로 새 H2 테스트는 claim 조회 한 곳만 +test spy로 대체한다. 나머지 repository 저장·별도 트랜잭션 재조회·materialization은 실제 H2다. +이 테스트가 운영 MySQL claim 동시성을 검증한다는 뜻은 아니다. 기존 감지 cache 경합·revision 테스트는 별도로 유지한다. + +```bash +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.i18n.translation.*' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' +``` + +전체 실행 명령, 결과와 미검증 범위는 [plan-task.md](plan-task.md)의 실행 증거를 따른다. diff --git a/docs/20261109_감지번역공급자공통화/reviews/phase-1-review.md b/docs/20261109_감지번역공급자공통화/reviews/phase-1-review.md new file mode 100644 index 00000000..e18d053e --- /dev/null +++ b/docs/20261109_감지번역공급자공통화/reviews/phase-1-review.md @@ -0,0 +1,45 @@ +# 감지·번역 공급자 공통화 최종 리뷰 + +| 항목 | 내용 | +|---|---| +| 일자 | 2026-11-09 | +| 범위 | P1-T1/P1-T2와 전체 작업 트리 변경, 신규 미추적 파일 포함 | +| 기준 | [PRD](../prd.md) LP-001~010, [구현 계획](../plan-task.md) | +| 리뷰어 | 독립 읽기 전용 gate reviewer | +| 판정 | **APPROVE** | + +## 발견 사항 + +확정 발견 사항 없음. 추가 회귀 수정 Task로 전환할 항목 없음. + +## 수용 기준 + +| 기준 | 확인 내용 | +|---|---| +| LP-001/002/008 | 감지·번역별 인터페이스와 순서 설정, 빈/중복/미등록 이름 시작 실패, enum/분기 없이 bean 확장 | +| LP-003/004/007 | 한 번씩 순차 호출, 첫 성공 중단, 전체 요청 폴백, 입력 guard, 안전한 전체 실패 | +| LP-005/006 | 실제 provider/version 저장, 모든 활성 provider의 cache-first, worker/materializer의 공통 lookup | +| LP-009 | 기존 캐시 데이터·canonical winner·트랜잭션·revision·afterCommit·워커 재시도·Papago 분할 보존 | +| LP-010 | 실제 코드와 일치하는 확장 가이드, 기본 papago, production AWS 미구현 명시 | + +## 품질·보안 + +- catch는 adapter 호출에만 적용하며 DB 오류는 전파한다. +- 원문·인증정보·공급자 응답·원시 vendor 오류를 신규 로그에 기록하지 않는다. +- 기존 latch·경합·stale revision assertion을 삭제/완화하지 않았다. +- 번역 worker의 오류 기대값 변경은 고정 안전 메시지 계약에 따른 것이다. +- 기존 Papago 분할/HTTP 테스트 17건을 수정하지 않았다. + +## 검증 근거 + +- reviewer가 XML 15개를 직접 확인: 85건, failures/errors/skipped 0. +- reviewer `git diff --check`: exit 0. +- 부모 실행 증거: fresh filtered `test --rerun` 성공, ktlintCheck/bootJar 확인. +- 부모 독립 Spring runtime: 감지/번역 순서 각각 교체, fallback 귀속, 캐시 무호출, 전체 실패 및 잘못된 설정 거부. +- 부모 loopback HTTP: 실제 Papago 감지 adapter의 form/header/원문 보존과 성공/blank/500 응답 처리. +- 리뷰는 읽기 전용이며 추가 에이전트, 실제 API, 테스트/빌드 재실행, 파일 변경을 하지 않았다. + +## 남은 검증 한계 + +실제 유료 API·운영 MySQL·전체 테스트·LSP는 미검증이다. H2의 MySQL `SKIP LOCKED` claim SQL 한 곳은 +테스트 대역이므로 운영 claim 동시성을 검증했다고 주장하지 않는다. 이 한계는 이번 범위의 승인과 구분한다.