Files
sodalive-backend-spring-boot/docs/20261109_감지번역공급자공통화/provider-guide.md
T

12 KiB

언어 감지·번역 공급자 확장 가이드

현재 실행 구조

감지 구현체는 PapagoLanguageDetectionProvider와 AwsLanguageDetectionProvider, 번역 구현체는 PapagoTranslationService와 AwsTranslationService다. 두 기능의 production 기본 순서는 모두 PAPAGO → AWS다. AWS는 기존 자격 증명과 리전 설정을 재사용한다.

감지와 번역은 별도 capability다. 감지만 또는 번역만 제공하는 공급자도 등록할 수 있다. 두 capability에서 같은 providerId를 쓰는 것은 가능하지만, 한 capability 안의 ID 중복은 시작 시 거부한다.

경계 계약
LanguageDetectionProvider providerId: LanguageProviderId, detect(query: String): String?
LanguageDetectionCacheService detectWithCache(query: String): DetectedLanguage?
DetectedLanguage detectedLanguage: String, providerId: LanguageProviderId
TranslationProvider providerId: LanguageProviderId, providerVersion: String, translate(TranslateRequest): TranslateResult
TranslationCoordinator findCached(sourceHash, sourceLanguage, targetLanguage): TranslationMemory?, translate(TranslateRequest): ProviderTranslationResult
ProviderTranslationResult result: TranslateResult, providerId: LanguageProviderId, providerVersion: String

인터페이스와 DTO는 src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/에 있다. 감지 coordinator는 기존 content/LanguageDetectionCacheService.kt이며 별도 vendor가 아니다. TranslationCoordinator도 TranslationProvider를 구현하지 않는다.

새 공급자 추가 순서

  1. LanguageProviderId에 공급자 enum 항목과 재배포 후에도 안정적인 DB code를 추가한다. 현재 항목은 PAPAGO("papago"), AWS("aws")이며 두 공급자의 실제 adapter가 등록돼 있다.
  2. 필요한 capability 인터페이스를 구현하고 Spring @Component/@Service 또는 @Bean으로 등록한다. 예를 들어 Papago adapter의 식별자는 override val providerId = LanguageProviderId.PAPAGO다.
  3. 공급자의 HTTP/SDK 및 인증은 adapter 안에 둔다. 외부 응답 형식과 빈 결과를 검증하고, 원문·인증정보·응답 본문·예외 cause를 로그로 출력하지 않는다. 번역은 입력별 결과를 같은 순서로 반환한다.
  4. LanguageProviderOrder.kt에서 구현한 capability의 목록에만 enum을 넣고 빌드·배포한다. @Primary나 selector 분기는 필요 없다.
  5. 아래 테스트 경계로 순서·실패 전환·실제 provider 저장·무호출 캐시 재사용·materializer 표시를 검증한다.

콘텐츠·댓글·커뮤니티 호출자, worker, materializer, scheduler에 공급자 분기를 추가하지 않는다. 기존 두 coordinator는 주입된 구현체 목록과 설정만 사용한다. 공급자마다 다른 분할·요청 크기 제한은 adapter 책임이다. CodePointTextSplitter.kt의 internal helper가 code point 경계의 분할·prefix와 UTF-8 길이 계산을 공유한다. Papago는 기존 5,000 UTF-16 코드 단위(Character.charCount)로, AWS Translate는 10,000 UTF-8 바이트로 분할한다. 각 adapter는 조각을 순차 호출해 원문별로 구분자 없이 결합하며, 조각 하나라도 실패하면 전체 요청의 부분 결과를 버린다. AWS Comprehend는 앞 최대 100,000 UTF-8 바이트만 감지한다. prefix는 원문 전체 hash/cache key나 번역 원문을 바꾸지 않는다. 혼합 언어 입력은 prefix와 전체 본문의 감지 결과가 다를 수 있다. blank 입력은 호출하지 않으며 짧은 비어 있지 않은 입력은 전달한다. 응답은 첫 후보만 사용하고 첫 코드가 blank/누락이면 뒤 후보를 대신 선택하지 않는다. 별도 confidence threshold는 없다. ko/en/ja 서비스 정책은 TranslationLanguages에 있으며 공급자를 추가해도 자동 확장하지 않는다.

기능별 독립 코드 목록

production 순서의 유일한 편집 지점은 src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderOrder.kt다. 목록의 왼쪽부터 사용하며 기본값은 두 기능 모두 listOf(LanguageProviderId.PAPAGO, LanguageProviderId.AWS)다.

