AWS 번역 공급자 추가 #469

Merged
klaus merged 12 commits from test into main 2026-11-10 05:25:25 +00:00
21 changed files with 228 additions and 148 deletions
Showing only changes of commit ec890047a5 - Show all commits
@@ -13,7 +13,7 @@ class LanguageDetectionCacheService(
providers: List<LanguageDetectionProvider>, providers: List<LanguageDetectionProvider>,
properties: LanguageProviderProperties properties: LanguageProviderProperties
) { ) {
private val providers = properties.orderedProviders(properties.detectionOrder, providers) { it.providerName } private val providers = properties.orderedProviders(properties.detectionOrder, providers) { it.providerId }
@Transactional @Transactional
fun detectWithCache(query: String): DetectedLanguage? { fun detectWithCache(query: String): DetectedLanguage? {
@@ -24,11 +24,11 @@ class LanguageDetectionCacheService(
providers.forEach { provider -> providers.forEach { provider ->
val cached = languageDetectionResultRepository.findBySourceHashAndProviderAndNormalizationVersion( val cached = languageDetectionResultRepository.findBySourceHashAndProviderAndNormalizationVersion(
sourceHash = sourceHash, sourceHash = sourceHash,
provider = provider.providerName, provider = provider.providerId.code,
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
) )
if (cached != null && cached.detectedLanguage.isNotBlank()) { if (cached != null && cached.detectedLanguage.isNotBlank()) {
return DetectedLanguage(cached.detectedLanguage, provider.providerName) return DetectedLanguage(cached.detectedLanguage, provider.providerId)
} }
} }
@@ -42,18 +42,18 @@ class LanguageDetectionCacheService(
sourceHash = sourceHash, sourceHash = sourceHash,
sourceTextSample = normalizedQuery.take(MAX_SAMPLE_LENGTH), sourceTextSample = normalizedQuery.take(MAX_SAMPLE_LENGTH),
detectedLanguage = detectedLanguage, detectedLanguage = detectedLanguage,
provider = provider.providerName, provider = provider.providerId.code,
confidence = null, confidence = null,
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
) )
val winner = requireNotNull( val winner = requireNotNull(
languageDetectionResultRepository.findDetectedLanguageForUpdate( languageDetectionResultRepository.findDetectedLanguageForUpdate(
sourceHash = sourceHash, sourceHash = sourceHash,
provider = provider.providerName, provider = provider.providerId.code,
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
) )
) )
return DetectedLanguage(winner, provider.providerName) return DetectedLanguage(winner, provider.providerId)
} }
return null return null
} }
@@ -1,3 +1,3 @@
package kr.co.vividnext.sodalive.i18n.translation package kr.co.vividnext.sodalive.i18n.translation
data class DetectedLanguage(val detectedLanguage: String, val providerName: String) data class DetectedLanguage(val detectedLanguage: String, val providerId: LanguageProviderId)
@@ -1,7 +1,7 @@
package kr.co.vividnext.sodalive.i18n.translation package kr.co.vividnext.sodalive.i18n.translation
interface LanguageDetectionProvider { interface LanguageDetectionProvider {
val providerName: String val providerId: LanguageProviderId
fun detect(query: String): String? fun detect(query: String): String?
} }
@@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.i18n.translation
enum class LanguageProviderId(val code: String) {
PAPAGO("papago"),
AWS("aws")
}
@@ -1,6 +1,6 @@
package kr.co.vividnext.sodalive.i18n.translation package kr.co.vividnext.sodalive.i18n.translation
object LanguageProviderOrder { object LanguageProviderOrder {
val detectionOrder = listOf("papago") val detectionOrder = listOf(LanguageProviderId.PAPAGO)
val translationOrder = listOf("papago") val translationOrder = listOf(LanguageProviderId.PAPAGO)
} }
@@ -5,18 +5,18 @@ import org.springframework.stereotype.Component
@Component @Component
class LanguageProviderProperties( class LanguageProviderProperties(
val detectionOrder: List<String>, val detectionOrder: List<LanguageProviderId>,
val translationOrder: List<String> val translationOrder: List<LanguageProviderId>
) { ) {
@Autowired @Autowired
constructor() : this(LanguageProviderOrder.detectionOrder, LanguageProviderOrder.translationOrder) 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<LanguageProviderId>, providers: List<T>, id: (T) -> LanguageProviderId): List<T> {
require(order.isNotEmpty() && order.none { it.isBlank() }) { "language provider order must not be empty" } require(order.isNotEmpty()) { "language provider order must not be empty" }
require(order.distinct().size == order.size) { "duplicate language provider order" } require(order.distinct().size == order.size) { "duplicate language provider order" }
val byName = providers.associateBy(name) val byId = providers.associateBy(id)
require(byName.size == providers.size) { "duplicate registered language provider name" } require(byId.size == providers.size) { "duplicate registered language provider name" }
require(order.all { it in byName }) { "unknown language provider in order" } require(order.all { it in byId }) { "unknown language provider in order" }
return order.map { byName.getValue(it) } return order.map { byId.getValue(it) }
} }
} }
@@ -16,7 +16,7 @@ class PapagoLanguageDetectionProvider(
private val papagoClientSecret: String private val papagoClientSecret: String
) : LanguageDetectionProvider { ) : LanguageDetectionProvider {
private val restTemplate = RestTemplate() private val restTemplate = RestTemplate()
override val providerName = "papago" override val providerId = LanguageProviderId.PAPAGO
override fun detect(query: String): String? { override fun detect(query: String): String? {
val headers = HttpHeaders().apply { val headers = HttpHeaders().apply {
@@ -20,7 +20,7 @@ class PapagoTranslationService(
private val papagoTranslateUrl = "https://papago.apigw.ntruss.com/nmt/v1/translation" private val papagoTranslateUrl = "https://papago.apigw.ntruss.com/nmt/v1/translation"
override val providerName: String = "papago" override val providerId: LanguageProviderId = LanguageProviderId.PAPAGO
override val providerVersion: String = "nmt-v1" override val providerVersion: String = "nmt-v1"
@@ -2,6 +2,6 @@ package kr.co.vividnext.sodalive.i18n.translation
data class ProviderTranslationResult( data class ProviderTranslationResult(
val result: TranslateResult, val result: TranslateResult,
val providerName: String, val providerId: LanguageProviderId,
val providerVersion: String val providerVersion: String
) )
@@ -8,7 +8,7 @@ class TranslationCoordinator(
properties: LanguageProviderProperties, properties: LanguageProviderProperties,
private val translationMemoryRepository: TranslationMemoryRepository private val translationMemoryRepository: TranslationMemoryRepository
) { ) {
private val providers = properties.orderedProviders(properties.translationOrder, providers) { it.providerName } private val providers = properties.orderedProviders(properties.translationOrder, providers) { it.providerId }
fun findCached(sourceHash: String, sourceLanguage: String, targetLanguage: String): TranslationMemory? { fun findCached(sourceHash: String, sourceLanguage: String, targetLanguage: String): TranslationMemory? {
providers.forEach { provider -> providers.forEach { provider ->
@@ -17,7 +17,7 @@ class TranslationCoordinator(
sourceHash, sourceHash,
sourceLanguage, sourceLanguage,
targetLanguage, targetLanguage,
provider.providerName, provider.providerId.code,
SourceTextNormalizer.NORMALIZATION_VERSION SourceTextNormalizer.NORMALIZATION_VERSION
) )
if (memory != null && memory.translatedText.isNotBlank()) return memory if (memory != null && memory.translatedText.isNotBlank()) return memory
@@ -42,7 +42,7 @@ class TranslationCoordinator(
result != null && result.translatedText.size == request.texts.size && result != null && result.translatedText.size == request.texts.size &&
result.translatedText.none { it.isBlank() } result.translatedText.none { it.isBlank() }
) { ) {
return ProviderTranslationResult(result, provider.providerName, provider.providerVersion) return ProviderTranslationResult(result, provider.providerId, provider.providerVersion)
} }
} }
throw IllegalStateException("all translation providers failed") throw IllegalStateException("all translation providers failed")
@@ -87,7 +87,7 @@ class TranslationJobWorker(
sourceHash = job.sourceHash, sourceHash = job.sourceHash,
sourceLanguage = job.sourceLanguage, sourceLanguage = job.sourceLanguage,
targetLanguage = job.targetLanguage, targetLanguage = job.targetLanguage,
provider = response.providerName, provider = response.providerId.code,
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
) )
if (memory == null) { if (memory == null) {
@@ -98,7 +98,7 @@ class TranslationJobWorker(
sourceLanguage = job.sourceLanguage, sourceLanguage = job.sourceLanguage,
targetLanguage = job.targetLanguage, targetLanguage = job.targetLanguage,
translatedText = translated, translatedText = translated,
provider = response.providerName, provider = response.providerId.code,
providerVersion = response.providerVersion providerVersion = response.providerVersion
) )
) )
@@ -1,7 +1,7 @@
package kr.co.vividnext.sodalive.i18n.translation package kr.co.vividnext.sodalive.i18n.translation
interface TranslationProvider { interface TranslationProvider {
val providerName: String val providerId: LanguageProviderId
val providerVersion: String val providerVersion: String
fun translate(request: TranslateRequest): TranslateResult fun translate(request: TranslateRequest): TranslateResult
@@ -17,6 +17,7 @@ import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.i18n.translation.DetectedLanguage import kr.co.vividnext.sodalive.i18n.translation.DetectedLanguage
import kr.co.vividnext.sodalive.i18n.translation.LanguageDetectionProvider 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.LanguageProviderProperties
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
@@ -730,7 +731,7 @@ class ControlledLanguageDetectionCacheService(
) : LanguageDetectionCacheService( ) : LanguageDetectionCacheService(
languageDetectionResultRepository, languageDetectionResultRepository,
listOf(object : LanguageDetectionProvider { listOf(object : LanguageDetectionProvider {
override val providerName = "papago" override val providerId = LanguageProviderId.PAPAGO
override fun detect(query: String): String? = null override fun detect(query: String): String? = null
}), }),
LanguageProviderProperties() LanguageProviderProperties()
@@ -759,15 +760,15 @@ class ControlledLanguageDetectionCacheService(
queries.add(query) queries.add(query)
val concurrentDetection = concurrentDetections[query] val concurrentDetection = concurrentDetections[query]
if (concurrentDetection != null) { if (concurrentDetection != null) {
return DetectedLanguage(concurrentDetection.detect(), "papago") return DetectedLanguage(concurrentDetection.detect(), LanguageProviderId.PAPAGO)
} }
val blockingDetection = blockingDetections[query] val blockingDetection = blockingDetections[query]
if (blockingDetection != null) { if (blockingDetection != null) {
blockingDetection.started.countDown() blockingDetection.started.countDown()
check(blockingDetection.release.await(5, TimeUnit.SECONDS)) 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.DetectedLanguage
import kr.co.vividnext.sodalive.i18n.translation.LanguageDetectionProvider 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.LanguageProviderProperties
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
@@ -36,7 +37,7 @@ class LanguageDetectionCacheServiceTest {
val detected = service.detectWithCache("Hello world") val detected = service.detectWithCache("Hello world")
assertEquals(DetectedLanguage("en", "papago"), detected) assertEquals(DetectedLanguage("en", LanguageProviderId.PAPAGO), detected)
assertEquals(0, providerCalls) assertEquals(0, providerCalls)
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion( Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
sourceHash = sourceHash, sourceHash = sourceHash,
@@ -80,7 +81,7 @@ class LanguageDetectionCacheServiceTest {
val detected = service.detectWithCache("Hello world") val detected = service.detectWithCache("Hello world")
assertEquals(DetectedLanguage("ko", "papago"), detected) assertEquals(DetectedLanguage("ko", LanguageProviderId.PAPAGO), detected)
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion( Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
sourceHash = sourceHash, sourceHash = sourceHash,
provider = "papago", provider = "papago",
@@ -104,7 +105,7 @@ class LanguageDetectionCacheServiceTest {
private fun service(repository: LanguageDetectionResultRepository, detect: () -> String?): LanguageDetectionCacheService { private fun service(repository: LanguageDetectionResultRepository, detect: () -> String?): LanguageDetectionCacheService {
val provider = object : LanguageDetectionProvider { val provider = object : LanguageDetectionProvider {
override val providerName = "papago" override val providerId = LanguageProviderId.PAPAGO
override fun detect(query: String) = detect() override fun detect(query: String) = detect()
} }
return LanguageDetectionCacheService(repository, listOf(provider), LanguageProviderProperties()) 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.LanguageDetectionCacheService
import kr.co.vividnext.sodalive.content.LanguageDetectionResult import kr.co.vividnext.sodalive.content.LanguageDetectionResult
import kr.co.vividnext.sodalive.content.LanguageDetectionResultRepository 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.assertEquals
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertSame import org.junit.jupiter.api.Assertions.assertSame
@@ -21,39 +23,39 @@ class LanguageDetectionProviderTest {
fun shouldFallbackAndReturnCanonicalWinnerWithActualProvider() { fun shouldFallbackAndReturnCanonicalWinnerWithActualProvider() {
val failures: List<() -> String?> = listOf({ throw IllegalStateException("sensitive body") }, { null }, { " " }) val failures: List<() -> String?> = listOf({ throw IllegalStateException("sensitive body") }, { null }, { " " })
failures.forEach { failure -> failures.forEach { failure ->
val first = RecordingDetectionProvider("first", failure) val first = RecordingDetectionProvider(PAPAGO, failure)
val second = RecordingDetectionProvider("second") { "EN" } val second = RecordingDetectionProvider(AWS) { "EN" }
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "second", "v1")).thenReturn("ko") 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), first.queries)
assertEquals(listOf(query), second.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 @Test
@DisplayName("보조 공급자의 유효 캐시가 있으면 첫 공급자도 호출하지 않는다") @DisplayName("보조 공급자의 유효 캐시가 있으면 첫 공급자도 호출하지 않는다")
fun shouldCheckAllConfiguredCachesBeforeCallingAnyProvider() { fun shouldCheckAllConfiguredCachesBeforeCallingAnyProvider() {
val first = RecordingDetectionProvider("first") { "ko" } val first = RecordingDetectionProvider(PAPAGO) { "ko" }
val second = RecordingDetectionProvider("second") { "en" } val second = RecordingDetectionProvider(AWS) { "en" }
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "second", "v1")) Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "aws", "v1"))
.thenReturn(LanguageDetectionResult(hash, "sample", "ja", "second", null, "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) assertEquals(emptyList<String>(), first.queries + second.queries)
} }
@Test @Test
@DisplayName("정상 미지원 언어 감지는 성공이며 후속 공급자를 호출하지 않는다") @DisplayName("정상 미지원 언어 감지는 성공이며 후속 공급자를 호출하지 않는다")
fun shouldKeepUnsupportedDetectionAsSuccessAndHonorOrder() { fun shouldKeepUnsupportedDetectionAsSuccessAndHonorOrder() {
val first = RecordingDetectionProvider("first") { "ko" } val first = RecordingDetectionProvider(PAPAGO) { "ko" }
val second = RecordingDetectionProvider("second") { "FR" } val second = RecordingDetectionProvider(AWS) { "FR" }
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "second", "v1")).thenReturn("fr") Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "aws", "v1")).thenReturn("fr")
assertEquals( assertEquals(
DetectedLanguage("fr", "second"), DetectedLanguage("fr", AWS),
service(listOf(first, second), listOf("second", "first")).detectWithCache(query) service(listOf(first, second), listOf(AWS, PAPAGO)).detectWithCache(query)
) )
assertEquals(emptyList<String>(), first.queries) assertEquals(emptyList<String>(), first.queries)
assertEquals(listOf(query), second.queries) assertEquals(listOf(query), second.queries)
@@ -62,8 +64,8 @@ class LanguageDetectionProviderTest {
@Test @Test
@DisplayName("감지 전체 실패와 공백 입력은 null이고 공백은 저장소와 공급자를 호출하지 않는다") @DisplayName("감지 전체 실패와 공백 입력은 null이고 공백은 저장소와 공급자를 호출하지 않는다")
fun shouldReturnNullAfterAllFailuresAndSkipBlankInput() { fun shouldReturnNullAfterAllFailuresAndSkipBlankInput() {
val first = RecordingDetectionProvider("first") { null } val first = RecordingDetectionProvider(PAPAGO) { null }
val second = RecordingDetectionProvider("second") { null } val second = RecordingDetectionProvider(AWS) { null }
val service = service(listOf(first, second)) val service = service(listOf(first, second))
assertNull(service.detectWithCache(" \n")) assertNull(service.detectWithCache(" \n"))
Mockito.verifyNoInteractions(repository) Mockito.verifyNoInteractions(repository)
@@ -75,28 +77,30 @@ class LanguageDetectionProviderTest {
@Test @Test
@DisplayName("감지 저장소 조회와 저장 오류는 다음 공급자로 전환하지 않고 전파한다") @DisplayName("감지 저장소 조회와 저장 오류는 다음 공급자로 전환하지 않고 전파한다")
fun shouldPropagateRepositoryFailuresWithoutFallback() { fun shouldPropagateRepositoryFailuresWithoutFallback() {
val first = RecordingDetectionProvider("first") { "ko" } val first = RecordingDetectionProvider(PAPAGO) { "ko" }
val second = RecordingDetectionProvider("second") { "en" } val second = RecordingDetectionProvider(AWS) { "en" }
val service = service(listOf(first, second)) val service = service(listOf(first, second))
val failure = IllegalStateException("database unavailable") val failure = IllegalStateException("database unavailable")
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "first", "v1")) Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "papago", "v1"))
.thenThrow(failure) .thenThrow(failure)
assertSame(failure, assertThrows(IllegalStateException::class.java) { service.detectWithCache(query) }) assertSame(failure, assertThrows(IllegalStateException::class.java) { service.detectWithCache(query) })
assertEquals(emptyList<String>(), first.queries + second.queries) assertEquals(emptyList<String>(), first.queries + second.queries)
Mockito.reset(repository) 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) }) assertSame(failure, assertThrows(IllegalStateException::class.java) { service.detectWithCache(query) })
assertEquals(listOf(query), first.queries) assertEquals(listOf(query), first.queries)
assertEquals(emptyList<String>(), second.queries) assertEquals(emptyList<String>(), second.queries)
} }
private fun service(providers: List<LanguageDetectionProvider>, order: List<String> = providers.map { it.providerName }) = private fun service(
LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(order, listOf("papago"))) providers: List<LanguageDetectionProvider>,
order: List<LanguageProviderId> = providers.map { it.providerId }
) = LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(order, listOf(PAPAGO)))
} }
class RecordingDetectionProvider( class RecordingDetectionProvider(
override val providerName: String, override val providerId: LanguageProviderId,
private val response: () -> String? private val response: () -> String?
) : LanguageDetectionProvider { ) : LanguageDetectionProvider {
val queries = mutableListOf<String>() 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.content.theme.AudioContentThemeQueryRepository
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityRepository 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.Member
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
@@ -86,16 +88,16 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
@DisplayName("보조 공급자의 감지를 H2에 귀속 저장하고 외부 호출 없이 캐시를 재사용한다") @DisplayName("보조 공급자의 감지를 H2에 귀속 저장하고 외부 호출 없이 캐시를 재사용한다")
fun shouldPersistSecondaryDetectionAndReuseCache() { fun shouldPersistSecondaryDetectionAndReuseCache() {
val source = "provider source ${UUID.randomUUID()}" val source = "provider source ${UUID.randomUUID()}"
assertEquals(DetectedLanguage("ko", "secondary"), detector.detectWithCache(source)) assertEquals(DetectedLanguage("ko", AWS), detector.detectWithCache(source))
assertEquals(DetectedLanguage("ko", "secondary"), detector.detectWithCache(source)) assertEquals(DetectedLanguage("ko", AWS), detector.detectWithCache(source))
val detection = transaction.execute { val detection = transaction.execute {
detectionRepository.findBySourceHashAndProviderAndNormalizationVersion( detectionRepository.findBySourceHashAndProviderAndNormalizationVersion(
SourceTextNormalizer.hash(source), SourceTextNormalizer.hash(source),
"secondary", "aws",
"v1" "v1"
) )
}!! }!!
assertEquals("secondary", detection.provider) assertEquals("aws", detection.provider)
assertEquals("ko", detection.detectedLanguage) assertEquals("ko", detection.detectedLanguage)
assertEquals(1, papagoDetection.queries.size) assertEquals(1, papagoDetection.queries.size)
assertEquals(1, secondaryDetection.queries.size) assertEquals(1, secondaryDetection.queries.size)
@@ -113,19 +115,19 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
SourceTextNormalizer.hash(source), SourceTextNormalizer.hash(source),
"ko", "ko",
"en", "en",
"secondary", "aws",
"v1" "v1"
) )
} }
assertNotNull(memory) assertNotNull(memory)
assertEquals("secondary", memory!!.provider) assertEquals("aws", memory!!.provider)
assertEquals("secondary-v1", memory.providerVersion) assertEquals("AWS-v1", memory.providerVersion)
assertEquals("secondary:0", memory.translatedText) assertEquals("AWS:0", memory.translatedText)
assertEquals( assertEquals(
TranslationJobStatus.COMPLETED, TranslationJobStatus.COMPLETED,
transaction.execute { jobRepository.findAll().single { it.resourceId == postId }.status } 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 { transaction.executeWithoutResult {
translations.deleteAll() translations.deleteAll()
@@ -137,7 +139,7 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
assertEquals(1, papagoTranslation.requests.size) assertEquals(1, papagoTranslation.requests.size)
assertEquals(1, secondaryTranslation.requests.size) assertEquals(1, secondaryTranslation.requests.size)
assertEquals(listOf(source), secondaryTranslation.requests.single().texts) 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) { private fun schedule(postId: Long, source: String) {
@@ -162,20 +164,20 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
class Providers { class Providers {
@Bean @Bean
fun languageProviderProperties() = LanguageProviderProperties( fun languageProviderProperties() = LanguageProviderProperties(
listOf("papago", "secondary"), listOf(PAPAGO, AWS),
listOf("papago", "secondary") listOf(PAPAGO, AWS)
) )
@Bean @Bean
fun papagoDetection() = RecordingDetectionProvider("papago") { throw IllegalStateException("sensitive provider body") } fun papagoDetection() = RecordingDetectionProvider(PAPAGO) { throw IllegalStateException("sensitive provider body") }
@Bean @Bean
fun secondaryDetection() = RecordingDetectionProvider("secondary") { "ko" } fun secondaryDetection() = RecordingDetectionProvider(AWS) { "ko" }
@Bean @Bean
fun papagoTranslation() = RecordingTranslationProvider("papago") { TranslateResult(emptyList()) } fun papagoTranslation() = RecordingTranslationProvider(PAPAGO) { TranslateResult(emptyList()) }
@Bean @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 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.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
@@ -9,33 +11,33 @@ class LanguageProviderPropertiesTest {
@Test @Test
@DisplayName("기능별 목록을 서로 독립된 설정 순서로 정렬한다") @DisplayName("기능별 목록을 서로 독립된 설정 순서로 정렬한다")
fun shouldOrderCapabilitiesIndependently() { fun shouldOrderCapabilitiesIndependently() {
val properties = LanguageProviderProperties(listOf("second", "first"), listOf("first", "second")) val properties = LanguageProviderProperties(listOf(AWS, PAPAGO), listOf(PAPAGO, AWS))
assertEquals( assertEquals(
listOf("second", "first"), listOf(AWS, PAPAGO),
properties.orderedProviders(properties.detectionOrder, listOf("first", "second")) { it } properties.orderedProviders(properties.detectionOrder, listOf(PAPAGO, AWS)) { it }
) )
assertEquals( assertEquals(
listOf("first", "second"), listOf(PAPAGO, AWS),
properties.orderedProviders(properties.translationOrder, listOf("second", "first")) { it } properties.orderedProviders(properties.translationOrder, listOf(AWS, PAPAGO)) { it }
) )
} }
@Test @Test
@DisplayName("빈 순서와 빈 이름과 중복 순서와 미등록 공급자는 초기화에서 거부한다") @DisplayName("빈 순서와 중복 순서와 미등록 AWS 공급자는 초기화에서 거부한다")
fun shouldRejectInvalidOrders() { fun shouldRejectInvalidOrders() {
val properties = LanguageProviderProperties() 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) { assertThrows(IllegalArgumentException::class.java) {
properties.orderedProviders(order, listOf("first", "second")) { it } properties.orderedProviders(order, listOf(PAPAGO)) { it }
} }
} }
} }
@Test @Test
@DisplayName("같은 기능 안에서 중복 등록된 공급자 이름은 초기화에서 거부한다") @DisplayName("같은 기능 안에서 선택되지 않은 공급자도 중복 등록을 거부한다")
fun shouldRejectDuplicateRegisteredNamesEvenWhenNotSelected() { fun shouldRejectDuplicateRegisteredIdsEvenWhenNotSelected() {
assertThrows(IllegalArgumentException::class.java) { 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.LanguageDetectionCacheService
import kr.co.vividnext.sodalive.content.LanguageDetectionResultRepository 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.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
@@ -16,7 +18,8 @@ import org.springframework.core.env.SystemEnvironmentPropertySource
import java.util.function.Supplier import java.util.function.Supplier
class LanguageProviderSpringConfigurationTest { 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) private val defaultRunner = runner.withUserConfiguration(LanguageProviderProperties::class.java)
@Test @Test
@@ -28,8 +31,8 @@ class LanguageProviderSpringConfigurationTest {
).run { context -> ).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)
} }
} }
@@ -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.detection-order"))
assertEquals("secondary,papago", context.environment.getProperty("sodalive.language-providers.translation-order")) assertEquals("secondary,papago", context.environment.getProperty("sodalive.language-providers.translation-order"))
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)
} }
} }
@@ -63,39 +66,39 @@ class LanguageProviderSpringConfigurationTest {
@DisplayName("Spring 테스트 bean의 감지 목록만 바꾸어도 번역 순서는 유지한다") @DisplayName("Spring 테스트 bean의 감지 목록만 바꾸어도 번역 순서는 유지한다")
fun shouldInjectIndependentCapabilityOrders() { fun shouldInjectIndependentCapabilityOrders() {
listOf( listOf(
listOf("papago", "secondary") to "papago", listOf(PAPAGO, AWS) to PAPAGO,
listOf("secondary", "papago") to "secondary" listOf(AWS, PAPAGO) to AWS
).forEach { (order, expected) -> ).forEach { (order, expected) ->
runner.withBean( runner.withBean(
LanguageProviderProperties::class.java, LanguageProviderProperties::class.java,
Supplier { LanguageProviderProperties(order, listOf("papago", "secondary")) } Supplier { LanguageProviderProperties(order, listOf(PAPAGO, AWS)) }
).run { context -> ).run { context ->
assertNull(context.startupFailure) assertNull(context.startupFailure)
val detector = context.getBean(LanguageDetectionCacheService::class.java) val detector = context.getBean(LanguageDetectionCacheService::class.java)
assertEquals(expected, detector.detectWithCache("source")?.providerName) assertEquals(expected, detector.detectWithCache("source")?.providerId)
assertEquals( assertEquals(
"papago", PAPAGO,
context.getBean(TranslationCoordinator::class.java) 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) 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) assertEquals(0, context.getBean("secondaryTranslation", RecordingTranslationProvider::class.java).requests.size)
} }
} }
runner.withBean( runner.withBean(
LanguageProviderProperties::class.java, LanguageProviderProperties::class.java,
Supplier { LanguageProviderProperties(listOf("papago", "secondary"), listOf("secondary", "papago")) } Supplier { LanguageProviderProperties(listOf(PAPAGO, AWS), listOf(AWS, PAPAGO)) }
).run { context -> ).run { context ->
assertNull(context.startupFailure) assertNull(context.startupFailure)
assertEquals( assertEquals(
"papago", PAPAGO,
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerName context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerId
) )
assertEquals( assertEquals(
"secondary", AWS,
context.getBean(TranslationCoordinator::class.java) 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 -> 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( assertEquals(
"papago", PAPAGO,
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerName context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerId
) )
assertEquals( assertEquals(
"papago", PAPAGO,
context.getBean(TranslationCoordinator::class.java) context.getBean(TranslationCoordinator::class.java)
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerName .translate(TranslateRequest(listOf("source"), "ko", "en")).providerId
) )
} }
} }
@Test @Test
@DisplayName("Spring 테스트 bean의 빈 목록과 빈 이름과 중복 순서와 미등록 이름은 시작 시 실패한다") @DisplayName("Spring 테스트 bean의 빈 목록과 중복 순서와 해당 기능의 AWS 구현체 부재는 시작 시 실패한다")
fun shouldRejectInvalidOrdersAtStartup() { fun shouldRejectInvalidOrdersAtStartup() {
listOf(emptyList(), listOf(""), listOf("papago", "papago"), listOf("unknown")).forEach { order -> listOf(emptyList(), listOf(PAPAGO, PAPAGO), listOf(AWS)).forEach { order ->
listOf( listOf(
LanguageProviderProperties(order, listOf("papago")), LanguageProviderProperties(order, listOf(PAPAGO)),
LanguageProviderProperties(listOf("papago"), order) LanguageProviderProperties(listOf(PAPAGO), order)
).forEach { properties -> ).forEach { properties ->
runner.withBean(LanguageProviderProperties::class.java, Supplier { properties }).run { context -> papagoRunner.withBean(LanguageProviderProperties::class.java, Supplier { properties }).run { context ->
assertNotNull(context.startupFailure) assertNotNull(context.startupFailure)
} }
} }
@@ -144,7 +147,7 @@ class LanguageProviderSpringConfigurationTest {
@Bean @Bean
fun detectionRepository(): LanguageDetectionResultRepository { fun detectionRepository(): LanguageDetectionResultRepository {
val repository = Mockito.mock(LanguageDetectionResultRepository::class.java) 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")) Mockito.`when`(repository.findDetectedLanguageForUpdate(SourceTextNormalizer.hash("source"), provider, "v1"))
.thenReturn("ko") .thenReturn("ko")
} }
@@ -152,15 +155,18 @@ class LanguageProviderSpringConfigurationTest {
} }
@Bean @Bean
fun papagoDetection() = RecordingDetectionProvider("papago") { "ko" } fun papagoDetection() = RecordingDetectionProvider(PAPAGO) { "ko" }
@Bean @Bean
fun secondaryDetection() = RecordingDetectionProvider("secondary") { "ko" } fun papagoTranslation() = RecordingTranslationProvider(PAPAGO)
}
@TestConfiguration
class AwsProviders {
@Bean
fun secondaryDetection() = RecordingDetectionProvider(AWS) { "ko" }
@Bean @Bean
fun papagoTranslation() = RecordingTranslationProvider("papago") fun secondaryTranslation() = RecordingTranslationProvider(AWS)
@Bean
fun secondaryTranslation() = RecordingTranslationProvider("secondary")
} }
} }
@@ -1,5 +1,7 @@
package kr.co.vividnext.sodalive.i18n.translation 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.assertEquals
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertSame import org.junit.jupiter.api.Assertions.assertSame
@@ -15,15 +17,15 @@ class TranslationCoordinatorTest {
@Test @Test
@DisplayName("설정 순서의 첫 성공 공급자와 버전을 반환하고 후속 호출을 멈춘다") @DisplayName("설정 순서의 첫 성공 공급자와 버전을 반환하고 후속 호출을 멈춘다")
fun shouldUseConfiguredOrderAndStopAfterSuccess() { fun shouldUseConfiguredOrderAndStopAfterSuccess() {
val first = RecordingTranslationProvider("first") val first = RecordingTranslationProvider(PAPAGO)
val second = RecordingTranslationProvider("second") val second = RecordingTranslationProvider(AWS)
val coordinator = coordinator(listOf(first, second), listOf("second", "first")) val coordinator = coordinator(listOf(first, second), listOf(AWS, PAPAGO))
val result = coordinator.translate(request) val result = coordinator.translate(request)
assertEquals("second", result.providerName) assertEquals(AWS, result.providerId)
assertEquals("second-v1", result.providerVersion) assertEquals("AWS-v1", result.providerVersion)
assertEquals(listOf("second:0", "second:1"), result.result.translatedText) assertEquals(listOf("AWS:0", "AWS:1"), result.result.translatedText)
assertEquals(listOf(request), second.requests) assertEquals(listOf(request), second.requests)
assertEquals(emptyList<TranslateRequest>(), first.requests) assertEquals(emptyList<TranslateRequest>(), first.requests)
} }
@@ -37,11 +39,11 @@ class TranslationCoordinatorTest {
{ TranslateResult(listOf("translated", " ")) } { TranslateResult(listOf("translated", " ")) }
) )
failures.forEach { failure -> failures.forEach { failure ->
val first = RecordingTranslationProvider("first", failure) val first = RecordingTranslationProvider(PAPAGO, failure)
val second = RecordingTranslationProvider("second") val second = RecordingTranslationProvider(AWS)
val coordinator = coordinator(listOf(first, second)) 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), first.requests)
assertEquals(listOf(request), second.requests) assertEquals(listOf(request), second.requests)
} }
@@ -50,8 +52,8 @@ class TranslationCoordinatorTest {
@Test @Test
@DisplayName("전체 공급자 실패는 원문이나 원시 예외 원인 없이 고정 메시지를 반환한다") @DisplayName("전체 공급자 실패는 원문이나 원시 예외 원인 없이 고정 메시지를 반환한다")
fun shouldSanitizeAllProviderFailures() { fun shouldSanitizeAllProviderFailures() {
val first = RecordingTranslationProvider("first") { throw IllegalStateException("sensitive response body") } val first = RecordingTranslationProvider(PAPAGO) { throw IllegalStateException("sensitive response body") }
val second = RecordingTranslationProvider("second") { TranslateResult(emptyList()) } val second = RecordingTranslationProvider(AWS) { TranslateResult(emptyList()) }
val error = assertThrows(IllegalStateException::class.java) { val error = assertThrows(IllegalStateException::class.java) {
coordinator(listOf(first, second)).translate(request) coordinator(listOf(first, second)).translate(request)
@@ -66,7 +68,7 @@ class TranslationCoordinatorTest {
@Test @Test
@DisplayName("빈 목록과 공백과 동일 언어와 미지원 언어는 공급자 호출 전에 거부한다") @DisplayName("빈 목록과 공백과 동일 언어와 미지원 언어는 공급자 호출 전에 거부한다")
fun shouldRejectInvalidRequestsBeforeCallingProviders() { fun shouldRejectInvalidRequestsBeforeCallingProviders() {
val provider = RecordingTranslationProvider("first") val provider = RecordingTranslationProvider(PAPAGO)
val coordinator = coordinator(listOf(provider)) val coordinator = coordinator(listOf(provider))
listOf( listOf(
request.copy(texts = emptyList()), request.copy(texts = emptyList()),
@@ -85,17 +87,17 @@ class TranslationCoordinatorTest {
@Test @Test
@DisplayName("모든 설정 공급자 캐시를 호출보다 먼저 조회하고 순서상 첫 유효 캐시를 선택한다") @DisplayName("모든 설정 공급자 캐시를 호출보다 먼저 조회하고 순서상 첫 유효 캐시를 선택한다")
fun shouldFindSecondaryCacheAndPreferConfiguredFirstValidCache() { fun shouldFindSecondaryCacheAndPreferConfiguredFirstValidCache() {
val first = RecordingTranslationProvider("first") val first = RecordingTranslationProvider(PAPAGO)
val second = RecordingTranslationProvider("second") val second = RecordingTranslationProvider(AWS)
val coordinator = coordinator(listOf(first, second)) val coordinator = coordinator(listOf(first, second))
val secondary = memory("second", "cached secondary") val secondary = memory("aws", "cached secondary")
givenMemory("second", secondary) givenMemory("aws", secondary)
assertSame(secondary, coordinator.findCached("hash", "ko", "en")) assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
givenMemory("first", memory("first", " ")) givenMemory("papago", memory("papago", " "))
assertSame(secondary, coordinator.findCached("hash", "ko", "en")) assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
val primary = memory("first", "cached primary") val primary = memory("papago", "cached primary")
givenMemory("first", primary) givenMemory("papago", primary)
assertSame(primary, coordinator.findCached("hash", "ko", "en")) assertSame(primary, coordinator.findCached("hash", "ko", "en"))
assertEquals(emptyList<TranslateRequest>(), first.requests + second.requests) assertEquals(emptyList<TranslateRequest>(), first.requests + second.requests)
} }
@@ -103,14 +105,14 @@ class TranslationCoordinatorTest {
@Test @Test
@DisplayName("캐시 저장소 오류는 공급자 실패로 숨기지 않는다") @DisplayName("캐시 저장소 오류는 공급자 실패로 숨기지 않는다")
fun shouldPropagateCacheRepositoryFailure() { fun shouldPropagateCacheRepositoryFailure() {
val provider = RecordingTranslationProvider("first") val provider = RecordingTranslationProvider(PAPAGO)
val failure = IllegalStateException("database unavailable") val failure = IllegalStateException("database unavailable")
Mockito.`when`( Mockito.`when`(
repository.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion( repository.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion(
"hash", "hash",
"ko", "ko",
"en", "en",
"first", "papago",
"v1" "v1"
) )
).thenThrow(failure) ).thenThrow(failure)
@@ -124,8 +126,8 @@ class TranslationCoordinatorTest {
private fun coordinator( private fun coordinator(
providers: List<TranslationProvider>, providers: List<TranslationProvider>,
order: List<String> = providers.map { it.providerName } order: List<LanguageProviderId> = providers.map { it.providerId }
) = TranslationCoordinator(providers, LanguageProviderProperties(listOf("papago"), order), 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",
@@ -151,12 +153,12 @@ class TranslationCoordinatorTest {
} }
class RecordingTranslationProvider( class RecordingTranslationProvider(
override val providerName: String, override val providerId: LanguageProviderId,
private val response: (TranslateRequest) -> TranslateResult = { request -> private val response: (TranslateRequest) -> TranslateResult = { request ->
TranslateResult(request.texts.indices.map { "$providerName:$it" }) TranslateResult(request.texts.indices.map { "$providerId:$it" })
} }
) : TranslationProvider { ) : TranslationProvider {
override val providerVersion = "$providerName-v1" override val providerVersion = "$providerId-v1"
val requests = mutableListOf<TranslateRequest>() val requests = mutableListOf<TranslateRequest>()
override fun translate(request: TranslateRequest): TranslateResult { override fun translate(request: TranslateRequest): TranslateResult {
@@ -236,7 +236,7 @@ class TranslationJobWorkerTest {
private fun successfulProvider(): TranslationProvider { private fun successfulProvider(): TranslationProvider {
return object : TranslationProvider { return object : TranslationProvider {
override val providerName: String = "papago" override val providerId: LanguageProviderId = LanguageProviderId.PAPAGO
override val providerVersion: String = "nmt-v1" override val providerVersion: String = "nmt-v1"
override fun translate(request: TranslateRequest): TranslateResult { override fun translate(request: TranslateRequest): TranslateResult {
@@ -247,7 +247,7 @@ class TranslationJobWorkerTest {
private fun failingProvider(): TranslationProvider { private fun failingProvider(): TranslationProvider {
return object : TranslationProvider { return object : TranslationProvider {
override val providerName: String = "papago" override val providerId: LanguageProviderId = LanguageProviderId.PAPAGO
override val providerVersion: String = "nmt-v1" override val providerVersion: String = "nmt-v1"
override fun translate(request: TranslateRequest): TranslateResult { override fun translate(request: TranslateRequest): TranslateResult {