refactor(i18n): 공급자 전환과 캐시 선택을 공통 처리로 연결한다

This commit is contained in:
2026-11-09 15:41:13 +09:00
parent 320adb037c
commit f7fadf7ac7
16 changed files with 545 additions and 221 deletions
@@ -15,9 +15,14 @@ import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.Creato
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLikeRepository
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.LanguageProviderProperties
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
import kr.co.vividnext.sodalive.i18n.translation.TranslationCoordinator
import kr.co.vividnext.sodalive.i18n.translation.TranslationJobRepository
import kr.co.vividnext.sodalive.i18n.translation.TranslationJobScheduler
import kr.co.vividnext.sodalive.i18n.translation.TranslationMemory
@@ -83,6 +88,9 @@ import javax.persistence.EntityManager
CreatorCommunityTranslationService::class,
TranslationSourceExtractor::class,
TranslationReadModelMaterializer::class,
TranslationCoordinator::class,
LanguageProviderProperties::class,
PapagoTranslationService::class,
ResourceTranslationJobScheduler::class,
TranslationJobScheduler::class,
CreatorCommunityLanguageDetectTest.AsyncConfiguration::class
@@ -255,7 +263,7 @@ class CreatorCommunityLanguageDetectTest @Autowired constructor(
sourceLanguage = "en",
targetLanguage = "ko",
translatedText = "translated detail content",
provider = TranslationReadModelMaterializer.DEFAULT_PROVIDER,
provider = "papago",
providerVersion = "nmt-v1"
)
)
@@ -719,7 +727,14 @@ class CreatorCommunityLanguageDetectTest @Autowired constructor(
class ControlledLanguageDetectionCacheService(
languageDetectionResultRepository: LanguageDetectionResultRepository
) : LanguageDetectionCacheService(languageDetectionResultRepository) {
) : LanguageDetectionCacheService(
languageDetectionResultRepository,
listOf(object : LanguageDetectionProvider {
override val providerName = "papago"
override fun detect(query: String): String? = null
}),
LanguageProviderProperties()
) {
val detections = ConcurrentHashMap<String, String>()
val queries = ConcurrentLinkedQueue<String>()
private val blockingDetections = ConcurrentHashMap<String, BlockingDetection>()
@@ -740,19 +755,19 @@ class ControlledLanguageDetectionCacheService(
return ConcurrentDetection(detectedLanguage).also { concurrentDetections[query] = it }
}
override fun detectWithCache(query: String, provider: String, detector: () -> String?): String? {
override fun detectWithCache(query: String): DetectedLanguage? {
queries.add(query)
val concurrentDetection = concurrentDetections[query]
if (concurrentDetection != null) {
return concurrentDetection.detect()
return DetectedLanguage(concurrentDetection.detect(), "papago")
}
val blockingDetection = blockingDetections[query]
if (blockingDetection != null) {
blockingDetection.started.countDown()
check(blockingDetection.release.await(5, TimeUnit.SECONDS))
return blockingDetection.detectedLanguage
return DetectedLanguage(blockingDetection.detectedLanguage, "papago")
}
return detections[query]
return detections[query]?.let { DetectedLanguage(it, "papago") }
}
}
@@ -4,9 +4,13 @@ import kr.co.vividnext.sodalive.configs.QueryDslConfig
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.LanguageProviderProperties
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
import kr.co.vividnext.sodalive.i18n.translation.PapagoLanguageDetectionProvider
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
import kr.co.vividnext.sodalive.i18n.translation.TranslationCoordinator
import kr.co.vividnext.sodalive.i18n.translation.TranslationJobRepository
import kr.co.vividnext.sodalive.i18n.translation.TranslationJobScheduler
import kr.co.vividnext.sodalive.i18n.translation.TranslationJobStatus
@@ -67,6 +71,10 @@ import java.util.concurrent.atomic.AtomicInteger
QueryDslConfig::class,
AudioContentThemeQueryRepository::class,
LanguageDetectionCacheService::class,
LanguageProviderProperties::class,
PapagoLanguageDetectionProvider::class,
PapagoTranslationService::class,
TranslationCoordinator::class,
LanguageDetectListener::class,
CreatorCommunityTranslationService::class,
TranslationSourceExtractor::class,
@@ -82,6 +90,7 @@ class CreatorCommunityLanguageDetectionCacheConcurrencyTest @Autowired construct
private val translationJobRepository: TranslationJobRepository,
private val languageDetectionCacheService: LanguageDetectionCacheService,
private val languageDetectListener: LanguageDetectListener,
private val papagoLanguageDetectionProvider: PapagoLanguageDetectionProvider,
transactionManager: PlatformTransactionManager
) {
private val requiresNewTransactionTemplate = TransactionTemplate(transactionManager).apply {
@@ -92,8 +101,7 @@ class CreatorCommunityLanguageDetectionCacheConcurrencyTest @Autowired construct
@BeforeEach
fun setUp() {
papagoRequests = ControlledPapagoRequestFactory()
val listener: LanguageDetectListener = AopTestUtils.getTargetObject(languageDetectListener)
ReflectionTestUtils.setField(listener, "restTemplate", RestTemplate(papagoRequests))
ReflectionTestUtils.setField(papagoLanguageDetectionProvider, "restTemplate", RestTemplate(papagoRequests))
}
@Test
@@ -232,7 +240,7 @@ class CreatorCommunityLanguageDetectionCacheConcurrencyTest @Autowired construct
val rows = requiresNewTransactionTemplate.execute {
languageDetectionResultRepository.findAll().filter {
it.sourceHash == SourceTextNormalizer.hash(content) &&
it.provider == LanguageDetectionCacheService.DEFAULT_PROVIDER &&
it.provider == "papago" &&
it.normalizationVersion == SourceTextNormalizer.NORMALIZATION_VERSION
}
}.orEmpty()
@@ -1,5 +1,8 @@
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.LanguageProviderProperties
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
@@ -10,7 +13,8 @@ class LanguageDetectionCacheServiceTest {
@Test
fun shouldReuseCachedLanguageDetectionForSameNormalizedText() {
val repository = Mockito.mock(LanguageDetectionResultRepository::class.java)
val service = LanguageDetectionCacheService(repository)
var providerCalls = 0
val service = service(repository) { providerCalls++; "ko" }
val sourceHash = SourceTextNormalizer.hash("Hello world")
Mockito.`when`(
@@ -30,13 +34,9 @@ class LanguageDetectionCacheServiceTest {
)
)
var providerCalls = 0
val detected = service.detectWithCache("Hello world") {
providerCalls++
"ko"
}
val detected = service.detectWithCache("Hello world")
assertEquals("en", detected)
assertEquals(DetectedLanguage("en", "papago"), detected)
assertEquals(0, providerCalls)
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
sourceHash = sourceHash,
@@ -50,7 +50,7 @@ class LanguageDetectionCacheServiceTest {
@DisplayName("최초 캐시 저장 경합에서는 저장된 승자의 감지 언어를 반환한다")
fun shouldReturnStoredWinnerAfterInitialCacheInsert() {
val repository = Mockito.mock(LanguageDetectionResultRepository::class.java)
val service = LanguageDetectionCacheService(repository)
val service = service(repository) { "en" }
val sourceHash = SourceTextNormalizer.hash("Hello world")
Mockito.`when`(
@@ -78,9 +78,9 @@ class LanguageDetectionCacheServiceTest {
)
).thenReturn("ko")
val detected = service.detectWithCache("Hello world") { "en" }
val detected = service.detectWithCache("Hello world")
assertEquals("ko", detected)
assertEquals(DetectedLanguage("ko", "papago"), detected)
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
sourceHash = sourceHash,
provider = "papago",
@@ -101,4 +101,12 @@ class LanguageDetectionCacheServiceTest {
)
Mockito.verifyNoMoreInteractions(repository)
}
private fun service(repository: LanguageDetectionResultRepository, detect: () -> String?): LanguageDetectionCacheService {
val provider = object : LanguageDetectionProvider {
override val providerName = "papago"
override fun detect(query: String) = detect()
}
return LanguageDetectionCacheService(repository, listOf(provider), LanguageProviderProperties())
}
}