docs(i18n): 공급자 확장 방법과 검증 결과를 기록한다

This commit is contained in:
2026-11-09 15:43:23 +09:00
parent f7fadf7ac7
commit f3a558f659
4 changed files with 573 additions and 0 deletions
@@ -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<String>
getTranslationOrder(): List<String>
TranslationCoordinator(List<TranslationProvider>, LanguageProviderProperties, TranslationMemoryRepository)
findCached(String sourceHash, String sourceLanguage, String targetLanguage): TranslationMemory?
translate(TranslateRequest): ProviderTranslationResult
LanguageDetectionCacheService(LanguageDetectionResultRepository, List<LanguageDetectionProvider>, 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/추출 라이브러리/로그가 함께 제거됐으며 필요한 증거는 이 문서에 보존했다.
작업 임시 노트도 문서에 이관 후 삭제한다. 다른 작업의 파일·프로세스·캐시에는 조치하지 않았다.