refactor(i18n): 공급자 식별자를 enum으로 통일한다

This commit is contained in:
2026-11-09 19:14:19 +09:00
parent 6093b37747
commit ec890047a5
21 changed files with 228 additions and 148 deletions
@@ -17,6 +17,7 @@ import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.i18n.translation.DetectedLanguage
import kr.co.vividnext.sodalive.i18n.translation.LanguageDetectionProvider
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderProperties
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
@@ -730,7 +731,7 @@ class ControlledLanguageDetectionCacheService(
) : LanguageDetectionCacheService(
languageDetectionResultRepository,
listOf(object : LanguageDetectionProvider {
override val providerName = "papago"
override val providerId = LanguageProviderId.PAPAGO
override fun detect(query: String): String? = null
}),
LanguageProviderProperties()
@@ -759,15 +760,15 @@ class ControlledLanguageDetectionCacheService(
queries.add(query)
val concurrentDetection = concurrentDetections[query]
if (concurrentDetection != null) {
return DetectedLanguage(concurrentDetection.detect(), "papago")
return DetectedLanguage(concurrentDetection.detect(), LanguageProviderId.PAPAGO)
}
val blockingDetection = blockingDetections[query]
if (blockingDetection != null) {
blockingDetection.started.countDown()
check(blockingDetection.release.await(5, TimeUnit.SECONDS))
return DetectedLanguage(blockingDetection.detectedLanguage, "papago")
return DetectedLanguage(blockingDetection.detectedLanguage, LanguageProviderId.PAPAGO)
}
return detections[query]?.let { DetectedLanguage(it, "papago") }
return detections[query]?.let { DetectedLanguage(it, LanguageProviderId.PAPAGO) }
}
}
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.content
import kr.co.vividnext.sodalive.i18n.translation.DetectedLanguage
import kr.co.vividnext.sodalive.i18n.translation.LanguageDetectionProvider
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderProperties
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
import org.junit.jupiter.api.Assertions.assertEquals
@@ -36,7 +37,7 @@ class LanguageDetectionCacheServiceTest {
val detected = service.detectWithCache("Hello world")
assertEquals(DetectedLanguage("en", "papago"), detected)
assertEquals(DetectedLanguage("en", LanguageProviderId.PAPAGO), detected)
assertEquals(0, providerCalls)
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
sourceHash = sourceHash,
@@ -80,7 +81,7 @@ class LanguageDetectionCacheServiceTest {
val detected = service.detectWithCache("Hello world")
assertEquals(DetectedLanguage("ko", "papago"), detected)
assertEquals(DetectedLanguage("ko", LanguageProviderId.PAPAGO), detected)
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
sourceHash = sourceHash,
provider = "papago",
@@ -104,7 +105,7 @@ class LanguageDetectionCacheServiceTest {
private fun service(repository: LanguageDetectionResultRepository, detect: () -> String?): LanguageDetectionCacheService {
val provider = object : LanguageDetectionProvider {
override val providerName = "papago"
override val providerId = LanguageProviderId.PAPAGO
override fun detect(query: String) = detect()
}
return LanguageDetectionCacheService(repository, listOf(provider), LanguageProviderProperties())
@@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.i18n.translation
import kr.co.vividnext.sodalive.content.LanguageDetectionCacheService
import kr.co.vividnext.sodalive.content.LanguageDetectionResult
import kr.co.vividnext.sodalive.content.LanguageDetectionResultRepository
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.AWS
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.PAPAGO
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertSame
@@ -21,39 +23,39 @@ class LanguageDetectionProviderTest {
fun shouldFallbackAndReturnCanonicalWinnerWithActualProvider() {
val failures: List<() -> String?> = listOf({ throw IllegalStateException("sensitive body") }, { null }, { " " })
failures.forEach { failure ->
val first = RecordingDetectionProvider("first", failure)
val second = RecordingDetectionProvider("second") { "EN" }
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "second", "v1")).thenReturn("ko")
val first = RecordingDetectionProvider(PAPAGO, failure)
val second = RecordingDetectionProvider(AWS) { "EN" }
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "aws", "v1")).thenReturn("ko")
assertEquals(DetectedLanguage("ko", "second"), service(listOf(first, second)).detectWithCache(query))
assertEquals(DetectedLanguage("ko", AWS), service(listOf(first, second)).detectWithCache(query))
assertEquals(listOf(query), first.queries)
assertEquals(listOf(query), second.queries)
}
Mockito.verify(repository, Mockito.times(3)).insertIfAbsent(hash, "original source", "en", "second", null, "v1")
Mockito.verify(repository, Mockito.times(3)).insertIfAbsent(hash, "original source", "en", "aws", null, "v1")
}
@Test
@DisplayName("보조 공급자의 유효 캐시가 있으면 첫 공급자도 호출하지 않는다")
fun shouldCheckAllConfiguredCachesBeforeCallingAnyProvider() {
val first = RecordingDetectionProvider("first") { "ko" }
val second = RecordingDetectionProvider("second") { "en" }
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "second", "v1"))
.thenReturn(LanguageDetectionResult(hash, "sample", "ja", "second", null, "v1"))
val first = RecordingDetectionProvider(PAPAGO) { "ko" }
val second = RecordingDetectionProvider(AWS) { "en" }
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "aws", "v1"))
.thenReturn(LanguageDetectionResult(hash, "sample", "ja", "aws", null, "v1"))
assertEquals(DetectedLanguage("ja", "second"), service(listOf(first, second)).detectWithCache(query))
assertEquals(DetectedLanguage("ja", AWS), service(listOf(first, second)).detectWithCache(query))
assertEquals(emptyList<String>(), first.queries + second.queries)
}
@Test
@DisplayName("정상 미지원 언어 감지는 성공이며 후속 공급자를 호출하지 않는다")
fun shouldKeepUnsupportedDetectionAsSuccessAndHonorOrder() {
val first = RecordingDetectionProvider("first") { "ko" }
val second = RecordingDetectionProvider("second") { "FR" }
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "second", "v1")).thenReturn("fr")
val first = RecordingDetectionProvider(PAPAGO) { "ko" }
val second = RecordingDetectionProvider(AWS) { "FR" }
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "aws", "v1")).thenReturn("fr")
assertEquals(
DetectedLanguage("fr", "second"),
service(listOf(first, second), listOf("second", "first")).detectWithCache(query)
DetectedLanguage("fr", AWS),
service(listOf(first, second), listOf(AWS, PAPAGO)).detectWithCache(query)
)
assertEquals(emptyList<String>(), first.queries)
assertEquals(listOf(query), second.queries)
@@ -62,8 +64,8 @@ class LanguageDetectionProviderTest {
@Test
@DisplayName("감지 전체 실패와 공백 입력은 null이고 공백은 저장소와 공급자를 호출하지 않는다")
fun shouldReturnNullAfterAllFailuresAndSkipBlankInput() {
val first = RecordingDetectionProvider("first") { null }
val second = RecordingDetectionProvider("second") { null }
val first = RecordingDetectionProvider(PAPAGO) { null }
val second = RecordingDetectionProvider(AWS) { null }
val service = service(listOf(first, second))
assertNull(service.detectWithCache(" \n"))
Mockito.verifyNoInteractions(repository)
@@ -75,28 +77,30 @@ class LanguageDetectionProviderTest {
@Test
@DisplayName("감지 저장소 조회와 저장 오류는 다음 공급자로 전환하지 않고 전파한다")
fun shouldPropagateRepositoryFailuresWithoutFallback() {
val first = RecordingDetectionProvider("first") { "ko" }
val second = RecordingDetectionProvider("second") { "en" }
val first = RecordingDetectionProvider(PAPAGO) { "ko" }
val second = RecordingDetectionProvider(AWS) { "en" }
val service = service(listOf(first, second))
val failure = IllegalStateException("database unavailable")
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "first", "v1"))
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "papago", "v1"))
.thenThrow(failure)
assertSame(failure, assertThrows(IllegalStateException::class.java) { service.detectWithCache(query) })
assertEquals(emptyList<String>(), first.queries + second.queries)
Mockito.reset(repository)
Mockito.`when`(repository.insertIfAbsent(hash, "original source", "ko", "first", null, "v1")).thenThrow(failure)
Mockito.`when`(repository.insertIfAbsent(hash, "original source", "ko", "papago", null, "v1")).thenThrow(failure)
assertSame(failure, assertThrows(IllegalStateException::class.java) { service.detectWithCache(query) })
assertEquals(listOf(query), first.queries)
assertEquals(emptyList<String>(), second.queries)
}
private fun service(providers: List<LanguageDetectionProvider>, order: List<String> = providers.map { it.providerName }) =
LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(order, listOf("papago")))
private fun service(
providers: List<LanguageDetectionProvider>,
order: List<LanguageProviderId> = providers.map { it.providerId }
) = LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(order, listOf(PAPAGO)))
}
class RecordingDetectionProvider(
override val providerName: String,
override val providerId: LanguageProviderId,
private val response: () -> String?
) : LanguageDetectionProvider {
val queries = mutableListOf<String>()
@@ -6,6 +6,8 @@ import kr.co.vividnext.sodalive.content.LanguageDetectionResultRepository
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityRepository
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.AWS
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.PAPAGO
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
@@ -86,16 +88,16 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
@DisplayName("보조 공급자의 감지를 H2에 귀속 저장하고 외부 호출 없이 캐시를 재사용한다")
fun shouldPersistSecondaryDetectionAndReuseCache() {
val source = "provider source ${UUID.randomUUID()}"
assertEquals(DetectedLanguage("ko", "secondary"), detector.detectWithCache(source))
assertEquals(DetectedLanguage("ko", "secondary"), detector.detectWithCache(source))
assertEquals(DetectedLanguage("ko", AWS), detector.detectWithCache(source))
assertEquals(DetectedLanguage("ko", AWS), detector.detectWithCache(source))
val detection = transaction.execute {
detectionRepository.findBySourceHashAndProviderAndNormalizationVersion(
SourceTextNormalizer.hash(source),
"secondary",
"aws",
"v1"
)
}!!
assertEquals("secondary", detection.provider)
assertEquals("aws", detection.provider)
assertEquals("ko", detection.detectedLanguage)
assertEquals(1, papagoDetection.queries.size)
assertEquals(1, secondaryDetection.queries.size)
@@ -113,19 +115,19 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
SourceTextNormalizer.hash(source),
"ko",
"en",
"secondary",
"aws",
"v1"
)
}
assertNotNull(memory)
assertEquals("secondary", memory!!.provider)
assertEquals("secondary-v1", memory.providerVersion)
assertEquals("secondary:0", memory.translatedText)
assertEquals("aws", memory!!.provider)
assertEquals("AWS-v1", memory.providerVersion)
assertEquals("AWS:0", memory.translatedText)
assertEquals(
TranslationJobStatus.COMPLETED,
transaction.execute { jobRepository.findAll().single { it.resourceId == postId }.status }
)
assertEquals("secondary:0", displayService.findDisplayContents(listOf(postId), "en")[postId]?.content)
assertEquals("AWS:0", displayService.findDisplayContents(listOf(postId), "en")[postId]?.content)
transaction.executeWithoutResult {
translations.deleteAll()
@@ -137,7 +139,7 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
assertEquals(1, papagoTranslation.requests.size)
assertEquals(1, secondaryTranslation.requests.size)
assertEquals(listOf(source), secondaryTranslation.requests.single().texts)
assertEquals("secondary:0", displayService.findDisplayContents(listOf(postId), "en")[postId]?.content)
assertEquals("AWS:0", displayService.findDisplayContents(listOf(postId), "en")[postId]?.content)
}
private fun schedule(postId: Long, source: String) {
@@ -162,20 +164,20 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
class Providers {
@Bean
fun languageProviderProperties() = LanguageProviderProperties(
listOf("papago", "secondary"),
listOf("papago", "secondary")
listOf(PAPAGO, AWS),
listOf(PAPAGO, AWS)
)
@Bean
fun papagoDetection() = RecordingDetectionProvider("papago") { throw IllegalStateException("sensitive provider body") }
fun papagoDetection() = RecordingDetectionProvider(PAPAGO) { throw IllegalStateException("sensitive provider body") }
@Bean
fun secondaryDetection() = RecordingDetectionProvider("secondary") { "ko" }
fun secondaryDetection() = RecordingDetectionProvider(AWS) { "ko" }
@Bean
fun papagoTranslation() = RecordingTranslationProvider("papago") { TranslateResult(emptyList()) }
fun papagoTranslation() = RecordingTranslationProvider(PAPAGO) { TranslateResult(emptyList()) }
@Bean
fun secondaryTranslation() = RecordingTranslationProvider("secondary")
fun secondaryTranslation() = RecordingTranslationProvider(AWS)
}
}
@@ -0,0 +1,56 @@
package kr.co.vividnext.sodalive.i18n.translation
import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.function.Executable
class LanguageProviderIdTest {
@Test
@DisplayName("공급자 인터페이스와 성공 결과의 런타임 식별자는 enum이다")
fun shouldExposeEnumProviderIdentityOnInterfacesAndResults() {
assertAll(
*listOf(
LanguageDetectionProvider::class.java,
TranslationProvider::class.java,
DetectedLanguage::class.java,
ProviderTranslationResult::class.java
).map { contract ->
Executable {
val getter = contract.methods.firstOrNull { it.name == "getProviderId" }
assertEquals(
"kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId",
getter?.returnType?.name,
"${contract.simpleName}.getProviderId must return LanguageProviderId"
)
assertTrue(getter?.returnType?.isEnum == true)
assertFalse(contract.methods.any { it.name == "getProviderName" })
}
}.toTypedArray()
)
}
@Test
@DisplayName("공급자의 저장 code는 기존 소문자 값으로 고정한다")
fun shouldKeepStablePersistenceCodes() {
assertEquals("papago", LanguageProviderId.PAPAGO.code)
assertEquals("aws", LanguageProviderId.AWS.code)
assertEquals(listOf(LanguageProviderId.PAPAGO, LanguageProviderId.AWS), LanguageProviderId.values().toList())
}
@Test
@DisplayName("production 코드 목록과 실제 Papago adapter는 PAPAGO enum을 사용한다")
fun shouldUsePapagoEnumForProductionDefaultsAndAdapters() {
assertEquals(listOf(LanguageProviderId.PAPAGO), LanguageProviderOrder.detectionOrder)
assertEquals(listOf(LanguageProviderId.PAPAGO), LanguageProviderOrder.translationOrder)
assertEquals(listOf(LanguageProviderId.PAPAGO), LanguageProviderProperties().detectionOrder)
assertEquals(listOf(LanguageProviderId.PAPAGO), LanguageProviderProperties().translationOrder)
assertEquals(LanguageProviderId.PAPAGO, PapagoLanguageDetectionProvider("test-id", "test-secret").providerId)
val translator = PapagoTranslationService("test-id", "test-secret")
assertEquals(LanguageProviderId.PAPAGO, translator.providerId)
assertEquals("nmt-v1", translator.providerVersion)
}
}
@@ -1,5 +1,7 @@
package kr.co.vividnext.sodalive.i18n.translation
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.AWS
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.PAPAGO
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName
@@ -9,33 +11,33 @@ class LanguageProviderPropertiesTest {
@Test
@DisplayName("기능별 목록을 서로 독립된 설정 순서로 정렬한다")
fun shouldOrderCapabilitiesIndependently() {
val properties = LanguageProviderProperties(listOf("second", "first"), listOf("first", "second"))
val properties = LanguageProviderProperties(listOf(AWS, PAPAGO), listOf(PAPAGO, AWS))
assertEquals(
listOf("second", "first"),
properties.orderedProviders(properties.detectionOrder, listOf("first", "second")) { it }
listOf(AWS, PAPAGO),
properties.orderedProviders(properties.detectionOrder, listOf(PAPAGO, AWS)) { it }
)
assertEquals(
listOf("first", "second"),
properties.orderedProviders(properties.translationOrder, listOf("second", "first")) { it }
listOf(PAPAGO, AWS),
properties.orderedProviders(properties.translationOrder, listOf(AWS, PAPAGO)) { it }
)
}
@Test
@DisplayName("빈 순서와 빈 이름과 중복 순서와 미등록 공급자는 초기화에서 거부한다")
@DisplayName("빈 순서와 중복 순서와 미등록 AWS 공급자는 초기화에서 거부한다")
fun shouldRejectInvalidOrders() {
val properties = LanguageProviderProperties()
listOf(emptyList(), listOf(""), listOf("first", "first"), listOf("unknown")).forEach { order ->
listOf(emptyList(), listOf(PAPAGO, PAPAGO), listOf(AWS)).forEach { order ->
assertThrows(IllegalArgumentException::class.java) {
properties.orderedProviders(order, listOf("first", "second")) { it }
properties.orderedProviders(order, listOf(PAPAGO)) { it }
}
}
}
@Test
@DisplayName("같은 기능 안에서 중복 등록된 공급자 이름은 초기화에서 거부한다")
fun shouldRejectDuplicateRegisteredNamesEvenWhenNotSelected() {
@DisplayName("같은 기능 안에서 선택되지 않은 공급자도 중복 등록을 거부한다")
fun shouldRejectDuplicateRegisteredIdsEvenWhenNotSelected() {
assertThrows(IllegalArgumentException::class.java) {
LanguageProviderProperties().orderedProviders(listOf("papago"), listOf("papago", "other", "other")) { it }
LanguageProviderProperties().orderedProviders(listOf(PAPAGO), listOf(PAPAGO, AWS, AWS)) { it }
}
}
}
@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.i18n.translation
import kr.co.vividnext.sodalive.content.LanguageDetectionCacheService
import kr.co.vividnext.sodalive.content.LanguageDetectionResultRepository
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.AWS
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.PAPAGO
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
@@ -16,7 +18,8 @@ import org.springframework.core.env.SystemEnvironmentPropertySource
import java.util.function.Supplier
class LanguageProviderSpringConfigurationTest {
private val runner = ApplicationContextRunner().withUserConfiguration(Providers::class.java)
private val papagoRunner = ApplicationContextRunner().withUserConfiguration(Providers::class.java)
private val runner = papagoRunner.withUserConfiguration(AwsProviders::class.java)
private val defaultRunner = runner.withUserConfiguration(LanguageProviderProperties::class.java)
@Test
@@ -28,8 +31,8 @@ class LanguageProviderSpringConfigurationTest {
).run { context ->
assertNull(context.startupFailure)
val properties = context.getBean(LanguageProviderProperties::class.java)
assertEquals(listOf("papago"), properties.detectionOrder)
assertEquals(listOf("papago"), properties.translationOrder)
assertEquals(listOf(PAPAGO), properties.detectionOrder)
assertEquals(listOf(PAPAGO), properties.translationOrder)
}
}
@@ -54,8 +57,8 @@ class LanguageProviderSpringConfigurationTest {
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)
assertEquals(listOf(PAPAGO), properties.detectionOrder)
assertEquals(listOf(PAPAGO), properties.translationOrder)
}
}
@@ -63,39 +66,39 @@ class LanguageProviderSpringConfigurationTest {
@DisplayName("Spring 테스트 bean의 감지 목록만 바꾸어도 번역 순서는 유지한다")
fun shouldInjectIndependentCapabilityOrders() {
listOf(
listOf("papago", "secondary") to "papago",
listOf("secondary", "papago") to "secondary"
listOf(PAPAGO, AWS) to PAPAGO,
listOf(AWS, PAPAGO) to AWS
).forEach { (order, expected) ->
runner.withBean(
LanguageProviderProperties::class.java,
Supplier { LanguageProviderProperties(order, listOf("papago", "secondary")) }
Supplier { LanguageProviderProperties(order, listOf(PAPAGO, AWS)) }
).run { context ->
assertNull(context.startupFailure)
val detector = context.getBean(LanguageDetectionCacheService::class.java)
assertEquals(expected, detector.detectWithCache("source")?.providerName)
assertEquals(expected, detector.detectWithCache("source")?.providerId)
assertEquals(
"papago",
PAPAGO,
context.getBean(TranslationCoordinator::class.java)
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerName
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerId
)
val secondary = context.getBean("secondaryDetection", RecordingDetectionProvider::class.java)
assertEquals(if (expected == "secondary") 1 else 0, secondary.queries.size)
assertEquals(if (expected == AWS) 1 else 0, secondary.queries.size)
assertEquals(0, context.getBean("secondaryTranslation", RecordingTranslationProvider::class.java).requests.size)
}
}
runner.withBean(
LanguageProviderProperties::class.java,
Supplier { LanguageProviderProperties(listOf("papago", "secondary"), listOf("secondary", "papago")) }
Supplier { LanguageProviderProperties(listOf(PAPAGO, AWS), listOf(AWS, PAPAGO)) }
).run { context ->
assertNull(context.startupFailure)
assertEquals(
"papago",
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerName
PAPAGO,
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerId
)
assertEquals(
"secondary",
AWS,
context.getBean(TranslationCoordinator::class.java)
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerName
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerId
)
}
}
@@ -106,29 +109,29 @@ class LanguageProviderSpringConfigurationTest {
defaultRunner.run { context ->
assertNull(context.startupFailure)
val properties = context.getBean(LanguageProviderProperties::class.java)
assertEquals(listOf("papago"), properties.detectionOrder)
assertEquals(listOf("papago"), properties.translationOrder)
assertEquals(listOf(PAPAGO), properties.detectionOrder)
assertEquals(listOf(PAPAGO), properties.translationOrder)
assertEquals(
"papago",
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerName
PAPAGO,
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerId
)
assertEquals(
"papago",
PAPAGO,
context.getBean(TranslationCoordinator::class.java)
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerName
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerId
)
}
}
@Test
@DisplayName("Spring 테스트 bean의 빈 목록과 빈 이름과 중복 순서와 미등록 이름은 시작 시 실패한다")
@DisplayName("Spring 테스트 bean의 빈 목록과 중복 순서와 해당 기능의 AWS 구현체 부재는 시작 시 실패한다")
fun shouldRejectInvalidOrdersAtStartup() {
listOf(emptyList(), listOf(""), listOf("papago", "papago"), listOf("unknown")).forEach { order ->
listOf(emptyList(), listOf(PAPAGO, PAPAGO), listOf(AWS)).forEach { order ->
listOf(
LanguageProviderProperties(order, listOf("papago")),
LanguageProviderProperties(listOf("papago"), order)
LanguageProviderProperties(order, listOf(PAPAGO)),
LanguageProviderProperties(listOf(PAPAGO), order)
).forEach { properties ->
runner.withBean(LanguageProviderProperties::class.java, Supplier { properties }).run { context ->
papagoRunner.withBean(LanguageProviderProperties::class.java, Supplier { properties }).run { context ->
assertNotNull(context.startupFailure)
}
}
@@ -144,7 +147,7 @@ class LanguageProviderSpringConfigurationTest {
@Bean
fun detectionRepository(): LanguageDetectionResultRepository {
val repository = Mockito.mock(LanguageDetectionResultRepository::class.java)
listOf("papago", "secondary").forEach { provider ->
listOf("papago", "aws").forEach { provider ->
Mockito.`when`(repository.findDetectedLanguageForUpdate(SourceTextNormalizer.hash("source"), provider, "v1"))
.thenReturn("ko")
}
@@ -152,15 +155,18 @@ class LanguageProviderSpringConfigurationTest {
}
@Bean
fun papagoDetection() = RecordingDetectionProvider("papago") { "ko" }
fun papagoDetection() = RecordingDetectionProvider(PAPAGO) { "ko" }
@Bean
fun secondaryDetection() = RecordingDetectionProvider("secondary") { "ko" }
fun papagoTranslation() = RecordingTranslationProvider(PAPAGO)
}
@TestConfiguration
class AwsProviders {
@Bean
fun secondaryDetection() = RecordingDetectionProvider(AWS) { "ko" }
@Bean
fun papagoTranslation() = RecordingTranslationProvider("papago")
@Bean
fun secondaryTranslation() = RecordingTranslationProvider("secondary")
fun secondaryTranslation() = RecordingTranslationProvider(AWS)
}
}
@@ -1,5 +1,7 @@
package kr.co.vividnext.sodalive.i18n.translation
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.AWS
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.PAPAGO
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertSame
@@ -15,15 +17,15 @@ class TranslationCoordinatorTest {
@Test
@DisplayName("설정 순서의 첫 성공 공급자와 버전을 반환하고 후속 호출을 멈춘다")
fun shouldUseConfiguredOrderAndStopAfterSuccess() {
val first = RecordingTranslationProvider("first")
val second = RecordingTranslationProvider("second")
val coordinator = coordinator(listOf(first, second), listOf("second", "first"))
val first = RecordingTranslationProvider(PAPAGO)
val second = RecordingTranslationProvider(AWS)
val coordinator = coordinator(listOf(first, second), listOf(AWS, PAPAGO))
val result = coordinator.translate(request)
assertEquals("second", result.providerName)
assertEquals("second-v1", result.providerVersion)
assertEquals(listOf("second:0", "second:1"), result.result.translatedText)
assertEquals(AWS, result.providerId)
assertEquals("AWS-v1", result.providerVersion)
assertEquals(listOf("AWS:0", "AWS:1"), result.result.translatedText)
assertEquals(listOf(request), second.requests)
assertEquals(emptyList<TranslateRequest>(), first.requests)
}
@@ -37,11 +39,11 @@ class TranslationCoordinatorTest {
{ TranslateResult(listOf("translated", " ")) }
)
failures.forEach { failure ->
val first = RecordingTranslationProvider("first", failure)
val second = RecordingTranslationProvider("second")
val first = RecordingTranslationProvider(PAPAGO, failure)
val second = RecordingTranslationProvider(AWS)
val coordinator = coordinator(listOf(first, second))
assertEquals("second", coordinator.translate(request).providerName)
assertEquals(AWS, coordinator.translate(request).providerId)
assertEquals(listOf(request), first.requests)
assertEquals(listOf(request), second.requests)
}
@@ -50,8 +52,8 @@ class TranslationCoordinatorTest {
@Test
@DisplayName("전체 공급자 실패는 원문이나 원시 예외 원인 없이 고정 메시지를 반환한다")
fun shouldSanitizeAllProviderFailures() {
val first = RecordingTranslationProvider("first") { throw IllegalStateException("sensitive response body") }
val second = RecordingTranslationProvider("second") { TranslateResult(emptyList()) }
val first = RecordingTranslationProvider(PAPAGO) { throw IllegalStateException("sensitive response body") }
val second = RecordingTranslationProvider(AWS) { TranslateResult(emptyList()) }
val error = assertThrows(IllegalStateException::class.java) {
coordinator(listOf(first, second)).translate(request)
@@ -66,7 +68,7 @@ class TranslationCoordinatorTest {
@Test
@DisplayName("빈 목록과 공백과 동일 언어와 미지원 언어는 공급자 호출 전에 거부한다")
fun shouldRejectInvalidRequestsBeforeCallingProviders() {
val provider = RecordingTranslationProvider("first")
val provider = RecordingTranslationProvider(PAPAGO)
val coordinator = coordinator(listOf(provider))
listOf(
request.copy(texts = emptyList()),
@@ -85,17 +87,17 @@ class TranslationCoordinatorTest {
@Test
@DisplayName("모든 설정 공급자 캐시를 호출보다 먼저 조회하고 순서상 첫 유효 캐시를 선택한다")
fun shouldFindSecondaryCacheAndPreferConfiguredFirstValidCache() {
val first = RecordingTranslationProvider("first")
val second = RecordingTranslationProvider("second")
val first = RecordingTranslationProvider(PAPAGO)
val second = RecordingTranslationProvider(AWS)
val coordinator = coordinator(listOf(first, second))
val secondary = memory("second", "cached secondary")
givenMemory("second", secondary)
val secondary = memory("aws", "cached secondary")
givenMemory("aws", secondary)
assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
givenMemory("first", memory("first", " "))
givenMemory("papago", memory("papago", " "))
assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
val primary = memory("first", "cached primary")
givenMemory("first", primary)
val primary = memory("papago", "cached primary")
givenMemory("papago", primary)
assertSame(primary, coordinator.findCached("hash", "ko", "en"))
assertEquals(emptyList<TranslateRequest>(), first.requests + second.requests)
}
@@ -103,14 +105,14 @@ class TranslationCoordinatorTest {
@Test
@DisplayName("캐시 저장소 오류는 공급자 실패로 숨기지 않는다")
fun shouldPropagateCacheRepositoryFailure() {
val provider = RecordingTranslationProvider("first")
val provider = RecordingTranslationProvider(PAPAGO)
val failure = IllegalStateException("database unavailable")
Mockito.`when`(
repository.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion(
"hash",
"ko",
"en",
"first",
"papago",
"v1"
)
).thenThrow(failure)
@@ -124,8 +126,8 @@ class TranslationCoordinatorTest {
private fun coordinator(
providers: List<TranslationProvider>,
order: List<String> = providers.map { it.providerName }
) = TranslationCoordinator(providers, LanguageProviderProperties(listOf("papago"), order), repository)
order: List<LanguageProviderId> = providers.map { it.providerId }
) = TranslationCoordinator(providers, LanguageProviderProperties(listOf(PAPAGO), order), repository)
private fun memory(provider: String, text: String) = TranslationMemory(
"hash",
@@ -151,12 +153,12 @@ class TranslationCoordinatorTest {
}
class RecordingTranslationProvider(
override val providerName: String,
override val providerId: LanguageProviderId,
private val response: (TranslateRequest) -> TranslateResult = { request ->
TranslateResult(request.texts.indices.map { "$providerName:$it" })
TranslateResult(request.texts.indices.map { "$providerId:$it" })
}
) : TranslationProvider {
override val providerVersion = "$providerName-v1"
override val providerVersion = "$providerId-v1"
val requests = mutableListOf<TranslateRequest>()
override fun translate(request: TranslateRequest): TranslateResult {
@@ -236,7 +236,7 @@ class TranslationJobWorkerTest {
private fun successfulProvider(): TranslationProvider {
return object : TranslationProvider {
override val providerName: String = "papago"
override val providerId: LanguageProviderId = LanguageProviderId.PAPAGO
override val providerVersion: String = "nmt-v1"
override fun translate(request: TranslateRequest): TranslateResult {
@@ -247,7 +247,7 @@ class TranslationJobWorkerTest {
private fun failingProvider(): TranslationProvider {
return object : TranslationProvider {
override val providerName: String = "papago"
override val providerId: LanguageProviderId = LanguageProviderId.PAPAGO
override val providerVersion: String = "nmt-v1"
override fun translate(request: TranslateRequest): TranslateResult {