Files

55 KiB

언어 감지·번역 공급자 공통화 구현 계획

항목 내용
작성일 2026-11-09
상태 Phase 1~3 구현·검증·독립 리뷰·리소스 정리 완료
요구사항 prd.md, LP-001~015 및 최종 Phase 계약
활성 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: 공급자 독립 실행과 캐시

  • 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 이름을 사용한다.

  • RED: 기존 behavior를 보존하는 fixture 갱신 후, 두 가짜 공급자의 순서·실패 전환·저장 귀속 테스트를 작성한다.
  • RED 확인: 신규 타입의 unresolved symbol 오류는 RED로 계산하지 않는다. 필요한 최소 선언으로 컴파일한 뒤 누락된 순서/전환/캐시 동작 때문에 실패하는 assertion을 실제로 확인한다.
  • GREEN: PRD 계약을 충족하는 최소 두 coordinator 및 설정/adapter 연결을 구현한다.
  • GREEN 확인: 새 focused 테스트에서 독립 순서·캐시·폴백·모두 실패·설정 오류가 통과한다.
  • 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만 옮긴다.

검증 명령:

./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
  • 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 결과를 사용했다.

  • 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 안에서만 보관하며 정리 시 삭제한다.

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이나 컴파일 실패는 없었다.

./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으로 고쳐 별도 변환 서비스 의존을 제거했다.

./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만 대체했다.

./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을 정확히 기대하도록 수정했으며 공급자 요청 형식은 기존과 같다.

./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
./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이다.

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/.

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/.

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였다.
./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건 통과)를 직접 대조했다. 리뷰 보고서.
  • 원자적 커밋은 (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/추출 라이브러리/로그가 함께 제거됐으며 필요한 증거는 이 문서에 보존했다. 작업 임시 노트도 문서에 이관 후 삭제한다. 다른 작업의 파일·프로세스·캐시에는 조치하지 않았다.

Phase 2: 코드 목록으로 공급자 순서 지정

  • 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는 변경하지 않는다.

  • RED: 실제 Spring 기본 생성에서 과거 priority property/env override를 무시해야 한다는 테스트를 추가하고 기존 binding 동작으로 실패함을 확인한다.
  • GREEN: 코드 목록·typed 생성자·Spring 기본 생성자를 적용하고 우선순위 YAML/env 연결을 삭제한다.
  • GREEN 확인: default/legacy override 무시/테스트별 독립 순서/잘못된 목록·공급자 등록 검증이 통과한다.
  • REFACTOR: 실제 H2 fallback 캐시 테스트를 property override 대신 테스트 bean으로 연결하고 기존 assertion을 유지한다.

명령:

./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
  • 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다.

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로 계산하지 않았다.

./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을 확인했다.

./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는 없다.

javap -classpath build/classes/kotlin/main kr.co.vividnext.sodalive.i18n.translation.LanguageProviderProperties kr.co.vividnext.sodalive.i18n.translation.LanguageProviderOrder
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 리뷰: 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. 필요한 증거는 이 문서에 이관했으며 임시 노트도 삭제한다. 다른 작업의 리소스는 건드리지 않았다.

Phase 3: 공급자 enum과 DB code 경계

  • 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의 같은 문자열은 바꾸지 않는다.

  • RED: 컴파일 가능한 기존 인터페이스/결과에 대한 runtime getter 반환 타입 assertion으로 새 enum 식별 계약의 실패를 재현한다. missing symbol 컴파일 오류를 RED로 보고하지 않는다.
  • GREEN: enum·providerId·typed 순서 및 DB 경계 .code 변환을 한 단위로 구현한다.
  • GREEN 확인: enum code 고정값, 원래 papago 캐시 재사용, 실제 DB의 papago/aws 문자열 저장·조회, 결과 enum 귀속이 통과한다.
  • REFACTOR: 관련 fixture만 정리하고 빈 목록/중복/미등록 AWS 검증·감지/번역 순서·실패 전환·경합·분할 회귀를 실행한다.

검증 명령: Phase 2의 결합 focused 명령(번역 package·감지 cache·커뮤니티 감지/경합·번역 service·end-to-end)을 그대로 실행하며 새 enum 테스트도 번역 package 필터에 포함한다. --no-daemon --offline, timeout600000 사용. ktlintCheck bootJar를 함께 검증한다. 전체 테스트는 사용자 실행 ./gradlew test로 남긴다.

  • 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다.

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다.

./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/중간 전체 컴파일 반복은 하지 않았다.

./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의 역변환이나 저장에 사용하지 않는다.

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개).

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개).

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, 확정 발견 사항·차단 이슈 없음. 리뷰 보고서. 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·다른 작업의 프로세스/파일은 변경하지 않았다.