docs(i18n): 공급자 확장 방법과 검증 결과를 기록한다
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
# 언어 감지·번역 공급자 확장 가이드
|
||||
|
||||
## 현재 실행 구조
|
||||
|
||||
운영 구현체는 `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. 구현한 capability의 설정 순서에만 이름을 넣고 재시작/재배포한다. `@Primary`, selector 분기, enum 등록은 필요 없다.
|
||||
4. 아래 테스트 경계로 순서·실패 전환·실제 provider 저장·무호출 캐시 재사용·materializer 표시를 검증한다.
|
||||
|
||||
콘텐츠·댓글·커뮤니티 호출자, worker, materializer, scheduler에 공급자 분기를 추가하지 않는다.
|
||||
기존 두 coordinator는 주입된 구현체 목록과 설정만 사용한다. 공급자마다 다른 분할·요청 크기 제한은 adapter 책임이다.
|
||||
Papago의 5,000 UTF-16 단위 분할과 서로게이트 쌍 보존은 `PapagoTranslationService`에 남아 있다.
|
||||
ko/en/ja 서비스 정책은 `TranslationLanguages`에 있으며 공급자를 추가해도 자동 확장하지 않는다.
|
||||
|
||||
## 기능별 독립 설정
|
||||
|
||||
`LanguageProviderProperties`는 component scan으로 등록되며 생성자 `@Value`로 문자열을 주입받는다.
|
||||
별도 `@EnableConfigurationProperties` 등록은 필요 없다. 쉼표로 나누고 각 이름의 앞뒤 공백을 제거한다.
|
||||
기본값은 두 기능 모두 `papago`다.
|
||||
|
||||
```yaml
|
||||
sodalive:
|
||||
language-providers:
|
||||
detection-order: ${LANGUAGE_PROVIDERS_DETECTION_ORDER:papago}
|
||||
translation-order: ${LANGUAGE_PROVIDERS_TRANSLATION_ORDER:papago}
|
||||
```
|
||||
|
||||
다음은 **secondary 구현체를 두 capability에 등록한 뒤에만** 적용할 수 있는 예시다.
|
||||
현재 `secondary` 구현체는 테스트에만 있다.
|
||||
|
||||
```bash
|
||||
export LANGUAGE_PROVIDERS_DETECTION_ORDER=secondary,papago
|
||||
export LANGUAGE_PROVIDERS_TRANSLATION_ORDER=papago,secondary
|
||||
```
|
||||
|
||||
감지만 순서를 바꾸어도 번역 순서는 바뀌지 않는다. 반대로 번역만 바꿀 수도 있다.
|
||||
Spring property를 직접 지정할 때는 `sodalive.language-providers.detection-order`와
|
||||
`sodalive.language-providers.translation-order`를 각각 설정한다.
|
||||
빈 값·빈 항목·중복 이름·미등록 이름·capability 안의 중복 bean 이름표(`providerName`)는 초기화에서 거부한다.
|
||||
공급자가 번역에만 등록됐다면 그 이름을 감지 순서에 넣을 수 없다.
|
||||
실행 중 설정 변경은 지원하지 않는다. AWS 구현체 등록 전 `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` | 실제 `@Value` binding, 두 test 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)의 실행 증거를 따른다.
|
||||
Reference in New Issue
Block a user