object LanguageProviderOrder {
    val detectionOrder = listOf(LanguageProviderId.PAPAGO, LanguageProviderId.AWS)
    val translationOrder = listOf(LanguageProviderId.PAPAGO, LanguageProviderId.AWS)
}

Kotlin 목록은 const val이 아닌 val이다. LanguageProviderProperties는 기존 주입 wrapper로 유지하며, Spring은 @Autowired 기본 생성자로 위 목록을 읽는다. 문자열 파싱은 하지 않는다. 테스트에서는 전역 목록을 변경하지 않고 LanguageProviderProperties(List<LanguageProviderId>, List<LanguageProviderId>)로 만든 테스트 bean을 주입한다.

다음은 감지만 AWS 우선으로 바꾸는 예시다. 현재 기본 순서를 뜻하지 않는다.

val detectionOrder = listOf(LanguageProviderId.AWS, LanguageProviderId.PAPAGO)
val translationOrder = listOf(LanguageProviderId.PAPAGO, LanguageProviderId.AWS)

감지만 순서를 바꾸어도 번역 순서는 바뀌지 않는다. 반대로 번역만 바꿀 수도 있다. 변경 후 빌드·배포해야 반영된다. 과거 우선순위 Spring property·환경변수·YAML 값이 남아 있어도 순서는 바뀌지 않는다. 공급자 인증정보는 기존 설정 경계로 주입하며 이 코드 목록에 넣지 않는다. 빈 목록·중복 ID·해당 capability의 구현체 부재·capability 안의 중복 구현체 ID는 초기화에서 거부한다. 빈 문자열 ID는 enum 타입으로 표현할 수 없다. 공급자가 번역에만 등록됐다면 그 ID를 감지 순서에 넣을 수 없다. 실행 중 설정 변경은 지원하지 않는다. 테스트의 Papago-only inventory는 두 순서를 명시적으로 [PAPAGO]로 제한한다.

AWS SDK와 설정

Comprehend와 Translate만 AWS SDK for Java v2 BOM 2.57.11을 사용하며 기존 SDK v1 의존성은 유지한다. 동기 HTTP runtime은 SDK의 전이 의존성이다. Spring Boot 2.7의 하향 버전 관리를 피하도록 httpclient5.version=5.6.4, httpcore5.version=5.4.3을 SDK POM 기준으로 고정한다.

AwsLanguageProviderConfiguration은 기존 cloud.aws.credentials.access-key, cloud.aws.credentials.secret-key를 StaticCredentialsProvider에 연결하고 cloud.aws.region.static을 Region.of에 전달한다(현재 설정은 ap-northeast-2). 기본 credentials chain이나 AWS 전용 새 인증 설정을 추가하지 않는다. 두 동기 client는 Spring Bean이며 종료 시 close한다. AWS 번역은 source/target을 명시하며 providerVersion은 안정 값 translate-2017-07-01이다. Papago의 providerVersion은 기존 nmt-v1이다. 두 값은 SDK 라이브러리 버전과 별개다.

실 AWS 호출·IAM·품질·과금은 자동 테스트에서 확인하지 않는다. 필요한 IAM 작업은 comprehend:DetectDominantLanguage, translate:TranslateText이며 이 가이드는 권한 변경을 승인하지 않는다.

DB code 호환성

내부 순서·구현체 매칭·성공 결과는 LanguageProviderId를 사용한다. entity/repository의 provider는 String 그대로다. coordinator/cache/worker의 DB 조회·저장 접점에서만 providerId.code로 변환하므로 기존 "papago" 캐시를 재사용한다. 실제 AWS 성공 결과의 저장값은 "aws"다. enum .name/.ordinal, @Enumerated/@Convert, 역변환 helper는 사용하지 않는다. 새 enum 항목을 추가할 때도 기존 code와 DB 스키마·캐시 키는 변경하지 않는다. providerVersion은 별도의 String 기록 필드다.

캐시와 오류 계약

감지는 정규화/hash/normalization version을 기존과 같이 사용하고 adapter에는 원래 query를 전달한다. 설정된 모든 공급자의 캐시를 순서대로 조회한 뒤, 유효 캐시가 하나도 없을 때만 외부 호출을 시작한다. 저장은 실제 성공 공급자로 insertIfAbsent하고 이어지는 locking read의 승자를 반환한다. ko/en/ja 밖의 유효 감지 결과도 성공이다. 커뮤니티의 기존 지원 언어 gate는 별도로 유지된다.

