refactor(i18n): 공급자 우선순위를 코드 목록으로 지정한다

This commit is contained in:
2026-11-09 18:22:50 +09:00
parent f3a558f659
commit 9136b0e3bb
8 changed files with 101 additions and 33 deletions
@@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.i18n.translation
object LanguageProviderOrder {
val detectionOrder = listOf("papago")
val translationOrder = listOf("papago")
}
@@ -1,17 +1,15 @@
package kr.co.vividnext.sodalive.i18n.translation 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 import org.springframework.stereotype.Component
@Component @Component
class LanguageProviderProperties( class LanguageProviderProperties(
@Value("\${sodalive.language-providers.detection-order:papago}") val detectionOrder: List<String>,
detectionOrder: String = "papago", val translationOrder: List<String>
@Value("\${sodalive.language-providers.translation-order:papago}")
translationOrder: String = "papago"
) { ) {
val detectionOrder = detectionOrder.split(',').map { it.trim() } @Autowired
val translationOrder = translationOrder.split(',').map { it.trim() } constructor() : this(LanguageProviderOrder.detectionOrder, LanguageProviderOrder.translationOrder)
fun <T> orderedProviders(order: List<String>, providers: List<T>, name: (T) -> String): List<T> { fun <T> orderedProviders(order: List<String>, providers: List<T>, name: (T) -> String): List<T> {
require(order.isNotEmpty() && order.none { it.isBlank() }) { "language provider order must not be empty" } require(order.isNotEmpty() && order.none { it.isBlank() }) { "language provider order must not be empty" }
-5
View File
@@ -19,11 +19,6 @@ management:
show-details: never show-details: never
show-components: never show-components: never
sodalive:
language-providers:
detection-order: ${LANGUAGE_PROVIDERS_DETECTION_ORDER:papago}
translation-order: ${LANGUAGE_PROVIDERS_TRANSLATION_ORDER:papago}
deployment: deployment:
artifact-version: ${DEPLOY_ARTIFACT_VERSION:local} artifact-version: ${DEPLOY_ARTIFACT_VERSION:local}
config-commit: ${DEPLOY_CONFIG_COMMIT:0000000000000000000000000000000000000000} config-commit: ${DEPLOY_CONFIG_COMMIT:0000000000000000000000000000000000000000}
@@ -92,7 +92,7 @@ class LanguageDetectionProviderTest {
} }
private fun service(providers: List<LanguageDetectionProvider>, order: List<String> = providers.map { it.providerName }) = private fun service(providers: List<LanguageDetectionProvider>, order: List<String> = providers.map { it.providerName }) =
LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(detectionOrder = order.joinToString(","))) LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(order, listOf("papago")))
} }
class RecordingDetectionProvider( class RecordingDetectionProvider(
@@ -35,16 +35,13 @@ import java.util.UUID
@DataJpaTest( @DataJpaTest(
properties = [ properties = [
"spring.cache.type=none", "spring.cache.type=none",
"spring.datasource.url=jdbc:h2:mem:language-provider-cache;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE", "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"
] ]
) )
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import( @Import(
QueryDslConfig::class, QueryDslConfig::class,
AudioContentThemeQueryRepository::class, AudioContentThemeQueryRepository::class,
LanguageProviderProperties::class,
LanguageDetectionCacheService::class, LanguageDetectionCacheService::class,
TranslationCoordinator::class, TranslationCoordinator::class,
TranslationJobWorker::class, TranslationJobWorker::class,
@@ -163,6 +160,12 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
@TestConfiguration @TestConfiguration
class Providers { class Providers {
@Bean
fun languageProviderProperties() = LanguageProviderProperties(
listOf("papago", "secondary"),
listOf("papago", "secondary")
)
@Bean @Bean
fun papagoDetection() = RecordingDetectionProvider("papago") { throw IllegalStateException("sensitive provider body") } fun papagoDetection() = RecordingDetectionProvider("papago") { throw IllegalStateException("sensitive provider body") }
@@ -9,7 +9,7 @@ class LanguageProviderPropertiesTest {
@Test @Test
@DisplayName("기능별 목록을 서로 독립된 설정 순서로 정렬한다") @DisplayName("기능별 목록을 서로 독립된 설정 순서로 정렬한다")
fun shouldOrderCapabilitiesIndependently() { fun shouldOrderCapabilitiesIndependently() {
val properties = LanguageProviderProperties("second,first", "first,second") val properties = LanguageProviderProperties(listOf("second", "first"), listOf("first", "second"))
assertEquals( assertEquals(
listOf("second", "first"), listOf("second", "first"),
properties.orderedProviders(properties.detectionOrder, listOf("first", "second")) { it } properties.orderedProviders(properties.detectionOrder, listOf("first", "second")) { it }
@@ -12,17 +12,63 @@ import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.test.context.runner.ApplicationContextRunner import org.springframework.boot.test.context.runner.ApplicationContextRunner
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Import import org.springframework.context.annotation.Import
import org.springframework.core.env.SystemEnvironmentPropertySource
import java.util.function.Supplier
class LanguageProviderSpringConfigurationTest { class LanguageProviderSpringConfigurationTest {
private val runner = ApplicationContextRunner().withUserConfiguration(Providers::class.java) private val runner = ApplicationContextRunner().withUserConfiguration(Providers::class.java)
private val defaultRunner = runner.withUserConfiguration(LanguageProviderProperties::class.java)
@Test @Test
@DisplayName("실제 Spring 설정 바인딩으로 감지만 순서를 바꾸어도 번역 순서는 유지한다") @DisplayName("과거 Spring 우선순위 property가 있어도 production 기본 순서는 바뀌지 않는다")
fun shouldBindIndependentCapabilityOrders() { fun shouldIgnoreLegacyPropertyOverrides() {
listOf("papago,secondary" to "papago", "secondary,papago" to "secondary").forEach { (order, expected) -> defaultRunner.withPropertyValues(
runner.withPropertyValues( "sodalive.language-providers.detection-order=secondary,papago",
"sodalive.language-providers.detection-order=$order", "sodalive.language-providers.translation-order=secondary,papago"
"sodalive.language-providers.translation-order=papago,secondary" ).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 -> ).run { context ->
assertNull(context.startupFailure) assertNull(context.startupFailure)
val detector = context.getBean(LanguageDetectionCacheService::class.java) val detector = context.getBean(LanguageDetectionCacheService::class.java)
@@ -37,7 +83,10 @@ class LanguageProviderSpringConfigurationTest {
assertEquals(0, context.getBean("secondaryTranslation", RecordingTranslationProvider::class.java).requests.size) 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) assertNull(context.startupFailure)
assertEquals( assertEquals(
"papago", "papago",
@@ -52,17 +101,34 @@ class LanguageProviderSpringConfigurationTest {
} }
@Test @Test
@DisplayName("Spring 기본값은 두 기능 모두 파파고이며 잘못된 설정은 시작 시 실패한다") @DisplayName("실제 production component의 Spring 기본 생성자는 두 기능 모두 파파고를 사용한다")
fun shouldDefaultToPapagoAndRejectInvalidConfigurationAtStartup() { fun shouldDefaultToPapago() {
runner.run { context -> defaultRunner.run { context ->
assertNull(context.startupFailure) assertNull(context.startupFailure)
val properties = context.getBean(LanguageProviderProperties::class.java) val properties = context.getBean(LanguageProviderProperties::class.java)
assertEquals(listOf("papago"), properties.detectionOrder) assertEquals(listOf("papago"), properties.detectionOrder)
assertEquals(listOf("papago"), properties.translationOrder) 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) assertNotNull(context.startupFailure)
} }
} }
@@ -70,7 +136,7 @@ class LanguageProviderSpringConfigurationTest {
} }
@TestConfiguration @TestConfiguration
@Import(LanguageProviderProperties::class, TranslationCoordinator::class, LanguageDetectionCacheService::class) @Import(TranslationCoordinator::class, LanguageDetectionCacheService::class)
class Providers { class Providers {
@Bean @Bean
fun memoryRepository(): TranslationMemoryRepository = Mockito.mock(TranslationMemoryRepository::class.java) fun memoryRepository(): TranslationMemoryRepository = Mockito.mock(TranslationMemoryRepository::class.java)
@@ -125,7 +125,7 @@ class TranslationCoordinatorTest {
private fun coordinator( private fun coordinator(
providers: List<TranslationProvider>, providers: List<TranslationProvider>,
order: List<String> = providers.map { it.providerName } order: List<String> = 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( private fun memory(provider: String, text: String) = TranslationMemory(
"hash", "hash",