docs(i18n): 공급자 enum과 DB code 호환 계약을 기록한다

This commit is contained in:
2026-11-09 19:15:27 +09:00
parent ec890047a5
commit 2bd2328548
4 changed files with 311 additions and 28 deletions
@@ -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<? extends LanguageProviderId>, List<? extends LanguageProviderId>)
LanguageProviderProperties() // @Autowired, production object 목록으로 위임
getDetectionOrder(): List<LanguageProviderId>; getTranslationOrder(): List<LanguageProviderId>
LanguageProviderOrder.INSTANCE.getDetectionOrder/getTranslationOrder(): List<LanguageProviderId>
orderedProviders(List<? extends LanguageProviderId>, List<? extends T>, Function1<? super T, ? extends LanguageProviderId>)
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<? extends TranslationProvider>, LanguageProviderProperties, TranslationMemoryRepository)
LanguageDetectionCacheService(LanguageDetectionResultRepository, List<? extends LanguageDetectionProvider>,
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·다른 작업의 프로세스/파일은 변경하지 않았다.