번역 worker와 materializer는 동일한 TranslationCoordinator.findCached를 사용한다. 앞선 공급자의 캐시가 없어도 뒤 공급자의 유효 캐시가 있으면 외부 호출을 하지 않는다. 여러 공급자 캐시가 있으면 설정 순서상 첫 유효 결과를 선택한다. job을 특정 vendor에 영구 고정하지 않는다. cache key와 DDL은 바꾸지 않았으며 providerVersion은 기록 필드이지 cache key가 아니다. 외부 번역은 worker의 저장 트랜잭션 밖에서 실행하고, 실제 성공 공급자로 저장 직전 재조회한다. materializer는 외부 번역을 호출하지 않는다.

adapter 예외·null 감지·빈 감지·번역 결과 개수 불일치·blank 번역 결과는 다음 공급자로 전환한다. 번역은 부분 결과를 섞지 않고 원래 요청 전체를 다음 공급자에게 한 번 넘긴다. 첫 성공 이후에는 중단한다. 저장소 조회·저장 오류는 adapter 실패로 catch하지 않는다. 감지가 모두 실패하면 null, 번역이 모두 실패하면 cause 없는 IllegalStateException("all translation providers failed")를 반환하며 기존 job 재시도 경로를 따른다. 빈 목록·blank 텍스트·동일 언어(대소문자 무시)·미지원 번역 언어는 외부 호출 전에 IllegalArgumentException("invalid translation request")로 거부한다. raw Papago의 기존 가드는 유지한다.

검증 경계

테스트 검증 내용
LanguageProviderSpringConfigurationTest production 기본 생성자와 PAPAGO 우선 성공, 과거 property/env 무시, 독립 순서와 잘못된 목록의 시작 실패; fake AWS context와 실제 adapter + SDK mock context 분리
LanguageProviderIdTest 런타임 enum getter 계약, 안정적인 papago/aws code, production 기본값과 실제 adapter ID
LanguageProviderPropertiesTest 순서 검증 및 선택되지 않은 bean을 포함한 capability 내 중복 ID 거부
LanguageDetectionProviderTest / TranslationCoordinatorTest 전체 요청 폴백, 첫 성공 중단, cache-first, 안전한 전체 실패, DB 오류 전파
PapagoLanguageDetectionProviderTest / PapagoTranslationServiceTest 제어된 HTTP form/header/response와 빈 raw 입력·공백을 포함한 19개 번역 회귀
CodePointTextSplitterTest UTF-16/UTF-8 한도, 한글·보조 평면 문자·공백 보존, prefix와 전체 원문의 분리
AwsLanguageDetectionProviderTest / AwsTranslationServiceTest 실제 adapter + SDK mock의 요청·응답, prefix/분할·결합, 첫 후보, 중간 실패 폐기와 호출 중단, 민감 오류 비노출
AwsLanguageProviderConfigurationTest 합성 credentials/region으로 실제 client 생성과 종료, 네트워크 무호출
LanguageProviderCacheEndToEndTest 실제 AWS adapters + SDK mock + H2의 5개 시나리오: 동일 prefix·서로 다른 전체 hash, 첫 blank 후보 미저장, aws/version/결합 결과 저장과 무호출 재사용·표시, 부분 실패 미저장과 안전한 worker 오류, 테스트 전용 AWS → PAPAGO의 전체 요청 fallback

H2 버전은 MySQL FOR UPDATE SKIP LOCKED를 지원하지 않으므로 새 H2 테스트는 claim 조회 한 곳만 test spy로 대체한다. 나머지 repository 저장·별도 트랜잭션 재조회·materialization은 실제 H2다. 이 테스트가 운영 MySQL claim 동시성이나 SDK serialization/signing·실 AWS 권한/품질을 검증한다는 뜻은 아니다. SDK mocks는 테스트마다 reset하고 Papago 기록 목록과 cache/job/read model을 비워 context 재사용을 격리한다. 기존 감지 cache 경합·revision 테스트는 별도로 유지한다.

./gradlew --no-daemon test --tests 'kr.co.vividnext.sodalive.i18n.translation.LanguageProviderCacheEndToEndTest'
./gradlew --no-daemon test --tests 'kr.co.vividnext.sodalive.i18n.translation.*' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest'

AWS 구현의 실행 명령, 결과와 미검증 범위는 AWS 구현 계획의 실행 증거를 따른다. 이전 공통화 기록은 공통화 계획에 보존한다. 전체 테스트는 사용자가 ./gradlew test로 실행한다.