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

107 lines
7.6 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. 구현한 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)의 실행 증거를 따른다.