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
@@ -92,7 +92,7 @@ class LanguageDetectionProviderTest {
}
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(
@@ -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") }
@@ -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 }
@@ -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)
@@ -125,7 +125,7 @@ class TranslationCoordinatorTest {
private fun coordinator(
providers: List<TranslationProvider>,
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(
"hash",