docs(i18n): 코드 기반 공급자 순서 설정과 검증을 기록한다

This commit is contained in:
2026-11-09 18:24:24 +09:00
parent 9136b0e3bb
commit 6093b37747
4 changed files with 229 additions and 20 deletions
@@ -3,8 +3,8 @@
| 항목 | 내용 | | 항목 | 내용 |
|---|---| |---|---|
| 작성일 | 2026-11-09 | | 작성일 | 2026-11-09 |
| 상태 | 구현·검증·독립 리뷰·리소스 정리 완료 | | 상태 | Phase 1·2 구현·검증·리뷰·리소스 정리 완료 |
| 요구사항 | [prd.md](prd.md), LP-001~010 및 확정 구현 계약 | | 요구사항 | [prd.md](prd.md), LP-001~012 및 확정 구현 계약 |
| 활성 Goal | 없음 | | 활성 Goal | 없음 |
## 목표와 제약 ## 목표와 제약
@@ -339,3 +339,160 @@ M v2/creator/channel/community/translation/CreatorCommunityTranslationServiceTes
출력: `CLEANUP PASS: owned language-providers.06uAbV removed; shared caches untouched`. 출력: `CLEANUP PASS: owned language-providers.06uAbV removed; shared caches untouched`.
임시 Java/class/추출 라이브러리/로그가 함께 제거됐으며 필요한 증거는 이 문서에 보존했다. 임시 Java/class/추출 라이브러리/로그가 함께 제거됐으며 필요한 증거는 이 문서에 보존했다.
작업 임시 노트도 문서에 이관 후 삭제한다. 다른 작업의 파일·프로세스·캐시에는 조치하지 않았다. 작업 임시 노트도 문서에 이관 후 삭제한다. 다른 작업의 파일·프로세스·캐시에는 조치하지 않았다.
### Phase 2: 코드 목록으로 공급자 순서 지정
- [x] **Task 2.1: 우선순위 환경 설정 제거 (P2-T1)**
**Objective:** LP-011/012에 따라 코드의 독립 목록만 production 순서를 결정한다.
**시작 조건:** 사용자의 상수 목록 방식 승인, 기존 Phase 1 완료.
**완료 증거:** 과거 property/env 무시 회귀 RED/GREEN, typed-list 순서/검증, Spring/H2 wiring 회귀.
**범위 밖:** 공급자 선택/캐시 로직 변경, 인증 키 환경변수 제거, AWS 구현, 전역 mutable 테스트 설정.
**Files:**
- Add: `src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderOrder.kt`.
- Modify: 같은 패키지 `LanguageProviderProperties.kt`, `src/main/resources/application.yml`의 우선순위 항목만.
- Update tests: `LanguageProviderPropertiesTest.kt`, `LanguageProviderSpringConfigurationTest.kt`,
`LanguageProviderCacheEndToEndTest.kt`, `LanguageDetectionProviderTest.kt`, `TranslationCoordinatorTest.kt`.
- Update docs: 이 PRD/plan과 `provider-guide.md`. 이전 완료 이력·리뷰는 삭제하지 않는다.
**계약:** `LanguageProviderOrder` object에 두 `val` 목록. `LanguageProviderProperties`의 주 생성자는
두 `List<String>`을 받고 `@Autowired`가 붙은 noarg 보조 생성자가 production object 목록으로 위임한다.
기존 `orderedProviders` 검증은 그대로 유지한다. 기본 생성자를 사용하는 호출자와 coordinator는 변경하지 않는다.
- [x] **RED:** 실제 Spring 기본 생성에서 과거 priority property/env override를 무시해야 한다는 테스트를 추가하고 기존 binding 동작으로 실패함을 확인한다.
- [x] **GREEN:** 코드 목록·typed 생성자·Spring 기본 생성자를 적용하고 우선순위 YAML/env 연결을 삭제한다.
- [x] **GREEN 확인:** default/legacy override 무시/테스트별 독립 순서/잘못된 목록·공급자 등록 검증이 통과한다.
- [x] **REFACTOR:** 실제 H2 fallback 캐시 테스트를 property override 대신 테스트 bean으로 연결하고 기존 assertion을 유지한다.
**명령:**
```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' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.community.translation.CreatorCommunityTranslationServiceTest'
./gradlew --no-daemon --offline ktlintCheck bootJar
```
- [x] **Phase 2 Gate (P2-GATE): 코드 순서 실행·리뷰·정리**
**Objective:** production bean이 코드 목록으로 동작하고 환경값으로 순서가 바뀌지 않음을 직접 실행 검증한다.
**시작 조건:** P2-T1 통과.
**대체 검증:** 부모 Spring driver에 충돌 property/env를 넣고 코드 목록 유지, 테스트 bean으로 목록 교체·validation을 확인한다.
**완료 증거:** focused 통과, build/lint, 독립 scoped 리뷰 승인, 소유 임시 경로 정리.
**범위 밖:** 전체 테스트·유료 API·운영 환경 조작. 전체 테스트는 사용자 `./gradlew test`.
Phase 2 실행 증거는 아래에 누적한다. 코드 변경 전 현재 git status는 깨끗했다.
### P2-T1 실행 증거 (2026-11-09)
승인된 Phase 2 계약과 기존 Phase 1 결과를 재사용했다. baseline 85건을 변경 전에 다시 실행하지 않고
실제 Spring의 과거 override 무시 회귀부터 작성했다. PRD의 승인 정정과 Phase 1 이력·리뷰는 수정하지 않았다.
**실행 환경:** parent 존재를 `ls -ld`로 확인하고 아래 명령으로 이 구현자만 소유하는 child를 생성했다.
이후 모든 shell 명령에서 아래 세 환경변수를 이 child로 지정했다. Gradle tool timeout은 모두 `600000ms`다.
```bash
mktemp -d /var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/provider-order-code.RRCyJ1/phase2.XXXXXX
export TMPDIR=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/provider-order-code.RRCyJ1/phase2.QPrb3i
export NODE_COMPILE_CACHE="$TMPDIR" BUN_RUNTIME_TRANSPILER_CACHE_PATH="$TMPDIR"
```
**RED (2건, 2실패, errors/skipped 0, exit 1, 20초):** production 코드를 바꾸기 전에 다음 명령을 실행했다.
두 테스트 모두 context는 정상 시작했지만 기대 `[papago]`, 실제 `[secondary, papago]`로 assertion 실패했다.
property 직접 지정과 `SystemEnvironmentPropertySource`의 과거 환경변수 → property placeholder 연결을 각각 검증했다.
환경변수 테스트는 실제 property가 `secondary,papago`로 해석됐다는 assertion도 통과한 뒤 실패했다.
컴파일·unresolved symbol·context 오류를 행동 RED로 계산하지 않았다.
```bash
./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderSpringConfigurationTest.shouldIgnoreLegacy*Overrides'
```
**GREEN:** 하나의 구현 변경으로 코드 목록·생성자·다섯 테스트의 typed-list fixture·현재 가이드를 전환했다.
`LanguageProviderOrder`에는 독립 `val` 두 개만 있으며 각각 `listOf("papago")`다. 전역 mutable 상태는 추가하지 않았다.
`LanguageProviderProperties`의 `orderedProviders`는 그대로다. production 기본/negative 테스트는
`withUserConfiguration(LanguageProviderProperties::class.java)`로 실제 component를 Spring이 생성하게 한다.
독립 순서·잘못된 목록은 테스트 bean으로 주입하고 H2는 테스트 `@Bean`으로 두 목록을 주입한다.
기존 캐시·폴백·귀속·materializer·동시성·revision assertion은 삭제하거나 완화하지 않았다.
**결합 focused 회귀 + lint/산출물 (exit 0, 3분 14초):** 아래 명령 한 번으로 모두 실행했다.
`:test`, `:ktlintCheck`, `:bootJar`의 실제 실행과 `BUILD SUCCESSFUL`을 확인했다.
```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 | 54/54 |
| 커뮤니티 감지 + cache 경합 | 14/14 (12 + 2) |
| 커뮤니티 번역 service + 기존 end-to-end | 20/20 (16 + 4) |
| 합계 | 15개 XML, 88/88, failures/errors/skipped 모두 0 |
새 override 무시 회귀 2/2가 GREEN이며 Spring 클래스 전체는 5/5다.
85건 대비 증가는 회귀 2건과 기존 default/invalid 검증을 서로 다른 테스트로 분리한 1건이다.
기존 Papago chunk 17건과 H2 secondary 귀속·재사용 2건도 통과했다.
변경하지 않은 테스트의 Kotlin 경고 7개(불필요한 `!!` 4개, unchecked cast 3개)와 Gradle 9 deprecation 경고는 남아 있다.
**부모 QA용 실제 생성자:** 아래 `javap` 명령은 exit 0이었다. 문자열 생성자와 setter는 없다.
```bash
javap -classpath build/classes/kotlin/main kr.co.vividnext.sodalive.i18n.translation.LanguageProviderProperties kr.co.vividnext.sodalive.i18n.translation.LanguageProviderOrder
```
```text
LanguageProviderProperties(java.util.List<java.lang.String>, java.util.List<java.lang.String>)
LanguageProviderProperties() // @Autowired, LanguageProviderOrder의 두 목록으로 위임
getDetectionOrder(): List<String>
getTranslationOrder(): List<String>
LanguageProviderOrder.INSTANCE.getDetectionOrder(): List<String>
LanguageProviderOrder.INSTANCE.getTranslationOrder(): List<String>
```
**이 구현자의 변경 파일:** production은 `i18n/translation/LanguageProviderOrder.kt` 추가,
`LanguageProviderProperties.kt` 수정과 `application.yml` 우선순위 블록 5줄 삭제뿐이다.
테스트는 같은 패키지의 `LanguageProviderPropertiesTest.kt`, `LanguageProviderSpringConfigurationTest.kt`,
`LanguageProviderCacheEndToEndTest.kt`, `LanguageDetectionProviderTest.kt`, `TranslationCoordinatorTest.kt`만 수정했다.
문서는 `provider-guide.md`와 이 `plan-task.md`를 갱신했다. 합계 10개다.
기본 생성자 기존 호출자·인증 환경변수·기타 설정·AWS/SDK·DB/schema·domain·캐시·폴백 코드는 변경하지 않았다.
**한계/소유권:** LSP/diagnostics 도구가 제공되지 않아 Kotlin 컴파일과 ktlint로 보완했다.
H2의 기존 claim SQL test spy 한계는 유지된다. 실제 API·운영 DB·전체 테스트는 실행하지 않았다.
전체 테스트는 사용자 명령 `./gradlew test`이며 무필터 test/check/build는 실행하지 않았다.
별도 수동 Spring driver와 ONE reviewer 및 P2-GATE 완료 판정은 부모가 맡는다.
commit/staging/push/worktree/하위 에이전트 작업은 하지 않았다.
**소유 임시 자원 정리 영수증 (2026-11-09 18:13:46 KST):** 모든 Gradle/test 실행이 종료된 뒤
`lsof -nP +D "$TMPDIR"` 출력이 없고 Java process 목록에 이 child를 사용하는 프로세스가 없음을 확인했다.
정확한 child 경로와 존재를 `test`로 대조한 뒤 이 구현자 소유 `phase2.QPrb3i`만 `rm -rf -- "$TMPDIR"`로 삭제했다.
`test ! -e "$TMPDIR"`와 parent `ls -ld`가 포함된 정리 명령은 exit 0이었다.
parent `provider-order-code.RRCyJ1`, 부모 QA의 형제 경로, 기존 공유 daemon/cache는 건드리지 않았다.
마지막 `git diff --check`는 exit 0이었다. P2-T1만 완료 처리하고 P2-GATE는 부모 검증을 위해 미완료로 남겼다.
### P2-GATE 부모 검증
- 부모가 최종 production/test/guide diff와 신규 `LanguageProviderOrder.kt`를 직접 읽었다.
구현자 결과 XML 15개를 대조해 88건, failures/errors/skipped 0을 확인했다.
- 별도 Java 17 `CodeOrderQa`로 실제 Spring component를 생성했다. 충돌하는 과거 Spring property와
`SystemEnvironmentPropertySource`를 주입해도 두 기본 순서는 코드의 `papago` 목록을 유지했다.
typed-list 생성자로 감지 B/A/C, 번역 C/B/A를 독립 지정했고 빈 목록·빈 이름·중복·미등록 순서 및
중복 공급자 이름 거부를 확인했다. 전역 목록은 변경하지 않았다.
출력: `QA PASS: production code defaults, ignored legacy configuration, independent lists and validation; context closed`.
driver exit 0, Spring context 종료, 외부 API 요청 없음.
- 부모 fresh 명령:
`./gradlew --no-daemon --offline test --rerun --tests 'kr.co.vividnext.sodalive.i18n.translation.*' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest' ktlintCheck bootJar`
→ exit 0, `BUILD SUCCESSFUL in 36s`, `:test` 실제 실행. lint/build는 검증된 현재 산출물과 일치해 UP-TO-DATE였다.
- 이후 사용자가 enum의 적합성을 물어 검토 의견만 제공했다. enum 변경은 별도 구현 승인 전이므로 Phase 2에 포함하지 않는다.
- 부모 fresh XML 11개 합계 54건, failures/errors/skipped 0을 확인했다.
- [독립 Phase 2 리뷰](reviews/phase-2-review.md): **APPROVE**, 차단/수정 요구 사항 없음.
이전 Phase 1 리뷰는 그대로 보존한다. 코드·테스트와 문서는 각각 원자적으로 커밋한다.
- 코드·테스트 커밋: `9136b0e3`. pre-commit ktlint와 커밋 메시지 사전/사후 검증을 통과했다.
- 부모 리소스 정리: `lsof -nP +D` 출력 없음으로 열린 파일이 없음을 확인하고, 소유 root
`provider-order-code.RRCyJ1`(driver/class/라이브러리/로그)만 제거했다. `test ! -e` exit 0.
출력: `CLEANUP PASS: owned provider-order-code.RRCyJ1 removed; shared caches untouched`.
필요한 증거는 이 문서에 이관했으며 임시 노트도 삭제한다. 다른 작업의 리소스는 건드리지 않았다.
@@ -3,7 +3,7 @@
| 항목 | 내용 | | 항목 | 내용 |
|---|---| |---|---|
| 작성일 | 2026-11-09 | | 작성일 | 2026-11-09 |
| 상태 | 구현·기능 범위 검증·독립 리뷰 완료 | | 상태 | Phase 1·2 구현 및 기능 범위 검증·독립 리뷰 완료 |
| 승인 범위 | 감지·번역 별도 인터페이스, 공통 공급자 선택·실패 전환, 기능별 순서 설정 | | 승인 범위 | 감지·번역 별도 인터페이스, 공통 공급자 선택·실패 전환, 기능별 순서 설정 |
| 계획 | [plan-task.md](plan-task.md) | | 계획 | [plan-task.md](plan-task.md) |
@@ -35,6 +35,8 @@
| LP-008 | 빈/중복/알 수 없는 공급자 순서 설정은 초기화 단계에서 명확히 거부한다. 새 구현체 추가 시 기존 선택 코드나 공급자 enum 수정이 필요 없다. | | LP-008 | 빈/중복/알 수 없는 공급자 순서 설정은 초기화 단계에서 명확히 거부한다. 새 구현체 추가 시 기존 선택 코드나 공급자 enum 수정이 필요 없다. |
| LP-009 | 기존 파파고 캐시·번역 데이터 및 분할 번역 동작과 감지 캐시 동시성 승자 반환을 보존한다. | | LP-009 | 기존 파파고 캐시·번역 데이터 및 분할 번역 동작과 감지 캐시 동시성 승자 반환을 보존한다. |
| LP-010 | 추가 방법과 기능별 순서 설정 예시, 실제 AWS 미구현 상태를 문서화한다. | | LP-010 | 추가 방법과 기능별 순서 설정 예시, 실제 AWS 미구현 상태를 문서화한다. |
| LP-011 | 공급자 순서는 `LanguageProviderOrder`의 독립 `val List<String>` 두 개로 지정한다. 기본은 각각 `listOf("papago")`이다. |
| LP-012 | 우선순위 환경변수·YAML·문자열 파싱을 제거한다. 기존 우선순위 property/env 값이 있어도 production 순서를 바꾸지 않는다. 인증정보 환경변수는 유지한다. |
## 보안·오류 처리 ## 보안·오류 처리
@@ -58,6 +60,8 @@ LSP 도구가 제공되지 않으면 Kotlin 컴파일과 ktlint로 보완하고
## 확정 구현 계약 ## 확정 구현 계약
아래는 Phase 1 구현 계약이며, 순서 설정 방식은 하단의 Phase 2 정정 계약이 우선한다. 기존 실행 기록은 보존한다.
- 기존 `TranslationProvider`와 raw `TranslateResult`는 유지한다. - 기존 `TranslationProvider`와 raw `TranslateResult`는 유지한다.
- `LanguageDetectionProvider`: `providerName: String`, `detect(query: String): String?`. - `LanguageDetectionProvider`: `providerName: String`, `detect(query: String): String?`.
- `DetectedLanguage`: `detectedLanguage: String`, `providerName: String`. - `DetectedLanguage`: `detectedLanguage: String`, `providerName: String`.
@@ -79,3 +83,16 @@ LSP 도구가 제공되지 않으면 Kotlin 컴파일과 ktlint로 보완하고
- 공통 `TranslationLanguages`로 서비스 언어 정책만 옮긴다. Papago 분할은 Papago adapter에 유지한다. - 공통 `TranslationLanguages`로 서비스 언어 정책만 옮긴다. Papago 분할은 Papago adapter에 유지한다.
- 현재 entity에는 공급자별 cache key가 있으므로 DDL 변경 없이 실제 공급자를 기록한다. - 현재 entity에는 공급자별 cache key가 있으므로 DDL 변경 없이 실제 공급자를 기록한다.
캐시 두 개가 있으면 설정상 앞선 결과를 선택하며 특정 job의 공급자를 영구 고정하는 새 모델은 만들지 않는다. 캐시 두 개가 있으면 설정상 앞선 결과를 선택하며 특정 job의 공급자를 영구 고정하는 새 모델은 만들지 않는다.
## Phase 2 결정 정정 — 2026-11-09
사용자는 YAML·환경변수 대신 코드의 상수 목록으로 감지·번역 순서를 지정하도록 승인했다.
- 유일한 production 순서 편집 지점은 `i18n/translation/LanguageProviderOrder.kt`의
`detectionOrder = listOf("papago")`, `translationOrder = listOf("papago")`이다. Kotlin 목록은 `const val`이 아닌 `val`이다.
- 목록의 왼쪽부터 사용한다. 감지·번역은 각각 수정할 수 있으며 변경 후 빌드·배포한다.
- 기존 `LanguageProviderProperties`는 typed-list 생성자와 명시적 Spring 기본 생성자로 코드 목록을 읽는다.
선택 코드·호출자의 이름 변경을 확산시키지 않는다. 문자열 생성자·`@Value`·split/trim은 제거한다.
- 테스트는 전역 목록을 바꾸지 않고 테스트 전용 bean에 typed list를 주입하여 독립 순서와 invalid order를 검증한다.
- 과거 우선순위 environment/property override는 무시한다. 다른 환경변수나 Spring 설정 체계는 변경하지 않는다.
- 실패 전환·공급자 검증·캐시·DB·인증·AWS 미구현 정책은 Phase 1을 그대로 유지한다.
@@ -27,7 +27,7 @@ AWS 감지·번역 adapter, 해당 SDK 및 전용 인증 설정은 추가하지
`providerName`은 capability 내 유일하고 재배포 후에도 안정적인 값이어야 한다. `providerName`은 capability 내 유일하고 재배포 후에도 안정적인 값이어야 한다.
2. 공급자의 HTTP/SDK 및 인증은 adapter 안에 둔다. 외부 응답 형식과 빈 결과를 검증하고, 2. 공급자의 HTTP/SDK 및 인증은 adapter 안에 둔다. 외부 응답 형식과 빈 결과를 검증하고,
원문·인증정보·응답 본문·예외 cause를 로그로 출력하지 않는다. 번역은 입력별 결과를 같은 순서로 반환한다. 원문·인증정보·응답 본문·예외 cause를 로그로 출력하지 않는다. 번역은 입력별 결과를 같은 순서로 반환한다.
3. 구현한 capability의 설정 순서에만 이름을 넣고 재시작/재배포한다. `@Primary`, selector 분기, enum 등록은 필요 없다. 3. `LanguageProviderOrder.kt`에서 구현한 capability의 목록에만 이름을 넣고 빌드·배포한다. `@Primary`, selector 분기, enum 등록은 필요 없다.
4. 아래 테스트 경계로 순서·실패 전환·실제 provider 저장·무호출 캐시 재사용·materializer 표시를 검증한다. 4. 아래 테스트 경계로 순서·실패 전환·실제 provider 저장·무호출 캐시 재사용·materializer 표시를 검증한다.
콘텐츠·댓글·커뮤니티 호출자, worker, materializer, scheduler에 공급자 분기를 추가하지 않는다. 콘텐츠·댓글·커뮤니티 호출자, worker, materializer, scheduler에 공급자 분기를 추가하지 않는다.
@@ -35,33 +35,37 @@ AWS 감지·번역 adapter, 해당 SDK 및 전용 인증 설정은 추가하지
Papago의 5,000 UTF-16 단위 분할과 서로게이트 쌍 보존은 `PapagoTranslationService`에 남아 있다. Papago의 5,000 UTF-16 단위 분할과 서로게이트 쌍 보존은 `PapagoTranslationService`에 남아 있다.
ko/en/ja 서비스 정책은 `TranslationLanguages`에 있으며 공급자를 추가해도 자동 확장하지 않는다. ko/en/ja 서비스 정책은 `TranslationLanguages`에 있으며 공급자를 추가해도 자동 확장하지 않는다.
## 기능별 독립 설정 ## 기능별 독립 코드 목록
`LanguageProviderProperties`는 component scan으로 등록되며 생성자 `@Value`로 문자열을 주입받는다. production 순서의 유일한 편집 지점은
별도 `@EnableConfigurationProperties` 등록은 필요 없다. 쉼표로 나누고 각 이름의 앞뒤 공백을 제거한다. `src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderOrder.kt`다.
기본값은 두 기능 모두 `papago`다. 목록의 왼쪽부터 사용하며 기본값은 두 기능 모두 `listOf("papago")`다.
```yaml ```kotlin
sodalive: object LanguageProviderOrder {
language-providers: val detectionOrder = listOf("papago")
detection-order: ${LANGUAGE_PROVIDERS_DETECTION_ORDER:papago} val translationOrder = listOf("papago")
translation-order: ${LANGUAGE_PROVIDERS_TRANSLATION_ORDER:papago} }
``` ```
Kotlin 목록은 `const val`이 아닌 `val`이다. `LanguageProviderProperties`는 기존 주입 wrapper로 유지하며,
Spring은 `@Autowired` 기본 생성자로 위 목록을 읽는다. 문자열 파싱은 하지 않는다.
테스트에서는 전역 목록을 변경하지 않고 `LanguageProviderProperties(List<String>, List<String>)`로 만든 테스트 bean을 주입한다.
다음은 **secondary 구현체를 두 capability에 등록한 뒤에만** 적용할 수 있는 예시다. 다음은 **secondary 구현체를 두 capability에 등록한 뒤에만** 적용할 수 있는 예시다.
현재 `secondary` 구현체는 테스트에만 있다. 현재 `secondary` 구현체는 테스트에만 있다.
```bash ```kotlin
export LANGUAGE_PROVIDERS_DETECTION_ORDER=secondary,papago val detectionOrder = listOf("secondary", "papago")
export LANGUAGE_PROVIDERS_TRANSLATION_ORDER=papago,secondary val translationOrder = listOf("papago", "secondary")
``` ```
감지만 순서를 바꾸어도 번역 순서는 바뀌지 않는다. 반대로 번역만 바꿀 수도 있다. 감지만 순서를 바꾸어도 번역 순서는 바뀌지 않는다. 반대로 번역만 바꿀 수도 있다.
Spring property를 직접 지정할 때는 `sodalive.language-providers.detection-order`와 변경 후 빌드·배포해야 반영된다. 과거 우선순위 Spring property·환경변수·YAML 값이 남아 있어도 순서는 바뀌지 않는다.
`sodalive.language-providers.translation-order`를 각각 설정한다. 공급자 인증정보는 계속 환경변수로 주입하며 이 코드 목록에 넣지 않는다.
빈 값·빈 항목·중복 이름·미등록 이름·capability 안의 중복 bean 이름표(`providerName`)는 초기화에서 거부한다. 빈 값·빈 항목·중복 이름·미등록 이름·capability 안의 중복 bean 이름표(`providerName`)는 초기화에서 거부한다.
공급자가 번역에만 등록됐다면 그 이름을 감지 순서에 넣을 수 없다. 공급자가 번역에만 등록됐다면 그 이름을 감지 순서에 넣을 수 없다.
실행 중 설정 변경은 지원하지 않는다. AWS 구현체 등록 전 `papago,aws`는 사용할 수 없다. 실행 중 설정 변경은 지원하지 않는다. AWS 구현체 등록 전 `listOf("papago", "aws")`는 사용할 수 없다.
## 캐시와 오류 계약 ## 캐시와 오류 계약
@@ -89,7 +93,7 @@ adapter 예외·null 감지·빈 감지·번역 결과 개수 불일치·blank
| 테스트 | 검증 내용 | | 테스트 | 검증 내용 |
|---|---| |---|---|
| `LanguageProviderSpringConfigurationTest` | 실제 `@Value` binding, 두 test bean, 기능별 독립 순서, 기본값과 잘못된 설정의 시작 실패 | | `LanguageProviderSpringConfigurationTest` | 실제 production 기본 생성자, 과거 property/env 무시, typed-list 테스트 bean의 독립 순서와 잘못된 목록의 시작 실패 |
| `LanguageProviderPropertiesTest` | 순서 검증 및 선택되지 않은 bean을 포함한 capability 내 중복 이름 거부 | | `LanguageProviderPropertiesTest` | 순서 검증 및 선택되지 않은 bean을 포함한 capability 내 중복 이름 거부 |
| `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 회귀 |
@@ -0,0 +1,31 @@
# 코드 기반 공급자 순서 Phase 2 리뷰
| 항목 | 내용 |
|---|---|
| 기준 | [PRD](../prd.md) LP-011/012, [계획](../plan-task.md) Phase 2 |
| 일자 | 2026-11-09 |
| 리뷰어 | 독립 읽기 전용 gate reviewer |
| 판정 | **APPROVE** |
## 판정 근거
- `LanguageProviderOrder`의 두 독립 `val List<String>`이 production 순서의 기준이며 기본값은 각각 papago다.
- `LanguageProviderProperties`는 typed-list 생성자와 `@Autowired` 기본 생성자만 제공한다.
우선순위용 String overload, `@Value`, 문자열 파싱이나 mutable 전역 설정이 없다.
- YAML 삭제는 우선순위 블록 5줄뿐이다. 인증 환경변수는 유지한다.
- 실제 Spring 기본 경로에서 과거 property/env 값은 순서에 영향을 주지 않는다.
- 독립 순서·invalid 목록·중복 공급자·H2 귀속/재사용의 기존 검증을 유지한다.
- 현재 가이드가 코드 편집 지점·빌드/배포·기존 override 무시 동작과 일치한다.
- enum 변경은 사용자가 적합성만 질문한 별도 미승인 범위로, 이번 구현 누락이 아니다.
**확정 발견 사항 없음. 수정 Task 전환 항목 없음.**
## 검증 근거와 한계
변경 전 의미 있는 RED 2건, 구현 후 88/88 통과와 실제 lint/bootJar 성공 기록을 검토했다.
부모가 XML 15개를 직접 대조했고, focused 54건을 `--rerun`하여 exit 0(36초)을 확인했다.
부모의 별도 Java/Spring 실행은 충돌 property/env 무시, 감지 B/A/C·번역 C/B/A, invalid 목록/공급자 거부를 통과했다.
Spring context는 종료됐으며 외부 API 요청은 없었다. reviewer `git diff --check`는 exit 0이다.
리뷰는 코드·테스트·설정을 변경하거나 테스트/빌드를 재실행하지 않았다.
전체 테스트·LSP·유료 API·운영 MySQL은 미검증이며 기존 H2 claim SQL 대역 한계를 그대로 명시한다.