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/한계는 아래 실행 증거에 누적했다.
구현상 보존 항목:
- 감지 cache 트랜잭션, insertIfAbsent 이후 locking read 승자 반환, cache hit 무호출.
- listener의 AFTER_COMMIT/REQUIRES_NEW, domain별 languageCode 가드, 커뮤니티 lock/refresh/revision 및 afterCommit 요청.
- worker는 외부 번역을 DB 저장 트랜잭션 밖에서 실행하고, 성공 vendor 기준 저장 직전 재조회·저장한다.
- materializer의 조회에는 외부 호출이 없고, 현재 원문 검증·read model 저장 방식은 유지한다.
- 테스트의 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:
CreatorCommunityTranslationEndToEndTest4건 통과, 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"와 parentls -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/awsadapter, 메모리 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 --checkexit 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 ! -eexit 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출력 없음으로 열린 파일이 없음을 확인하고, 소유 rootprovider-order-code.RRCyJ1(driver/class/라이브러리/로그)만 제거했다.test ! -eexit 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"및 parentls -ldexit 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곳이다.providerNamealias와 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 ! -eexit 0을 확인했다. 출력:CLEANUP PASS: owned provider-id-enum.jLt5Ki removed; shared caches untouched. driver/class/추출 라이브러리/로그를 제거했으며 필요한 증거는 이 문서에 남겼다. 임시 노트도 이관 후 삭제한다. 공유 cache·다른 작업의 프로세스/파일은 변경하지 않았다.