149 lines
12 KiB
Markdown
149 lines
12 KiB
Markdown
# 언어 감지·번역 공급자 확장 가이드
|
|
|
|
## 현재 실행 구조
|
|
|
|
감지 구현체는 `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)`다.
|
|
|
|
```kotlin
|
|
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 우선으로 바꾸는 예시다. 현재 기본 순서를 뜻하지 않는다.
|
|
|
|
```kotlin
|
|
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 테스트는 별도로 유지한다.
|
|
|
|
```bash
|
|
./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 구현 계획](../20261109_AWS언어감지번역/plan-task.md)의 실행 증거를 따른다.
|
|
이전 공통화 기록은 [공통화 계획](plan-task.md)에 보존한다. 전체 테스트는 사용자가 `./gradlew test`로 실행한다.
|