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 | | 작성일 | 2026-11-09 |
| 상태 | Phase 1·2 구현·검증·리뷰·리소스 정리 완료 | | 상태 | Phase 1~3 구현·검증·독립 리뷰·리소스 정리 완료 |
| 요구사항 | [prd.md](prd.md), LP-001~012 및 확정 구현 계약 | | 요구사항 | [prd.md](prd.md), LP-001~015 및 최종 Phase 계약 |
| 활성 Goal | 없음 | | 활성 Goal | 없음 |
## 목표와 제약 ## 목표와 제약
@@ -496,3 +496,222 @@ parent `provider-order-code.RRCyJ1`, 부모 QA의 형제 경로, 기존 공유 d
`provider-order-code.RRCyJ1`(driver/class/라이브러리/로그)만 제거했다. `test ! -e` exit 0. `provider-order-code.RRCyJ1`(driver/class/라이브러리/로그)만 제거했다. `test ! -e` exit 0.
출력: `CLEANUP PASS: owned provider-order-code.RRCyJ1 removed; shared caches untouched`. 출력: `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·다른 작업의 프로세스/파일은 변경하지 않았다.
@@ -3,7 +3,7 @@
| 항목 | 내용 | | 항목 | 내용 |
|---|---| |---|---|
| 작성일 | 2026-11-09 | | 작성일 | 2026-11-09 |
| 상태 | Phase 1·2 구현 및 기능 범위 검증·독립 리뷰 완료 | | 상태 | Phase 1~3 구현·기능 범위 검증·독립 리뷰 완료 |
| 승인 범위 | 감지·번역 별도 인터페이스, 공통 공급자 선택·실패 전환, 기능별 순서 설정 | | 승인 범위 | 감지·번역 별도 인터페이스, 공통 공급자 선택·실패 전환, 기능별 순서 설정 |
| 계획 | [plan-task.md](plan-task.md) | | 계획 | [plan-task.md](plan-task.md) |
@@ -32,11 +32,14 @@
| LP-005 | 캐시는 실제 성공 공급자 이름으로 저장한다. AWS 역할의 가짜 공급자 성공을 papago 이름으로 저장하지 않는다. | | LP-005 | 캐시는 실제 성공 공급자 이름으로 저장한다. AWS 역할의 가짜 공급자 성공을 papago 이름으로 저장하지 않는다. |
| LP-006 | 설정된 공급자들의 유효 캐시를 우선 재사용한다. 새 외부 호출의 순서는 별도 설정 순서를 따른다. 워커와 결과 조립의 캐시 선택 정책이 같다. | | LP-006 | 설정된 공급자들의 유효 캐시를 우선 재사용한다. 새 외부 호출의 순서는 별도 설정 순서를 따른다. 워커와 결과 조립의 캐시 선택 정책이 같다. |
| LP-007 | 빈 원문·동일 언어·서비스 미지원 번역 언어는 불필요한 공급자 전환을 일으키지 않는다. 유효한 미지원 언어 감지 결과는 감지 장애와 구분한다. | | LP-007 | 빈 원문·동일 언어·서비스 미지원 번역 언어는 불필요한 공급자 전환을 일으키지 않는다. 유효한 미지원 언어 감지 결과는 감지 장애와 구분한다. |
| LP-008 | 빈/중복/알 수 없는 공급자 순서 설정은 초기화 단계에서 명확히 거부한다. 새 구현체 추가 시 기존 선택 코드나 공급자 enum 수정이 필요 없다. | | LP-008 | 빈/중복/미등록 구현체의 공급자 순서는 초기화 단계에서 거부한다. 선택 코드에 공급자 분기를 추가하지 않는다. Phase 3부터 새 공급자는 enum 항목을 추가한다. |
| LP-009 | 기존 파파고 캐시·번역 데이터 및 분할 번역 동작과 감지 캐시 동시성 승자 반환을 보존한다. | | LP-009 | 기존 파파고 캐시·번역 데이터 및 분할 번역 동작과 감지 캐시 동시성 승자 반환을 보존한다. |
| LP-010 | 추가 방법과 기능별 순서 설정 예시, 실제 AWS 미구현 상태를 문서화한다. | | LP-010 | 추가 방법과 기능별 순서 설정 예시, 실제 AWS 미구현 상태를 문서화한다. |
| LP-011 | 공급자 순서는 `LanguageProviderOrder`의 독립 `val List<String>` 두 개로 지정한다. 기본은 각각 `listOf("papago")`이다. | | LP-011 | 공급자 순서는 `LanguageProviderOrder`의 독립 `val List<String>` 두 개로 지정한다. 기본은 각각 `listOf("papago")`이다. |
| LP-012 | 우선순위 환경변수·YAML·문자열 파싱을 제거한다. 기존 우선순위 property/env 값이 있어도 production 순서를 바꾸지 않는다. 인증정보 환경변수는 유지한다. | | 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를 검증한다. - 테스트는 전역 목록을 바꾸지 않고 테스트 전용 bean에 typed list를 주입하여 독립 순서와 invalid order를 검증한다.
- 과거 우선순위 environment/property override는 무시한다. 다른 환경변수나 Spring 설정 체계는 변경하지 않는다. - 과거 우선순위 environment/property override는 무시한다. 다른 환경변수나 Spring 설정 체계는 변경하지 않는다.
- 실패 전환·공급자 검증·캐시·DB·인증·AWS 미구현 정책은 Phase 1을 그대로 유지한다. - 실패 전환·공급자 검증·캐시·DB·인증·AWS 미구현 정책은 Phase 1을 그대로 유지한다.
## Phase 3 결정 정정 — 2026-11-09
사용자는 공급자 enum 도입과 식별자 용도의 papago 리터럴 변경을 승인했다. 타입 관련 항목은 이 계약이 앞선 Phase보다 우선한다.
- `LanguageProviderId(val code: String)`에 `PAPAGO("papago")`, `AWS("aws")`를 정의한다.
- `LanguageProviderOrder`, `LanguageProviderProperties`의 두 순서는 `List<LanguageProviderId>`이다.
기본 목록은 각각 `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 항목·구현체·코드 순서 목록을 추가한다. 도메인 호출자와 선택 알고리즘은 변경하지 않는다.
@@ -3,19 +3,19 @@
## 현재 실행 구조 ## 현재 실행 구조
운영 구현체는 `PapagoLanguageDetectionProvider`와 `PapagoTranslationService`뿐이다. 운영 구현체는 `PapagoLanguageDetectionProvider`와 `PapagoTranslationService`뿐이다.
AWS 감지·번역 adapter, 해당 SDK 및 전용 인증 설정은 추가하지 않았다. 지금 `aws`를 순서에 넣으면 시작에 실패한다. AWS 감지·번역 adapter, 해당 SDK 및 전용 인증 설정은 추가하지 않았다. 지금 `LanguageProviderId.AWS`를 순서에 넣으면 시작에 실패한다.
감지와 번역은 별도 capability다. 감지만 또는 번역만 제공하는 공급자도 등록할 수 있다. 감지와 번역은 별도 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?` | | `LanguageDetectionCacheService` | `detectWithCache(query: String): DetectedLanguage?` |
| `DetectedLanguage` | `detectedLanguage: String`, `providerName: String` | | `DetectedLanguage` | `detectedLanguage: String`, `providerId: LanguageProviderId` |
| `TranslationProvider` | `providerName: String`, `providerVersion: String`, `translate(TranslateRequest): TranslateResult` | | `TranslationProvider` | `providerId: LanguageProviderId`, `providerVersion: String`, `translate(TranslateRequest): TranslateResult` |
| `TranslationCoordinator` | `findCached(sourceHash, sourceLanguage, targetLanguage): TranslationMemory?`, `translate(TranslateRequest): ProviderTranslationResult` | | `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/`에 있다. 인터페이스와 DTO는 `src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/`에 있다.
감지 coordinator는 기존 `content/LanguageDetectionCacheService.kt`이며 별도 vendor가 아니다. 감지 coordinator는 기존 `content/LanguageDetectionCacheService.kt`이며 별도 vendor가 아니다.
@@ -23,12 +23,14 @@ AWS 감지·번역 adapter, 해당 SDK 및 전용 인증 설정은 추가하지
## 새 공급자 추가 순서 ## 새 공급자 추가 순서
1. 필요한 capability 인터페이스를 구현하고 Spring `@Component`/`@Service` 또는 `@Bean`으로 등록한다. 1. `LanguageProviderId`에 공급자 enum 항목과 재배포 후에도 안정적인 DB `code`를 추가한다.
`providerName`은 capability 내 유일하고 재배포 후에도 안정적인 값이어야 한다. 현재 항목은 `PAPAGO("papago")`, `AWS("aws")`다. AWS는 이미 ID가 있으므로 실제 adapter만 별도로 구현한다.
2. 공급자의 HTTP/SDK 및 인증은 adapter 안에 둔다. 외부 응답 형식과 빈 결과를 검증하고, 2. 필요한 capability 인터페이스를 구현하고 Spring `@Component`/`@Service` 또는 `@Bean`으로 등록한다.
예를 들어 Papago adapter의 식별자는 `override val providerId = LanguageProviderId.PAPAGO`다.
3. 공급자의 HTTP/SDK 및 인증은 adapter 안에 둔다. 외부 응답 형식과 빈 결과를 검증하고,
원문·인증정보·응답 본문·예외 cause를 로그로 출력하지 않는다. 번역은 입력별 결과를 같은 순서로 반환한다. 원문·인증정보·응답 본문·예외 cause를 로그로 출력하지 않는다. 번역은 입력별 결과를 같은 순서로 반환한다.
3. `LanguageProviderOrder.kt`에서 구현한 capability의 목록에만 이름을 넣고 빌드·배포한다. `@Primary`, selector 분기, enum 등록은 필요 없다. 4. `LanguageProviderOrder.kt`에서 구현한 capability의 목록에만 enum을 넣고 빌드·배포한다. `@Primary`나 selector 분기는 필요 없다.
4. 아래 테스트 경계로 순서·실패 전환·실제 provider 저장·무호출 캐시 재사용·materializer 표시를 검증한다. 5. 아래 테스트 경계로 순서·실패 전환·실제 provider 저장·무호출 캐시 재사용·materializer 표시를 검증한다.
콘텐츠·댓글·커뮤니티 호출자, worker, materializer, scheduler에 공급자 분기를 추가하지 않는다. 콘텐츠·댓글·커뮤니티 호출자, worker, materializer, scheduler에 공급자 분기를 추가하지 않는다.
기존 두 coordinator는 주입된 구현체 목록과 설정만 사용한다. 공급자마다 다른 분할·요청 크기 제한은 adapter 책임이다. 기존 두 coordinator는 주입된 구현체 목록과 설정만 사용한다. 공급자마다 다른 분할·요청 크기 제한은 adapter 책임이다.
@@ -39,33 +41,42 @@ ko/en/ja 서비스 정책은 `TranslationLanguages`에 있으며 공급자를
production 순서의 유일한 편집 지점은 production 순서의 유일한 편집 지점은
`src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderOrder.kt`다. `src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderOrder.kt`다.
목록의 왼쪽부터 사용하며 기본값은 두 기능 모두 `listOf("papago")`다. 목록의 왼쪽부터 사용하며 기본값은 두 기능 모두 `listOf(LanguageProviderId.PAPAGO)`다.
```kotlin ```kotlin
object LanguageProviderOrder { object LanguageProviderOrder {
val detectionOrder = listOf("papago") val detectionOrder = listOf(LanguageProviderId.PAPAGO)
val translationOrder = listOf("papago") val translationOrder = listOf(LanguageProviderId.PAPAGO)
} }
``` ```
Kotlin 목록은 `const val`이 아닌 `val`이다. `LanguageProviderProperties`는 기존 주입 wrapper로 유지하며, Kotlin 목록은 `const val`이 아닌 `val`이다. `LanguageProviderProperties`는 기존 주입 wrapper로 유지하며,
Spring은 `@Autowired` 기본 생성자로 위 목록을 읽는다. 문자열 파싱은 하지 않는다. Spring은 `@Autowired` 기본 생성자로 위 목록을 읽는다. 문자열 파싱은 하지 않는다.
테스트에서는 전역 목록을 변경하지 않고 `LanguageProviderProperties(List<String>, List<String>)`로 만든 테스트 bean을 주입한다. 테스트에서는 전역 목록을 변경하지 않고
`LanguageProviderProperties(List<LanguageProviderId>, List<LanguageProviderId>)`로 만든 테스트 bean을 주입한다.
다음은 **secondary 구현체를 두 capability에 등록한 뒤에만** 적용할 수 있는 예시다. 다음은 **실제 AWS 구현체를 두 capability에 등록한 뒤에만** 적용할 수 있는 예시다.
현재 `secondary` 구현체는 테스트에만 있다. 현재 AWS ID의 가짜 구현체는 테스트에만 있다.
```kotlin ```kotlin
val detectionOrder = listOf("secondary", "papago") val detectionOrder = listOf(LanguageProviderId.AWS, LanguageProviderId.PAPAGO)
val translationOrder = listOf("papago", "secondary") val translationOrder = listOf(LanguageProviderId.PAPAGO, LanguageProviderId.AWS)
``` ```
감지만 순서를 바꾸어도 번역 순서는 바뀌지 않는다. 반대로 번역만 바꿀 수도 있다. 감지만 순서를 바꾸어도 번역 순서는 바뀌지 않는다. 반대로 번역만 바꿀 수도 있다.
변경 후 빌드·배포해야 반영된다. 과거 우선순위 Spring property·환경변수·YAML 값이 남아 있어도 순서는 바뀌지 않는다. 변경 후 빌드·배포해야 반영된다. 과거 우선순위 Spring property·환경변수·YAML 값이 남아 있어도 순서는 바뀌지 않는다.
공급자 인증정보는 계속 환경변수로 주입하며 이 코드 목록에 넣지 않는다. 공급자 인증정보는 계속 환경변수로 주입하며 이 코드 목록에 넣지 않는다.
빈 값·빈 항목·중복 이름·미등록 이름·capability 안의 중복 bean 이름표(`providerName`)는 초기화에서 거부한다. 빈 목록·중복 ID·해당 capability의 구현체 부재·capability 안의 중복 구현체 ID는 초기화에서 거부한다.
공급자가 번역에만 등록됐다면 그 이름을 감지 순서에 넣을 수 없다. 빈 문자열 ID는 enum 타입으로 표현할 수 없다. 공급자가 번역에만 등록됐다면 그 ID를 감지 순서에 넣을 수 없다.
실행 중 설정 변경은 지원하지 않는다. AWS 구현체 등록 전 `listOf("papago", "aws")`는 사용할 수 없다. 실행 중 설정 변경은 지원하지 않는다. 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의 독립 순서와 잘못된 목록의 시작 실패 | | `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 오류 전파 | | `LanguageDetectionProviderTest` / `TranslationCoordinatorTest` | 전체 요청 폴백, 첫 성공 중단, cache-first, 안전한 전체 실패, DB 오류 전파 |
| `PapagoLanguageDetectionProviderTest` / `PapagoTranslationServiceTest` | 제어된 HTTP form/header/response와 기존 17개 chunk 회귀 | | `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 조회 한 곳만 H2 버전은 MySQL `FOR UPDATE SKIP LOCKED`를 지원하지 않으므로 새 H2 테스트는 claim 조회 한 곳만
test spy로 대체한다. 나머지 repository 저장·별도 트랜잭션 재조회·materialization은 실제 H2다. test spy로 대체한다. 나머지 repository 저장·별도 트랜잭션 재조회·materialization은 실제 H2다.
@@ -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 한계가 있다.