111 lines
8.0 KiB
Markdown
111 lines
8.0 KiB
Markdown
# 언어 감지·번역 공급자 확장 가이드
|
|
|
|
## 현재 실행 구조
|
|
|
|
운영 구현체는 `PapagoLanguageDetectionProvider`와 `PapagoTranslationService`뿐이다.
|
|
AWS 감지·번역 adapter, 해당 SDK 및 전용 인증 설정은 추가하지 않았다. 지금 `aws`를 순서에 넣으면 시작에 실패한다.
|
|
|
|
감지와 번역은 별도 capability다. 감지만 또는 번역만 제공하는 공급자도 등록할 수 있다.
|
|
두 capability에서 같은 `providerName`을 쓰는 것은 가능하지만, 한 capability 안의 이름 중복은 시작 시 거부한다.
|
|
|
|
| 경계 | 계약 |
|
|
|---|---|
|
|
| `LanguageDetectionProvider` | `providerName: String`, `detect(query: String): String?` |
|
|
| `LanguageDetectionCacheService` | `detectWithCache(query: String): DetectedLanguage?` |
|
|
| `DetectedLanguage` | `detectedLanguage: String`, `providerName: String` |
|
|
| `TranslationProvider` | `providerName: String`, `providerVersion: String`, `translate(TranslateRequest): TranslateResult` |
|
|
| `TranslationCoordinator` | `findCached(sourceHash, sourceLanguage, targetLanguage): TranslationMemory?`, `translate(TranslateRequest): ProviderTranslationResult` |
|
|
| `ProviderTranslationResult` | `result: TranslateResult`, `providerName: String`, `providerVersion: String` |
|
|
|
|
인터페이스와 DTO는 `src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/`에 있다.
|
|
감지 coordinator는 기존 `content/LanguageDetectionCacheService.kt`이며 별도 vendor가 아니다.
|
|
`TranslationCoordinator`도 `TranslationProvider`를 구현하지 않는다.
|
|
|
|
## 새 공급자 추가 순서
|
|
|
|
1. 필요한 capability 인터페이스를 구현하고 Spring `@Component`/`@Service` 또는 `@Bean`으로 등록한다.
|
|
`providerName`은 capability 내 유일하고 재배포 후에도 안정적인 값이어야 한다.
|
|
2. 공급자의 HTTP/SDK 및 인증은 adapter 안에 둔다. 외부 응답 형식과 빈 결과를 검증하고,
|
|
원문·인증정보·응답 본문·예외 cause를 로그로 출력하지 않는다. 번역은 입력별 결과를 같은 순서로 반환한다.
|
|
3. `LanguageProviderOrder.kt`에서 구현한 capability의 목록에만 이름을 넣고 빌드·배포한다. `@Primary`, selector 분기, enum 등록은 필요 없다.
|
|
4. 아래 테스트 경계로 순서·실패 전환·실제 provider 저장·무호출 캐시 재사용·materializer 표시를 검증한다.
|
|
|
|
콘텐츠·댓글·커뮤니티 호출자, worker, materializer, scheduler에 공급자 분기를 추가하지 않는다.
|
|
기존 두 coordinator는 주입된 구현체 목록과 설정만 사용한다. 공급자마다 다른 분할·요청 크기 제한은 adapter 책임이다.
|
|
Papago의 5,000 UTF-16 단위 분할과 서로게이트 쌍 보존은 `PapagoTranslationService`에 남아 있다.
|
|
ko/en/ja 서비스 정책은 `TranslationLanguages`에 있으며 공급자를 추가해도 자동 확장하지 않는다.
|
|
|
|
## 기능별 독립 코드 목록
|
|
|
|
production 순서의 유일한 편집 지점은
|
|
`src/main/kotlin/kr/co/vividnext/sodalive/i18n/translation/LanguageProviderOrder.kt`다.
|
|
목록의 왼쪽부터 사용하며 기본값은 두 기능 모두 `listOf("papago")`다.
|
|
|
|
```kotlin
|
|
object LanguageProviderOrder {
|
|
val detectionOrder = listOf("papago")
|
|
val translationOrder = listOf("papago")
|
|
}
|
|
```
|
|
|
|
Kotlin 목록은 `const val`이 아닌 `val`이다. `LanguageProviderProperties`는 기존 주입 wrapper로 유지하며,
|
|
Spring은 `@Autowired` 기본 생성자로 위 목록을 읽는다. 문자열 파싱은 하지 않는다.
|
|
테스트에서는 전역 목록을 변경하지 않고 `LanguageProviderProperties(List<String>, List<String>)`로 만든 테스트 bean을 주입한다.
|
|
|
|
다음은 **secondary 구현체를 두 capability에 등록한 뒤에만** 적용할 수 있는 예시다.
|
|
현재 `secondary` 구현체는 테스트에만 있다.
|
|
|
|
```kotlin
|
|
val detectionOrder = listOf("secondary", "papago")
|
|
val translationOrder = listOf("papago", "secondary")
|
|
```
|
|
|
|
감지만 순서를 바꾸어도 번역 순서는 바뀌지 않는다. 반대로 번역만 바꿀 수도 있다.
|
|
변경 후 빌드·배포해야 반영된다. 과거 우선순위 Spring property·환경변수·YAML 값이 남아 있어도 순서는 바뀌지 않는다.
|
|
공급자 인증정보는 계속 환경변수로 주입하며 이 코드 목록에 넣지 않는다.
|
|
빈 값·빈 항목·중복 이름·미등록 이름·capability 안의 중복 bean 이름표(`providerName`)는 초기화에서 거부한다.
|
|
공급자가 번역에만 등록됐다면 그 이름을 감지 순서에 넣을 수 없다.
|
|
실행 중 설정 변경은 지원하지 않는다. AWS 구현체 등록 전 `listOf("papago", "aws")`는 사용할 수 없다.
|
|
|
|
## 캐시와 오류 계약
|
|
|
|
감지는 정규화/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 기본 생성자, 과거 property/env 무시, typed-list 테스트 bean의 독립 순서와 잘못된 목록의 시작 실패 |
|
|
| `LanguageProviderPropertiesTest` | 순서 검증 및 선택되지 않은 bean을 포함한 capability 내 중복 이름 거부 |
|
|
| `LanguageDetectionProviderTest` / `TranslationCoordinatorTest` | 전체 요청 폴백, 첫 성공 중단, cache-first, 안전한 전체 실패, DB 오류 전파 |
|
|
| `PapagoLanguageDetectionProviderTest` / `PapagoTranslationServiceTest` | 제어된 HTTP form/header/response와 기존 17개 chunk 회귀 |
|
|
| `LanguageProviderCacheEndToEndTest` | 실제 H2의 secondary 감지/번역 귀속, providerVersion, 두 번째 요청 무호출, worker와 materializer의 실제 표시 |
|
|
|
|
H2 버전은 MySQL `FOR UPDATE SKIP LOCKED`를 지원하지 않으므로 새 H2 테스트는 claim 조회 한 곳만
|
|
test spy로 대체한다. 나머지 repository 저장·별도 트랜잭션 재조회·materialization은 실제 H2다.
|
|
이 테스트가 운영 MySQL claim 동시성을 검증한다는 뜻은 아니다. 기존 감지 cache 경합·revision 테스트는 별도로 유지한다.
|
|
|
|
```bash
|
|
./gradlew --no-daemon --offline test --tests 'kr.co.vividnext.sodalive.i18n.translation.*' --tests 'kr.co.vividnext.sodalive.content.LanguageDetectionCacheServiceTest'
|
|
```
|
|
|
|
전체 실행 명령, 결과와 미검증 범위는 [plan-task.md](plan-task.md)의 실행 증거를 따른다.
|