Files
life-helper/docs/design/311-llm-warmup/fn-concurrent_load_guard.md
joungmin 41457ab96e [08-Documenter] #311 설계서 Approved + reference + 사용자 가이드 FAQ
- docs/design/311-llm-warmup/ 3파일 상태 Draft → Approved (v0.4.1)
- docs/reference/311-llm-warmup.md 신규 (상태 머신, API, UI binding,
  마이크로카피, 테스트)
- docs/guides/ai-chat-using.md 헤더 + FAQ 2건 추가 (warmup 인지/회복)

Refs #311
2026-06-15 13:25:38 +09:00

86 lines
4.0 KiB
Markdown

# 함수 설계서: `GemmaLlmService.load` concurrent guard (#311)
> **부모 설계서**: ./README.md · **상태**: Approved (v0.4.1, 2026-06-15)
> **작성**: [AI] Architect · **구현**: `app/lib/data/ai/gemma_llm_service.dart:load` (수정) · **테스트**: `app/test/data/ai/gemma_llm_service_test.dart` (concurrent 케이스 추가) / `chat_warmup_test.dart` (시뮬)
## 1. 시그니처
```dart
class GemmaLlmService implements LlmService {
Future<void>? _loadingFuture; // 신규 필드
@override
Future<void> load() {
if (_loaded) return Future.value();
final existing = _loadingFuture;
if (existing != null) return existing;
final future = _doLoad();
_loadingFuture = future;
return future.whenComplete(() {
_loadingFuture = null;
});
}
Future<void> _doLoad() async {
// 기존 load() 본문 (initialize → installModel → getActiveModel).
}
}
```
> `MockLlmService.load()` 도 같은 패턴 적용 (`_loadingFuture` 필드 추가). 테스트의 동시성 검증 일관성.
## 2. 책임 (단일 책임)
`load()` 가 진행 중일 때 다른 caller 가 호출하면 새 작업을 시작하지 않고 같은 Future 를 반환한다. native runtime 의 `FlutterGemma.installModel` + `getActiveModel` 가 두 번 불리지 않도록 보호.
## 3. 입력
- 없음 (메서드).
## 4. 출력
- **반환**: `Future<void>` — 단일 native init 작업의 완료 future. 모든 caller 가 같은 인스턴스 공유.
- **부수효과**: `_loadingFuture`, `_loaded`, `_model` 필드 변경.
## 5. 동작 / 알고리즘
```
1. _loaded == true → 즉시 완료 Future 반환.
2. _loadingFuture != null → 그 future 그대로 반환. (새 작업 시작 X)
3. 그 외:
a. future = _doLoad();
b. _loadingFuture = future;
c. future.whenComplete(() => _loadingFuture = null);
d. return future;
```
`_doLoad()` 내부 = 기존 `load()` 본문 그대로 (initialize → installModel → getActiveModel → _loaded=true).
## 6. 에러 & 실패 모드
| 조건 | 처리 | 반환/예외 |
|------|------|-----------|
| `_doLoad()` 가 throw | `whenComplete``_loadingFuture = null` 처리 후 throw 전파 | 모든 caller 가 같은 exception 받음 |
| caller A 가 await 중에 caller B 도 호출 | 같은 future 반환 (step 2) | 둘 다 동일하게 완료 또는 fail |
| 첫 호출 실패 후 재시도 | `_loadingFuture=null` 로 cleared → 다음 호출 시 새 `_doLoad()` 시작 | 정상 재시도 가능 |
## 7. 엣지케이스
- **load() 와 unload() race**: caller A 가 load → 진행 중 caller B 가 unload() 호출. `_doLoad()` 가 _model 설정 직후 unload 가 _model.close() 호출. 본 이슈 범위 외 — 현재 시점에 unload() 호출 경로 없음 (#219 가 다룰 영역). 본 설계는 load 의 concurrent 만 다룬다.
- **whenComplete 실행 시점**: future 가 동기 완료 (이미 _loaded=true 인 첫 분기) 시에도 `_loadingFuture=null` 보장. 단, step 1 에서 early return 이라 _loadingFuture 는 손대지 않음.
## 8. 복잡도 / 성능
- 시간: 첫 호출 = 기존 _doLoad 비용. 후속 caller = O(1) future 공유.
- 공간: future 1개 + null 가능 필드.
- 호출 빈도: ChatScreen mount + userTurn 첫 호출 + frame suggestion (#215) — 모두 일생에 몇 회.
## 9. 의존성
- `flutter_gemma` `FlutterGemma.initialize` / `installModel` / `getActiveModel` (기존).
- `_loaded` / `_model` 필드 (기존).
## 10. 테스트 케이스
- [ ] 정상: load() 1회 호출 → _doLoad() 1회 실행.
- [ ] concurrent: load() 두 번 await 동시 호출 → _doLoad() 1회만 실행, 두 future 같은 Future 인스턴스.
- [ ] 실패 후 재시도: 첫 _doLoad throws → caller A 에게 propagate → _loadingFuture cleared → 두 번째 load() 새 _doLoad 시작.
- [ ] isLoaded 이미 true: load() → 즉시 완료, _doLoad 미실행.
> Gemma native 는 통합 테스트에서만 검증 가능. 단위 테스트는 MockLlmService 의 동일 가드로 시뮬.
## 11. 추적성
- 인수조건: AC7.
- 관련 follow-up: #220 (purge try/catch — 동일 정신).
- 관련 ADR: 없음.