diff --git a/docs/20261109_감지번역공급자공통화/plan-task.md b/docs/20261109_감지번역공급자공통화/plan-task.md index 9811c43e..1843a6db 100644 --- a/docs/20261109_감지번역공급자공통화/plan-task.md +++ b/docs/20261109_감지번역공급자공통화/plan-task.md @@ -3,8 +3,8 @@ | 항목 | 내용 | |---|---| | 작성일 | 2026-11-09 | -| 상태 | Phase 1·2 구현·검증·리뷰·리소스 정리 완료 | -| 요구사항 | [prd.md](prd.md), LP-001~012 및 확정 구현 계약 | +| 상태 | Phase 1~3 구현·검증·독립 리뷰·리소스 정리 완료 | +| 요구사항 | [prd.md](prd.md), LP-001~015 및 최종 Phase 계약 | | 활성 Goal | 없음 | ## 목표와 제약 @@ -496,3 +496,222 @@ parent `provider-order-code.RRCyJ1`, 부모 QA의 형제 경로, 기존 공유 d `provider-order-code.RRCyJ1`(driver/class/라이브러리/로그)만 제거했다. `test ! -e` exit 0. 출력: `CLEANUP PASS: owned provider-order-code.RRCyJ1 removed; shared caches untouched`. 필요한 증거는 이 문서에 이관했으며 임시 노트도 삭제한다. 다른 작업의 리소스는 건드리지 않았다. + +### Phase 3: 공급자 enum과 DB code 경계 + +- [x] **Task 3.1: 식별자 enum 전환 (P3-T1)** + +**Objective:** LP-013~015에 따라 코드 내부 공급자 식별은 enum, 저장 값은 기존 안정 문자열로 유지한다. +**시작 조건:** 사용자 enum 구현 승인, Phase 2 완료, 변경 전 focused baseline 88건 확인. +**완료 증거:** enum 타입 계약 RED/GREEN, 기존 papago 데이터 재사용 및 code 저장 호환, focused 회귀·lint·bootJar. +**범위 밖:** AWS 실제 adapter/SDK, DDL/entity/repository 타입 변경, 공급자 정책·캐시 정책·언어 정책·분할·인증 변경. + +**Files (src/main/kotlin/kr/co/vividnext/sodalive/ 기준):** +- Add: `i18n/translation/LanguageProviderId.kt`. +- Modify: 같은 패키지 `LanguageProviderOrder.kt`, `LanguageProviderProperties.kt`, `TranslationProvider.kt`, + `LanguageDetectionProvider.kt`, `DetectedLanguage.kt`, `ProviderTranslationResult.kt`, 두 Papago adapter, + `TranslationCoordinator.kt`, `TranslationJobWorker.kt`; `content/LanguageDetectionCacheService.kt`. +- Update: 직접 영향 테스트의 fake provider/순서/결과 타입. DB 문자열 기대값은 유지한다. +- Update: 현재 `provider-guide.md`와 이 PRD/plan. 이전 완료 증거/리뷰는 보존한다. + +**계약:** PRD Phase 3 그대로 적용한다. `orderedProviders`는 enum 목록과 `(T) -> LanguageProviderId`로 +선택한다. 존재하지 않는 문자열/blank ID 테스트는 정적 타입으로 대체되며 AWS ID에 해당 구현체가 없으면 거부한다. +테스트 first/second/secondary 공급자는 PAPAGO/AWS로 옮기고, 번역 본문 fixture의 같은 문자열은 바꾸지 않는다. + +- [x] **RED:** 컴파일 가능한 기존 인터페이스/결과에 대한 runtime getter 반환 타입 assertion으로 새 enum 식별 계약의 실패를 재현한다. missing symbol 컴파일 오류를 RED로 보고하지 않는다. +- [x] **GREEN:** enum·providerId·typed 순서 및 DB 경계 `.code` 변환을 한 단위로 구현한다. +- [x] **GREEN 확인:** enum code 고정값, 원래 papago 캐시 재사용, 실제 DB의 papago/aws 문자열 저장·조회, 결과 enum 귀속이 통과한다. +- [x] **REFACTOR:** 관련 fixture만 정리하고 빈 목록/중복/미등록 AWS 검증·감지/번역 순서·실패 전환·경합·분할 회귀를 실행한다. + +**검증 명령:** Phase 2의 결합 focused 명령(번역 package·감지 cache·커뮤니티 감지/경합·번역 service·end-to-end)을 +그대로 실행하며 새 enum 테스트도 번역 package 필터에 포함한다. `--no-daemon --offline`, timeout600000 사용. +`ktlintCheck bootJar`를 함께 검증한다. 전체 테스트는 사용자 실행 `./gradlew test`로 남긴다. + +- [x] **Phase 3 Gate (P3-GATE): typed 실행·저장 호환·리뷰** + +**Objective:** production 목록/구현체/결과가 enum이며 DB 값은 lowercase code임을 독립 검증한다. +**시작 조건:** P3-T1 통과. +**완료 증거:** 부모 Java/Spring enum driver, H2 DB 호환 테스트 대조, 한 명의 독립 리뷰 승인, 소유 리소스 정리. +**TDD 예외:** 독립 실행 확인·리뷰이며 새 구현을 추가하지 않는다. +**범위 밖:** 실제 유료 API·운영 MySQL·전체 테스트. LSP 미제공은 컴파일/lint 보완과 함께 명시한다. + +### P3 초기 증거 + +- 변경 전 깨끗한 작업 트리를 확인했고 Phase 2 결합 focused baseline은 exit 0, `BUILD SUCCESSFUL in 45s`였다. +- SQL/entity의 provider String과 기존 code 값을 유지하는 계약을 확정했다. enum은 실제 AWS 연동을 의미하지 않는다. + +### P3-T1 실행 증거 (2026-11-09) + +부모가 직전에 확인한 baseline 88건(45초)을 재실행하지 않았다. 승인된 최종 Phase 3 계약으로 기존 작업을 이어갔다. +PRD는 수정하지 않았으며 Phase 1·2 실행 이력과 기존 리뷰도 보존했다. P3-T1만 완료하고 P3-GATE는 부모가 판정한다. + +**실행 환경:** parent를 read 및 `ls -ld`로 확인하고 `mktemp -d "$TMPDIR/phase3.XXXXXX"`로 +이 구현자만 소유하는 child를 생성했다. 생성 후 모든 shell 명령에서 아래 세 환경변수를 지정했다. +모든 Gradle 실행은 `--no-daemon --offline`, tool timeout `600000ms`다. + +```bash +export TMPDIR=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/provider-id-enum.jLt5Ki/phase3.eX59R8 +export NODE_COMPILE_CACHE="$TMPDIR" BUN_RUNTIME_TRANSPILER_CACHE_PATH="$TMPDIR" +``` + +**RED (1건, failures 1, errors/skipped 0, exit 1, 19초):** production 변경 전에 `LanguageProviderIdTest`를 추가했다. +reflection의 `"getProviderId"`와 반환 타입의 전체 이름 문자열을 사용하므로 구 코드에서도 정상 컴파일된다. +JUnit `assertAll`에서 두 raw interface 및 두 성공 결과의 getter가 모두 없어 expected enum type / actual null인 +`AssertionFailedError` 4개가 발생했다. missing symbol·컴파일 오류·예외 기반 실패가 아닌 런타임 assertion RED다. + +```bash +./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderIdTest' +``` + +**GREEN:** enum·인터페이스·adapter·결과·순서·매칭·직접 영향 fixture를 한 단위로 전환한 뒤 결합 회귀를 한 번 실행했다. +`PAPAGO("papago")`, `AWS("aws")`만 정의했고 production 기본 목록은 두 기능 모두 PAPAGO 하나다. +`providerVersion`은 String 그대로이며 `providerName` alias/호환 wrapper와 test-only enum은 없다. +`.code` 사용은 cache 조회/insert/locking read 3곳, coordinator 조회 1곳, worker 조회/저장 2곳의 DB 경계뿐이다. +production 식별자 리터럴 `"papago"`는 enum 선언 한 곳에만 남았다. HTTP URL·인증 key·과거 env 무시 문자열은 유지했다. + +**보존/저장 검증:** 아래 assertion들은 기대값을 enum에서 계산하지 않는다. + +- 기존 Papago 감지 cache hit는 `"papago"` fixture를 재사용하고 외부 호출 0회를 검증한다. + 기존 H2 최초 감지 경합 2건은 저장값 `"papago"`, 승자 반환, target·job 수와 revision을 그대로 검증한다. +- H2 가짜 보조 공급자는 AWS enum이다. 감지/번역을 별도 트랜잭션에서 `"aws"`로 재조회하고 실제 provider를 확인한다. + 번역 memory의 실제 version은 `"AWS-v1"`, 본문과 materializer 표시값은 `"AWS:0"`이며 재사용 후 호출 수는 각각 1회다. + 이는 테스트 adapter가 만드는 version/본문 fixture이고 DB provider code `"aws"`와 별도다. +- 독립 감지/번역 순서·첫 성공 중단·전체 요청 폴백·개수 불일치/blank·전체 실패·DB 오류 전파 assertion을 유지했다. + 기존 Papago HTTP/chunk 17건의 `first`/`second` 본문 fixture는 변경하지 않았다. +- 빈 문자열 ID만 enum으로 표현할 수 없어 제거했다. 빈 목록·중복 순서·미선택 구현체의 중복 ID 검증은 유지했다. + 기존 unknown 문자열 사례는 AWS enum 순서와 해당 구현체 부재로 바꾸고, 실제 Spring에서 감지/번역 각각 시작 실패를 확인했다. + +**최종 결합 focused + lint/산출물 (exit 0, 2분 45초):** `:test`, `:ktlintCheck`, `:bootJar`가 모두 실제 실행됐다. +`BUILD SUCCESSFUL`을 확인했으며 XML 16개를 직접 집계했다. 불필요한 baseline/중간 전체 컴파일 반복은 하지 않았다. + +```bash +./gradlew --no-daemon --offline test \ + --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로 확인한 범위 | 결과 | +|---|---| +| 번역 package + 감지 cache | 57/57 | +| 커뮤니티 감지 + cache 경합 | 14/14 (12 + 2) | +| 커뮤니티 번역 service + 기존 end-to-end | 20/20 (16 + 4) | +| 합계 | 91/91, failures/errors/skipped 모두 0 | + +신규 enum 테스트 3건(런타임 타입·code·production 기본값/실제 adapter ID)이 baseline 대비 증가분이다. +기존 unrelated Kotlin 경고 7개(불필요한 `!!` 4, unchecked cast 3)와 Gradle 9 deprecation 경고는 남아 있다. +제공된 LSP/diagnostics 도구는 없으며 Kotlin 컴파일·ktlint로 보완했다. 성공한 LSP 진단으로 보고하지 않는다. + +### Phase 3 부모 Java QA용 실제 생성자/Getter + +`javap -classpath build/classes/kotlin/main`으로 다음 바인딩을 확인했다(exit 0). +패키지는 content service를 제외하면 `kr.co.vividnext.sodalive.i18n.translation`이다. +Java enum 자체가 자동 생성하는 `valueOf`는 존재하지만 production의 역변환이나 저장에 사용하지 않는다. + +```text +LanguageProviderId.PAPAGO / AWS; getCode(): String +LanguageProviderProperties(List, List) +LanguageProviderProperties() // @Autowired, production object 목록으로 위임 +getDetectionOrder(): List; getTranslationOrder(): List +LanguageProviderOrder.INSTANCE.getDetectionOrder/getTranslationOrder(): List +orderedProviders(List, List, Function1) +LanguageDetectionProvider.getProviderId(): LanguageProviderId; detect(String): String? +TranslationProvider.getProviderId(): LanguageProviderId; getProviderVersion(): String +DetectedLanguage(String detectedLanguage, LanguageProviderId providerId); getProviderId(): LanguageProviderId +ProviderTranslationResult(TranslateResult result, LanguageProviderId providerId, String providerVersion) +ProviderTranslationResult.getProviderId(): LanguageProviderId; getProviderVersion(): String +PapagoLanguageDetectionProvider(String clientId, String clientSecret); getProviderId(): LanguageProviderId +PapagoTranslationService(String clientId, String clientSecret); getProviderId(): LanguageProviderId +TranslationCoordinator(List, LanguageProviderProperties, TranslationMemoryRepository) +LanguageDetectionCacheService(LanguageDetectionResultRepository, List, + LanguageProviderProperties) +``` + +### P3-T1 변경 파일 전체 (23개) + +production prefix: `src/main/kotlin/kr/co/vividnext/sodalive/` (12개). + +```text +A i18n/translation/LanguageProviderId.kt +M i18n/translation/LanguageProviderOrder.kt +M i18n/translation/LanguageProviderProperties.kt +M i18n/translation/TranslationProvider.kt +M i18n/translation/LanguageDetectionProvider.kt +M i18n/translation/DetectedLanguage.kt +M i18n/translation/ProviderTranslationResult.kt +M i18n/translation/PapagoLanguageDetectionProvider.kt +M i18n/translation/PapagoTranslationService.kt +M i18n/translation/TranslationCoordinator.kt +M i18n/translation/TranslationJobWorker.kt +M content/LanguageDetectionCacheService.kt +``` + +test prefix: `src/test/kotlin/kr/co/vividnext/sodalive/` (9개). + +```text +A i18n/translation/LanguageProviderIdTest.kt +M i18n/translation/LanguageProviderPropertiesTest.kt +M i18n/translation/LanguageProviderSpringConfigurationTest.kt +M i18n/translation/LanguageProviderCacheEndToEndTest.kt +M i18n/translation/LanguageDetectionProviderTest.kt +M i18n/translation/TranslationCoordinatorTest.kt +M i18n/translation/TranslationJobWorkerTest.kt +M content/LanguageDetectionCacheServiceTest.kt +M content/CreatorCommunityLanguageDetectTest.kt +``` + +문서는 현재 `provider-guide.md`와 이 `plan-task.md`만 변경했다(2개). +부모가 사전 갱신한 `prd.md`는 수정하지 않았다. entity/repository/DDL·설정·domain service·실제 AWS/SDK는 변경하지 않았다. +`CreatorCommunityLanguageDetectionCacheConcurrencyTest`와 community 번역 service/end-to-end는 수정 없이 회귀 실행했다. + +**한계/부모 경계:** H2의 기존 MySQL claim SQL test spy는 변경하지 않았다. 실제 DB 저장·재조회·표시는 검증했지만 +운영 MySQL claim 동시성은 새로 증명하지 않는다. 유료 API·운영 DB·전체 테스트는 실행하지 않았다. +무필터 test/check/build 대신 요청된 필터와 bootJar만 실행했고 사용자 전체 검증 명령은 `./gradlew test`다. +Java/Spring typed driver와 한 명의 독립 reviewer, P3-GATE 판정은 부모가 맡는다. +이 구현자는 staging/commit/push/worktree/하위 에이전트 작업을 하지 않았다. + +### P3-T1 소유 임시 자원 정리 + +- 정리 대상은 생성 시 확인한 child `provider-id-enum.jLt5Ki/phase3.eX59R8` 하나다. + RED/GREEN 로그의 명령·횟수·실패 원인·결과를 이 문서에 이관한 뒤 삭제한다. +- Gradle/test 실행 종료 후 `lsof -nP +D "$TMPDIR"` 출력이 없었다. Java process 목록에도 이 child를 사용하는 + Gradle/test/compiler 프로세스가 없다. 기존 shared Gradle daemon(PID 85602)은 종료하거나 변경하지 않는다. +- 정확한 child 경로와 존재를 `test`로 대조한 다음 이 child만 삭제했다. parent와 공유 cache는 유지했다. +- 완료 영수증: `2026-11-09 19:02:10 KST`, lsof exit 1(열린 파일 없음), `test ! -e "$TMPDIR"` 및 + parent `ls -ld` exit 0. 출력은 `CLEANUP PASS: owned phase3.eX59R8 removed; parent and shared caches untouched`다. + 로그/일회성 임시 파일만 제거했으며 필요한 증거는 위에 보존했다. + +### P3-GATE 부모 독립 검증 + +- 부모가 신규 enum/테스트, 모든 production/test diff와 현재 가이드를 직접 대조했다. + production의 `"papago"` 식별자 리터럴은 enum 선언 1곳, `.code`는 기존 DB 접점 6곳이다. + `providerName` alias와 providerId의 `.name`/`.ordinal`/`valueOf` 사용은 발견되지 않았다. +- Phase 3 결합 focused 명령에 `test --rerun`을 적용하고 `ktlintCheck bootJar`를 함께 실행했다. + exit 0, `BUILD SUCCESSFUL in 52s`, `:test` 실제 실행, 나머지 현재 컴파일/lint/build 18개 작업은 UP-TO-DATE였다. + 부모가 fresh XML 16개를 직접 확인: 91건, failures/errors/skipped 0. +- 별도 Java 17 `EnumProviderQa`에서 실제 컴파일 클래스와 Spring context를 실행했다. + raw interface·결과 4개 runtime getter의 enum 반환 타입, 실제 두 Papago adapter의 ID와 code 고정값, + PAPAGO-only 기본값을 확인했다. +- 메모리 repository 대역에 기존 `"papago"` 캐시를 미리 넣고 provider 호출 0회로 재사용함을 확인했다. + 가짜 PAPAGO 실패 → AWS 성공에서 결과는 AWS enum이며 감지 저장/조회 인자는 소문자 `"aws"`였다. + 모든 repository provider 인자가 `"papago"` 또는 `"aws"`임을 독립 문자열 기대값으로 검사했다. + AWS enum만 순서에 있고 해당 구현체가 없으면 Spring 초기화가 실패했다. +- driver 출력: `QA PASS: enum runtime and persistence-boundary compatibility; all Spring contexts closed; no API calls`. + exit 0이고 모든 context를 종료했다. 이 driver의 repository는 대역이며 실제 DB 저장 증거는 H2 테스트와 구분한다. +- 실제 H2 AWS code 저장/재조회·worker/materializer 표시 및 기존 Papago 경합 테스트도 위 fresh 명령에 포함됐다. + 전체 테스트·운영 MySQL·유료 API·LSP 미검증 범위는 기존대로 유지한다. +- 독립 Phase 3 gate 리뷰: **APPROVE**, 확정 발견 사항·차단 이슈 없음. + [리뷰 보고서](reviews/phase-3-review.md). XML 16개와 모든 변경을 대조했고 `git diff --check`도 통과했다. +- 인터페이스·구현체·결과·호출자·fixture의 타입 변경은 분리하면 컴파일되지 않으므로 코드/테스트를 한 원자적 커밋으로 묶는다. + 요구사항·가이드·검증·리뷰 기록은 별도 문서 커밋으로 남긴다. push하지 않는다. +- 코드·테스트 커밋: `ec890047`. 변경 Kotlin hook ktlint와 메시지 사전/사후 검증이 통과했다. + 코드 커밋 후 작업 트리에는 이 작업의 문서 변경만 남았음을 확인했다. +- 부모 최종 정리: 모든 실행이 끝난 뒤 `lsof -nP +D` 출력이 없었다. 생성·내용 확인한 소유 + `provider-id-enum.jLt5Ki`만 삭제했고 `test ! -e` exit 0을 확인했다. + 출력: `CLEANUP PASS: owned provider-id-enum.jLt5Ki removed; shared caches untouched`. + driver/class/추출 라이브러리/로그를 제거했으며 필요한 증거는 이 문서에 남겼다. + 임시 노트도 이관 후 삭제한다. 공유 cache·다른 작업의 프로세스/파일은 변경하지 않았다. diff --git a/docs/20261109_감지번역공급자공통화/prd.md b/docs/20261109_감지번역공급자공통화/prd.md index 3eb02044..294495cb 100644 --- a/docs/20261109_감지번역공급자공통화/prd.md +++ b/docs/20261109_감지번역공급자공통화/prd.md @@ -3,7 +3,7 @@ | 항목 | 내용 | |---|---| | 작성일 | 2026-11-09 | -| 상태 | Phase 1·2 구현 및 기능 범위 검증·독립 리뷰 완료 | +| 상태 | Phase 1~3 구현·기능 범위 검증·독립 리뷰 완료 | | 승인 범위 | 감지·번역 별도 인터페이스, 공통 공급자 선택·실패 전환, 기능별 순서 설정 | | 계획 | [plan-task.md](plan-task.md) | @@ -32,11 +32,14 @@ | LP-005 | 캐시는 실제 성공 공급자 이름으로 저장한다. AWS 역할의 가짜 공급자 성공을 papago 이름으로 저장하지 않는다. | | LP-006 | 설정된 공급자들의 유효 캐시를 우선 재사용한다. 새 외부 호출의 순서는 별도 설정 순서를 따른다. 워커와 결과 조립의 캐시 선택 정책이 같다. | | LP-007 | 빈 원문·동일 언어·서비스 미지원 번역 언어는 불필요한 공급자 전환을 일으키지 않는다. 유효한 미지원 언어 감지 결과는 감지 장애와 구분한다. | -| LP-008 | 빈/중복/알 수 없는 공급자 순서 설정은 초기화 단계에서 명확히 거부한다. 새 구현체 추가 시 기존 선택 코드나 공급자 enum 수정이 필요 없다. | +| LP-008 | 빈/중복/미등록 구현체의 공급자 순서는 초기화 단계에서 거부한다. 선택 코드에 공급자 분기를 추가하지 않는다. Phase 3부터 새 공급자는 enum 항목을 추가한다. | | LP-009 | 기존 파파고 캐시·번역 데이터 및 분할 번역 동작과 감지 캐시 동시성 승자 반환을 보존한다. | | LP-010 | 추가 방법과 기능별 순서 설정 예시, 실제 AWS 미구현 상태를 문서화한다. | | LP-011 | 공급자 순서는 `LanguageProviderOrder`의 독립 `val List` 두 개로 지정한다. 기본은 각각 `listOf("papago")`이다. | | LP-012 | 우선순위 환경변수·YAML·문자열 파싱을 제거한다. 기존 우선순위 property/env 값이 있어도 production 순서를 바꾸지 않는다. 인증정보 환경변수는 유지한다. | +| LP-013 | 순서 목록·감지/번역 인터페이스·구현체·성공 결과·매칭의 식별자는 `LanguageProviderId` enum으로 통일한다. | +| LP-014 | DB entity/repository의 provider는 String으로 유지하며 `.code`로만 변환한다. 기존 `"papago"` 데이터·캐시 키·중복 방지 계약을 유지한다. | +| LP-015 | enum은 `PAPAGO("papago")`, `AWS("aws")`이며 AWS 실제 구현은 추가하지 않는다. 기본 순서는 둘 다 PAPAGO 하나이고 미구현 AWS 선택은 거부한다. | ## 보안·오류 처리 @@ -96,3 +99,20 @@ LSP 도구가 제공되지 않으면 Kotlin 컴파일과 ktlint로 보완하고 - 테스트는 전역 목록을 바꾸지 않고 테스트 전용 bean에 typed list를 주입하여 독립 순서와 invalid order를 검증한다. - 과거 우선순위 environment/property override는 무시한다. 다른 환경변수나 Spring 설정 체계는 변경하지 않는다. - 실패 전환·공급자 검증·캐시·DB·인증·AWS 미구현 정책은 Phase 1을 그대로 유지한다. + +## Phase 3 결정 정정 — 2026-11-09 + +사용자는 공급자 enum 도입과 식별자 용도의 papago 리터럴 변경을 승인했다. 타입 관련 항목은 이 계약이 앞선 Phase보다 우선한다. + +- `LanguageProviderId(val code: String)`에 `PAPAGO("papago")`, `AWS("aws")`를 정의한다. +- `LanguageProviderOrder`, `LanguageProviderProperties`의 두 순서는 `List`이다. + 기본 목록은 각각 `listOf(LanguageProviderId.PAPAGO)`이며 코드에서만 순서를 변경한다. +- 두 공급자 인터페이스, 두 실제 Papago adapter, `DetectedLanguage`, `ProviderTranslationResult`는 + `providerName: String` 대신 `providerId: LanguageProviderId`를 사용한다. 버전은 기존 String을 유지한다. +- 선택/검증/coordinator는 enum으로 매칭한다. DB 조회/저장 접점에서만 `.code`로 변환한다. + entity와 repository, DDL은 변경하지 않는다. `.name`, `.ordinal`, `valueOf` 저장/변환과 불필요한 fromCode는 추가하지 않는다. +- API URL·인증 설정 키·원문/번역 fixture·DB 문자열 기대값·과거 환경변수 무시 테스트는 enum 전환 대상이 아니다. +- 기존 DB 값 호환 테스트는 기대값을 enum에서 계산하지 않고 독립 문자열 `"papago"`로 검증한다. + 가짜 보조 공급자는 AWS enum과 저장값 `"aws"`를 사용하며 test-only enum 항목을 추가하지 않는다. +- 빈 문자열 ID는 타입으로 표현할 수 없어진다. 빈 목록·중복 순서·중복 구현체·해당 capability 구현체 부재 검증은 유지한다. +- 새로운 공급자 추가 시 enum 항목·구현체·코드 순서 목록을 추가한다. 도메인 호출자와 선택 알고리즘은 변경하지 않는다. diff --git a/docs/20261109_감지번역공급자공통화/provider-guide.md b/docs/20261109_감지번역공급자공통화/provider-guide.md index 7370202d..89aaacd7 100644 --- a/docs/20261109_감지번역공급자공통화/provider-guide.md +++ b/docs/20261109_감지번역공급자공통화/provider-guide.md @@ -3,19 +3,19 @@ ## 현재 실행 구조 운영 구현체는 `PapagoLanguageDetectionProvider`와 `PapagoTranslationService`뿐이다. -AWS 감지·번역 adapter, 해당 SDK 및 전용 인증 설정은 추가하지 않았다. 지금 `aws`를 순서에 넣으면 시작에 실패한다. +AWS 감지·번역 adapter, 해당 SDK 및 전용 인증 설정은 추가하지 않았다. 지금 `LanguageProviderId.AWS`를 순서에 넣으면 시작에 실패한다. 감지와 번역은 별도 capability다. 감지만 또는 번역만 제공하는 공급자도 등록할 수 있다. -두 capability에서 같은 `providerName`을 쓰는 것은 가능하지만, 한 capability 안의 이름 중복은 시작 시 거부한다. +두 capability에서 같은 `providerId`를 쓰는 것은 가능하지만, 한 capability 안의 ID 중복은 시작 시 거부한다. | 경계 | 계약 | |---|---| -| `LanguageDetectionProvider` | `providerName: String`, `detect(query: String): String?` | +| `LanguageDetectionProvider` | `providerId: LanguageProviderId`, `detect(query: String): String?` | | `LanguageDetectionCacheService` | `detectWithCache(query: String): DetectedLanguage?` | -| `DetectedLanguage` | `detectedLanguage: String`, `providerName: String` | -| `TranslationProvider` | `providerName: String`, `providerVersion: String`, `translate(TranslateRequest): TranslateResult` | +| `DetectedLanguage` | `detectedLanguage: String`, `providerId: LanguageProviderId` | +| `TranslationProvider` | `providerId: LanguageProviderId`, `providerVersion: String`, `translate(TranslateRequest): TranslateResult` | | `TranslationCoordinator` | `findCached(sourceHash, sourceLanguage, targetLanguage): TranslationMemory?`, `translate(TranslateRequest): ProviderTranslationResult` | -| `ProviderTranslationResult` | `result: TranslateResult`, `providerName: String`, `providerVersion: String` | +| `ProviderTranslationResult` | `result: TranslateResult`, `providerId: LanguageProviderId`, `providerVersion: String` | 인터페이스와 DTO는 `src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/`에 있다. 감지 coordinator는 기존 `content/LanguageDetectionCacheService.kt`이며 별도 vendor가 아니다. @@ -23,12 +23,14 @@ AWS 감지·번역 adapter, 해당 SDK 및 전용 인증 설정은 추가하지 ## 새 공급자 추가 순서 -1. 필요한 capability 인터페이스를 구현하고 Spring `@Component`/`@Service` 또는 `@Bean`으로 등록한다. - `providerName`은 capability 내 유일하고 재배포 후에도 안정적인 값이어야 한다. -2. 공급자의 HTTP/SDK 및 인증은 adapter 안에 둔다. 외부 응답 형식과 빈 결과를 검증하고, +1. `LanguageProviderId`에 공급자 enum 항목과 재배포 후에도 안정적인 DB `code`를 추가한다. + 현재 항목은 `PAPAGO("papago")`, `AWS("aws")`다. AWS는 이미 ID가 있으므로 실제 adapter만 별도로 구현한다. +2. 필요한 capability 인터페이스를 구현하고 Spring `@Component`/`@Service` 또는 `@Bean`으로 등록한다. + 예를 들어 Papago adapter의 식별자는 `override val providerId = LanguageProviderId.PAPAGO`다. +3. 공급자의 HTTP/SDK 및 인증은 adapter 안에 둔다. 외부 응답 형식과 빈 결과를 검증하고, 원문·인증정보·응답 본문·예외 cause를 로그로 출력하지 않는다. 번역은 입력별 결과를 같은 순서로 반환한다. -3. `LanguageProviderOrder.kt`에서 구현한 capability의 목록에만 이름을 넣고 빌드·배포한다. `@Primary`, selector 분기, enum 등록은 필요 없다. -4. 아래 테스트 경계로 순서·실패 전환·실제 provider 저장·무호출 캐시 재사용·materializer 표시를 검증한다. +4. `LanguageProviderOrder.kt`에서 구현한 capability의 목록에만 enum을 넣고 빌드·배포한다. `@Primary`나 selector 분기는 필요 없다. +5. 아래 테스트 경계로 순서·실패 전환·실제 provider 저장·무호출 캐시 재사용·materializer 표시를 검증한다. 콘텐츠·댓글·커뮤니티 호출자, worker, materializer, scheduler에 공급자 분기를 추가하지 않는다. 기존 두 coordinator는 주입된 구현체 목록과 설정만 사용한다. 공급자마다 다른 분할·요청 크기 제한은 adapter 책임이다. @@ -39,33 +41,42 @@ ko/en/ja 서비스 정책은 `TranslationLanguages`에 있으며 공급자를 production 순서의 유일한 편집 지점은 `src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderOrder.kt`다. -목록의 왼쪽부터 사용하며 기본값은 두 기능 모두 `listOf("papago")`다. +목록의 왼쪽부터 사용하며 기본값은 두 기능 모두 `listOf(LanguageProviderId.PAPAGO)`다. ```kotlin object LanguageProviderOrder { - val detectionOrder = listOf("papago") - val translationOrder = listOf("papago") + val detectionOrder = listOf(LanguageProviderId.PAPAGO) + val translationOrder = listOf(LanguageProviderId.PAPAGO) } ``` Kotlin 목록은 `const val`이 아닌 `val`이다. `LanguageProviderProperties`는 기존 주입 wrapper로 유지하며, Spring은 `@Autowired` 기본 생성자로 위 목록을 읽는다. 문자열 파싱은 하지 않는다. -테스트에서는 전역 목록을 변경하지 않고 `LanguageProviderProperties(List, List)`로 만든 테스트 bean을 주입한다. +테스트에서는 전역 목록을 변경하지 않고 +`LanguageProviderProperties(List, List)`로 만든 테스트 bean을 주입한다. -다음은 **secondary 구현체를 두 capability에 등록한 뒤에만** 적용할 수 있는 예시다. -현재 `secondary` 구현체는 테스트에만 있다. +다음은 **실제 AWS 구현체를 두 capability에 등록한 뒤에만** 적용할 수 있는 예시다. +현재 AWS ID의 가짜 구현체는 테스트에만 있다. ```kotlin -val detectionOrder = listOf("secondary", "papago") -val translationOrder = listOf("papago", "secondary") +val detectionOrder = listOf(LanguageProviderId.AWS, LanguageProviderId.PAPAGO) +val translationOrder = listOf(LanguageProviderId.PAPAGO, LanguageProviderId.AWS) ``` 감지만 순서를 바꾸어도 번역 순서는 바뀌지 않는다. 반대로 번역만 바꿀 수도 있다. 변경 후 빌드·배포해야 반영된다. 과거 우선순위 Spring property·환경변수·YAML 값이 남아 있어도 순서는 바뀌지 않는다. 공급자 인증정보는 계속 환경변수로 주입하며 이 코드 목록에 넣지 않는다. -빈 값·빈 항목·중복 이름·미등록 이름·capability 안의 중복 bean 이름표(`providerName`)는 초기화에서 거부한다. -공급자가 번역에만 등록됐다면 그 이름을 감지 순서에 넣을 수 없다. -실행 중 설정 변경은 지원하지 않는다. AWS 구현체 등록 전 `listOf("papago", "aws")`는 사용할 수 없다. +빈 목록·중복 ID·해당 capability의 구현체 부재·capability 안의 중복 구현체 ID는 초기화에서 거부한다. +빈 문자열 ID는 enum 타입으로 표현할 수 없다. 공급자가 번역에만 등록됐다면 그 ID를 감지 순서에 넣을 수 없다. +실행 중 설정 변경은 지원하지 않는다. AWS 구현체 등록 전 +`listOf(LanguageProviderId.PAPAGO, LanguageProviderId.AWS)`는 사용할 수 없다. + +## DB code 호환성 + +내부 순서·구현체 매칭·성공 결과는 `LanguageProviderId`를 사용한다. entity/repository의 `provider`는 String 그대로다. +coordinator/cache/worker의 DB 조회·저장 접점에서만 `providerId.code`로 변환하므로 기존 `"papago"` 캐시를 재사용한다. +가짜 AWS의 저장값은 `"aws"`다. enum `.name`/`.ordinal`, `@Enumerated`/`@Convert`, 역변환 helper는 사용하지 않는다. +새 enum 항목을 추가할 때도 기존 code와 DB 스키마·캐시 키는 변경하지 않는다. `providerVersion`은 별도의 String 기록 필드다. ## 캐시와 오류 계약 @@ -94,10 +105,11 @@ adapter 예외·null 감지·빈 감지·번역 결과 개수 불일치·blank | 테스트 | 검증 내용 | |---|---| | `LanguageProviderSpringConfigurationTest` | 실제 production 기본 생성자, 과거 property/env 무시, typed-list 테스트 bean의 독립 순서와 잘못된 목록의 시작 실패 | -| `LanguageProviderPropertiesTest` | 순서 검증 및 선택되지 않은 bean을 포함한 capability 내 중복 이름 거부 | +| `LanguageProviderIdTest` | 런타임 enum getter 계약, 안정적인 papago/aws code, production 기본값과 실제 adapter ID | +| `LanguageProviderPropertiesTest` | 순서 검증 및 선택되지 않은 bean을 포함한 capability 내 중복 ID 거부 | | `LanguageDetectionProviderTest` / `TranslationCoordinatorTest` | 전체 요청 폴백, 첫 성공 중단, cache-first, 안전한 전체 실패, DB 오류 전파 | | `PapagoLanguageDetectionProviderTest` / `PapagoTranslationServiceTest` | 제어된 HTTP form/header/response와 기존 17개 chunk 회귀 | -| `LanguageProviderCacheEndToEndTest` | 실제 H2의 secondary 감지/번역 귀속, providerVersion, 두 번째 요청 무호출, worker와 materializer의 실제 표시 | +| `LanguageProviderCacheEndToEndTest` | 실제 H2의 AWS 감지/번역 귀속과 문자열 aws, providerVersion, 두 번째 요청 무호출, worker와 materializer의 실제 표시 | H2 버전은 MySQL `FOR UPDATE SKIP LOCKED`를 지원하지 않으므로 새 H2 테스트는 claim 조회 한 곳만 test spy로 대체한다. 나머지 repository 저장·별도 트랜잭션 재조회·materialization은 실제 H2다. diff --git a/docs/20261109_감지번역공급자공통화/reviews/phase-3-review.md b/docs/20261109_감지번역공급자공통화/reviews/phase-3-review.md new file mode 100644 index 00000000..382ac14a --- /dev/null +++ b/docs/20261109_감지번역공급자공통화/reviews/phase-3-review.md @@ -0,0 +1,32 @@ +# 공급자 enum 전환 Phase 3 리뷰 + +| 항목 | 내용 | +|---|---| +| 일자 | 2026-11-09 | +| 범위 | LP-013~015 및 Phase 1·2 보존 계약, 전체 미커밋 변경과 신규 파일 | +| 기준 | [PRD](../prd.md) Phase 3, [계획](../plan-task.md) P3, 현재 확장 가이드 | +| 리뷰어 | 독립 읽기 전용 gate reviewer | +| 판정 | **APPROVE** | + +## 확인 결과 + +- 공급자 인터페이스·구현체·성공 결과·순서·매칭은 enum이며 providerName alias/호환 shim이 없다. +- enum은 PAPAGO/AWS 두 항목이고 기본 순서는 PAPAGO 하나다. 실제 AWS adapter/SDK는 추가하지 않았다. +- `.code`는 DB 조회·저장 경계 6곳에 적용된다. entity/repository String, 고유 키와 schema는 그대로다. +- `.name`/`.ordinal`/`valueOf` 저장/변환이나 불필요한 fromCode가 없다. +- 기존 papago 문자열 기대값을 유지하고 AWS 저장값은 독립 문자열 aws로 확인한다. +- 빈 목록·중복 순서·중복 구현체·해당 capability의 AWS 구현체 부재를 거부한다. +- fallback·개수/blank·첫 성공 중단·DB 오류 전파·cache-first·동시성·revision·afterCommit·분할 검증을 유지한다. + +**확정 발견 사항 없음. 추가 수정 Task 전환 항목 없음.** + +## 실행 증거와 한계 + +리뷰어가 현재 XML 16개를 직접 대조하여 91/91, failures/errors/skipped 0을 확인했다. +컴파일 가능한 runtime getter assertion의 RED, 구현자의 실제 lint/bootJar 성공, +부모 fresh filtered test --rerun 성공(52초)과 별도 Java/Spring 실행 증거를 구분해 검토했다. +부모 driver는 typed getter/adapter ID/기존 문자열 cache 재사용/실패 전환/DB code 인자/미구현 AWS 거부를 확인했다. +driver repository 대역은 실제 H2 저장·재조회·표시 증거와 구분했다. 리뷰어 git diff --check는 exit 0이다. + +리뷰는 파일 변경·테스트/빌드 재실행·서버/API 호출·추가 에이전트 위임 없이 수행했다. +전체 테스트·LSP·운영 MySQL·실제 유료 API는 미검증이며 H2의 SKIP LOCKED claim은 기존 test spy 한계가 있다.