diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderOrder.kt b/src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderOrder.kt new file mode 100644 index 00000000..e1c4b39b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderOrder.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.i18n.translation + +object LanguageProviderOrder { + val detectionOrder = listOf("papago") + val translationOrder = listOf("papago") +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderProperties.kt b/src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderProperties.kt index 00157910..9fdb4232 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderProperties.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderProperties.kt @@ -1,17 +1,15 @@ package kr.co.vividnext.sodalive.i18n.translation -import org.springframework.beans.factory.annotation.Value +import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component class LanguageProviderProperties( - @Value("\${sodalive.language-providers.detection-order:papago}") - detectionOrder: String = "papago", - @Value("\${sodalive.language-providers.translation-order:papago}") - translationOrder: String = "papago" + val detectionOrder: List, + val translationOrder: List ) { - val detectionOrder = detectionOrder.split(',').map { it.trim() } - val translationOrder = translationOrder.split(',').map { it.trim() } + @Autowired + constructor() : this(LanguageProviderOrder.detectionOrder, LanguageProviderOrder.translationOrder) fun orderedProviders(order: List, providers: List, name: (T) -> String): List { require(order.isNotEmpty() && order.none { it.isBlank() }) { "language provider order must not be empty" } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3cbb4da4..f9ba76d7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,11 +19,6 @@ management: show-details: never show-components: never -sodalive: - language-providers: - detection-order: ${LANGUAGE_PROVIDERS_DETECTION_ORDER:papago} - translation-order: ${LANGUAGE_PROVIDERS_TRANSLATION_ORDER:papago} - deployment: artifact-version: ${DEPLOY_ARTIFACT_VERSION:local} config-commit: ${DEPLOY_CONFIG_COMMIT:0000000000000000000000000000000000000000} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageDetectionProviderTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageDetectionProviderTest.kt index 9f05a9df..6fdae682 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageDetectionProviderTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageDetectionProviderTest.kt @@ -92,7 +92,7 @@ class LanguageDetectionProviderTest { } private fun service(providers: List, order: List = providers.map { it.providerName }) = - LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(detectionOrder = order.joinToString(","))) + LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(order, listOf("papago"))) } class RecordingDetectionProvider( diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderCacheEndToEndTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderCacheEndToEndTest.kt index 5cf02a4b..1d5e0203 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderCacheEndToEndTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderCacheEndToEndTest.kt @@ -35,16 +35,13 @@ import java.util.UUID @DataJpaTest( properties = [ "spring.cache.type=none", - "spring.datasource.url=jdbc:h2:mem:language-provider-cache;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE", - "sodalive.language-providers.detection-order=papago,secondary", - "sodalive.language-providers.translation-order=papago,secondary" + "spring.datasource.url=jdbc:h2:mem:language-provider-cache;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE" ] ) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Import( QueryDslConfig::class, AudioContentThemeQueryRepository::class, - LanguageProviderProperties::class, LanguageDetectionCacheService::class, TranslationCoordinator::class, TranslationJobWorker::class, @@ -163,6 +160,12 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor( @TestConfiguration class Providers { + @Bean + fun languageProviderProperties() = LanguageProviderProperties( + listOf("papago", "secondary"), + listOf("papago", "secondary") + ) + @Bean fun papagoDetection() = RecordingDetectionProvider("papago") { throw IllegalStateException("sensitive provider body") } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderPropertiesTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderPropertiesTest.kt index 5551576c..bc639e20 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderPropertiesTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderPropertiesTest.kt @@ -9,7 +9,7 @@ class LanguageProviderPropertiesTest { @Test @DisplayName("기능별 목록을 서로 독립된 설정 순서로 정렬한다") fun shouldOrderCapabilitiesIndependently() { - val properties = LanguageProviderProperties("second,first", "first,second") + val properties = LanguageProviderProperties(listOf("second", "first"), listOf("first", "second")) assertEquals( listOf("second", "first"), properties.orderedProviders(properties.detectionOrder, listOf("first", "second")) { it } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderSpringConfigurationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderSpringConfigurationTest.kt index 50af9bf0..c127f746 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderSpringConfigurationTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderSpringConfigurationTest.kt @@ -12,17 +12,63 @@ import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.context.runner.ApplicationContextRunner import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import +import org.springframework.core.env.SystemEnvironmentPropertySource +import java.util.function.Supplier class LanguageProviderSpringConfigurationTest { private val runner = ApplicationContextRunner().withUserConfiguration(Providers::class.java) + private val defaultRunner = runner.withUserConfiguration(LanguageProviderProperties::class.java) @Test - @DisplayName("실제 Spring 설정 바인딩으로 감지만 순서를 바꾸어도 번역 순서는 유지한다") - fun shouldBindIndependentCapabilityOrders() { - listOf("papago,secondary" to "papago", "secondary,papago" to "secondary").forEach { (order, expected) -> - runner.withPropertyValues( - "sodalive.language-providers.detection-order=$order", - "sodalive.language-providers.translation-order=papago,secondary" + @DisplayName("과거 Spring 우선순위 property가 있어도 production 기본 순서는 바뀌지 않는다") + fun shouldIgnoreLegacyPropertyOverrides() { + defaultRunner.withPropertyValues( + "sodalive.language-providers.detection-order=secondary,papago", + "sodalive.language-providers.translation-order=secondary,papago" + ).run { context -> + assertNull(context.startupFailure) + val properties = context.getBean(LanguageProviderProperties::class.java) + assertEquals(listOf("papago"), properties.detectionOrder) + assertEquals(listOf("papago"), properties.translationOrder) + } + } + + @Test + @DisplayName("과거 우선순위 환경변수가 property로 연결돼도 production 기본 순서는 바뀌지 않는다") + fun shouldIgnoreLegacyEnvironmentOverrides() { + defaultRunner.withInitializer { context -> + context.environment.propertySources.addFirst( + SystemEnvironmentPropertySource( + "legacyProviderOrderEnvironment", + mapOf( + "LANGUAGE_PROVIDERS_DETECTION_ORDER" to "secondary,papago", + "LANGUAGE_PROVIDERS_TRANSLATION_ORDER" to "secondary,papago" + ) + ) + ) + }.withPropertyValues( + "sodalive.language-providers.detection-order=\${LANGUAGE_PROVIDERS_DETECTION_ORDER:papago}", + "sodalive.language-providers.translation-order=\${LANGUAGE_PROVIDERS_TRANSLATION_ORDER:papago}" + ).run { context -> + assertNull(context.startupFailure) + assertEquals("secondary,papago", context.environment.getProperty("sodalive.language-providers.detection-order")) + assertEquals("secondary,papago", context.environment.getProperty("sodalive.language-providers.translation-order")) + val properties = context.getBean(LanguageProviderProperties::class.java) + assertEquals(listOf("papago"), properties.detectionOrder) + assertEquals(listOf("papago"), properties.translationOrder) + } + } + + @Test + @DisplayName("Spring 테스트 bean의 감지 목록만 바꾸어도 번역 순서는 유지한다") + fun shouldInjectIndependentCapabilityOrders() { + listOf( + listOf("papago", "secondary") to "papago", + listOf("secondary", "papago") to "secondary" + ).forEach { (order, expected) -> + runner.withBean( + LanguageProviderProperties::class.java, + Supplier { LanguageProviderProperties(order, listOf("papago", "secondary")) } ).run { context -> assertNull(context.startupFailure) val detector = context.getBean(LanguageDetectionCacheService::class.java) @@ -37,7 +83,10 @@ class LanguageProviderSpringConfigurationTest { assertEquals(0, context.getBean("secondaryTranslation", RecordingTranslationProvider::class.java).requests.size) } } - runner.withPropertyValues("sodalive.language-providers.translation-order=secondary,papago").run { context -> + runner.withBean( + LanguageProviderProperties::class.java, + Supplier { LanguageProviderProperties(listOf("papago", "secondary"), listOf("secondary", "papago")) } + ).run { context -> assertNull(context.startupFailure) assertEquals( "papago", @@ -52,17 +101,34 @@ class LanguageProviderSpringConfigurationTest { } @Test - @DisplayName("Spring 기본값은 두 기능 모두 파파고이며 잘못된 설정은 시작 시 실패한다") - fun shouldDefaultToPapagoAndRejectInvalidConfigurationAtStartup() { - runner.run { context -> + @DisplayName("실제 production component의 Spring 기본 생성자는 두 기능 모두 파파고를 사용한다") + fun shouldDefaultToPapago() { + defaultRunner.run { context -> assertNull(context.startupFailure) val properties = context.getBean(LanguageProviderProperties::class.java) assertEquals(listOf("papago"), properties.detectionOrder) assertEquals(listOf("papago"), properties.translationOrder) + assertEquals( + "papago", + context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerName + ) + assertEquals( + "papago", + context.getBean(TranslationCoordinator::class.java) + .translate(TranslateRequest(listOf("source"), "ko", "en")).providerName + ) } - listOf("detection-order", "translation-order").forEach { key -> - listOf("", "papago,papago", "unknown").forEach { value -> - runner.withPropertyValues("sodalive.language-providers.$key=$value").run { context -> + } + + @Test + @DisplayName("Spring 테스트 bean의 빈 목록과 빈 이름과 중복 순서와 미등록 이름은 시작 시 실패한다") + fun shouldRejectInvalidOrdersAtStartup() { + listOf(emptyList(), listOf(""), listOf("papago", "papago"), listOf("unknown")).forEach { order -> + listOf( + LanguageProviderProperties(order, listOf("papago")), + LanguageProviderProperties(listOf("papago"), order) + ).forEach { properties -> + runner.withBean(LanguageProviderProperties::class.java, Supplier { properties }).run { context -> assertNotNull(context.startupFailure) } } @@ -70,7 +136,7 @@ class LanguageProviderSpringConfigurationTest { } @TestConfiguration - @Import(LanguageProviderProperties::class, TranslationCoordinator::class, LanguageDetectionCacheService::class) + @Import(TranslationCoordinator::class, LanguageDetectionCacheService::class) class Providers { @Bean fun memoryRepository(): TranslationMemoryRepository = Mockito.mock(TranslationMemoryRepository::class.java) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/TranslationCoordinatorTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/TranslationCoordinatorTest.kt index 8dbab3a3..555ddef2 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/TranslationCoordinatorTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/i18n/translation/TranslationCoordinatorTest.kt @@ -125,7 +125,7 @@ class TranslationCoordinatorTest { private fun coordinator( providers: List, order: List = providers.map { it.providerName } - ) = TranslationCoordinator(providers, LanguageProviderProperties(translationOrder = order.joinToString(",")), repository) + ) = TranslationCoordinator(providers, LanguageProviderProperties(listOf("papago"), order), repository) private fun memory(provider: String, text: String) = TranslationMemory( "hash",