이전 글 : 3. Scopes - 기초 다음 글 : 5. 비동기 객체 - 기초
style : nestedListAdvanced Scope Feature
Final Scopes
사용 예시
- Building plugin systems where scope setup must be atomic
- Preventing accidental registration after scope initialization
목적
race condition 예방 ← scope 생성 이후 lock
getIt.pushNewScope(
isFinal: true, // Can't register after init completes
init: (getIt) {
// MUST register everything here
getIt.registerSingleton<ServiceA>(ServiceA());
getIt.registerSingleton<ServiceB>(ServiceB());
},
);Shadow Change Handlers
사용하는 경우
- 1: 비활성 상태일 때 중단되어야 하는 리소스 집약적 서비스
- 2: 정리 및 복구가 필요한 구독 기반 서비스
- 3: 중복된 백그라운드 작업 방지
Object 가 shadowed 혹은 restored 될 경우, notify 를 하도록 설정 가능.
class StreamingService implements ShadowChangeHandlers {
StreamSubscription? _subscription;
final Stream<dynamic> dataStream = Stream.empty();
void init() {
_subscription = dataStream.listen(_handleData);
}
void _handleData(dynamic data) {
// 데이터 처리
}
@override
void onGetShadowed(Object shadowingObject) {
// 다른 StreamingService가 활성화됨 - 현재 작업 일시 중지
_subscription?.pause();
print('일시 중지: $shadowingObject 가 현재 스트림을 처리 중입니다');
}
@override
void onLeaveShadow(Object shadowingObject) {
// 다시 활성화됨 - 작업 재개
_subscription?.resume();
print('재개: $shadowingObject 가 제거되었습니다');
}
}Scope Change Notification
- 모든
scope변경 발생 시notifier설정 가능.
getIt.onScopeChanged = (bool pushed) {
if (pushed) {
print('New scope pushed - UI might need rebuild');
} else {
print('Scope popped - UI might need rebuild');
}
};Scope 생명 주기 & 제거
Disposal Order
- 스코프 해제 함수 호출 (제공된 경우)
- 객체 등록 시
dispose제공한 경우. → 사용 시streamclose작업 등 가능.
- 객체 등록 시
- 등록된 객체들의 dispose 함수를 등록 역순으로 호출
- 나중에 등록된 객체가 먼저 등록된 객체를 참조하고 있을 가능성이 높음.
- 참조하는 객체를 먼저 안전하게 제거하여 Dangling Reference 오류를 방지
- 스택에서 해당 스코프를 완전히 제거
void main() async {
// 스코프에서 서비스 인스턴스 획득
final service = getIt<MyServiceImpl>();
service.doWork();
service.saveState();
// 스코프 종료 시 정리(cleanup) 작업 호출
await getIt.popScope();
// 현재 스코프는 파기되었으며, 등록된 서비스는 해제됨
// 이후 접근 시 부모 스코프에 등록된 인스턴스를 획득 (존재하는 경우)
final parentScopeService = getIt<MyServiceImpl>();
}Implementing Disposable Interface
- 객체를 등록할 때
dispose제공하는 것이 기본. - 그러나,
Disposable을 구현함으로써도 해당 기능 사용 가능.
class MyService implements Disposable {
final Stream<dynamic> stream = Stream.empty();
StreamSubscription? _subscription;
void init() {
_subscription = stream.listen((data) {});
}
@override
Future<void> onDispose() async {
await _subscription?.cancel();
// Cleanup resources
}
}
void setup() {
// Automatically calls onDispose when scope pops or object is unregistered
getIt.registerSingleton<MyService>(MyService()..init());
}Reset vs Pop
함수별 사용하는 경우
resetScope
- 로그아웃 → 재로그인
- 앱 전체
scope는 유지 인증 토큰,프로필 정보등만 제거하면 재사용하기 적합.
- 앱 전체
- 복잡한 폼 입력
- 만약 사용자가 입력를 거의 다 하고
전체 취소를 원한다면?scope자체를reset하는 것이 개발 및ux모두 good
- 만약 사용자가 입력를 거의 다 하고
popScope
- 일회성 다이얼로그
- 해당 다이얼로그 용
service혹은 다른 객체가 필요하다면? →dialog가close될 때pop처리
- 해당 다이얼로그 용
→ 결론: scope 를 계속 유지할 것인가(reset), 유지하지 않을 것인가(pop).
// resetScope - 현재 스코프의 모든 등록을 삭제하지만 스코프 자체는 유지함 (dispose 실행)
await getIt.resetScope(dispose: true);
// popScope - 현재 스코프 전체를 제거하고 이전(부모) 스코프를 복구함
await getIt.popScope();| 구분 | resetScope | popScope |
|---|---|---|
| 스택 깊이 | 유지 (Depth가 변하지 않음) | 감소 (Depth가 하나 줄어듦) |
| 메모리 주소 | 동일한 스코프 인덱스를 가리킴 | 이전 스코프 인덱스로 포인터 이동 |
| 핵심 의도 | ”재사용” (Clean Slate) | “종료” (Destruction) |
| 내부 상태 처리 | 초기화 | 완전 삭제 |
1. 주요 활용 패턴
로그인/로그아웃 흐름
class AuthService {
Future<void> login(String username, String password) async {
final user = await getIt<ApiClient>().login(username, password);
// 인증된 스코프 생성
getIt.pushNewScope(scopeName: 'authenticated');
// 인증 관련 객체 등록
getIt.registerSingleton<User>(user);
getIt.registerSingleton<ApiClient>(AuthenticatedApiClient(user.token));
getIt.registerSingleton<NotificationService>(NotificationService(user.id));
}
Future<void> logout() async {
// 스코프 제거 - 인증된 모든 서비스 자동 정리(Cleanup)
await getIt.popScope();
// 이제 베이스 스코프의 객체들이 다시 활성화됨
}
}멀티 테넌시(Tenant) 관리
class TenantManager {
Future<void> switchTenant(String tenantId) async {
// 이전 테넌트 스코프가 존재하면 제거
if (getIt.hasScope('tenant')) {
await getIt.popScope();
}
// 새로운 테넌트 스코프 비동기 생성
await getIt.pushNewScopeAsync(
scopeName: 'tenant',
init: (getIt) async {
final config = await loadTenantConfig(tenantId);
getIt.registerSingleton<TenantConfig>(config);
final database = await openTenantDatabase(tenantId);
getIt.registerSingleton<Database>(database);
final api = ApiClient(config.apiUrl);
getIt.registerSingleton<TenantServices>(
TenantServices(database, api),
);
},
);
}
Future<TenantConfig> loadTenantConfig(String tenantId) async {
await Future.delayed(const Duration(milliseconds: 10));
return TenantConfig('tenant_db', 'api_key_123');
}
}기능별 스코프 동적 제어
class FeatureManager {
final Map<String, bool> _activeFeatures = {};
void enableFeature(String featureName, FeatureImplementation impl) {
if (_activeFeatures[featureName] == true) return;
getIt.pushNewScope(scopeName: 'feature-$featureName');
impl.register(getIt);
_activeFeatures[featureName] = true;
}
Future<void> disableFeature(String featureName) async {
if (_activeFeatures[featureName] != true) return;
await getIt.dropScope('feature-$featureName');
_activeFeatures[featureName] = false;
}
}테스트 환경에서의 활용 (Mocking)
group('UserService Tests', () {
setUp(() {
configureDependencies(); // 실제 DI 초기화 실행
getIt.pushNewScope(); // 테스트용 스코프 생성
// 특정 서비스만 Mock으로 교체
getIt.registerSingleton<ApiClient>(MockApiClient());
getIt.registerSingleton<Database>(MockDatabase());
});
tearDown(() async {
await getIt.popScope(); // Mock 제거 및 실제 서비스 복구
});
test('사용자 데이터를 로드해야 함', () async {
final service = getIt<UserService>();
final user = await service.loadUser('123');
expect(user.id, '123');
});
});- 장점: 모든 등록 과정을 복제할 필요 없음, 필요한 부분만 Mocking 가능,
popScope()를 통한 자동 정리.
디버깅 및 상태 확인
현재 상태 모니터링
-
현재 스코프 확인:
getIt.currentScopeName(이름 없는 경우 null) -
객체 등록 위치 추적:
final registration = getIt.findFirstObjectRegistration<MyService>(); print('등록된 스코프: ${registration?.instanceName}'); -
스코프 존재 여부:
getIt.hasScope('authenticated')
권장 사항
권장하는 방식
-
이름 지정: 디버깅을 위해 스코프에 이름을 부여하십시오.
-
init 파라미터 활용: 스코프 생성과 동시에 객체를 등록하십시오.
-
비동기 처리:
popScope()호출 시 반드시await를 사용하십시오. -
인터페이스 구현: 수동 dispose 함수 전달보다
Disposable인터페이스 구현을 권장합니다.
4.2 피해야 할 방식
- 임시 상태 저장: 단순 변수나 파라미터로 해결 가능한 곳에 스코프를 쓰지 마십시오.
- 누수 주의: 스코프를
pop하지 않으면 메모리 누수가 발생합니다. - Build 메서드 내 금지: Flutter
build메서드 안에서 직접 스코프를 생성하지 마십시오. (watch_it라이브러리 활용 권장)
관련 API
스코프 관리 메서드
| 메서드 | 설명 |
|---|---|
pushNewScope() | 새로운 스코프 생성 및 즉시 등록 지원 |
popScope() | 현재 스코프 제거 및 객체 파기 |
dropScope(name) | 특정 이름의 스코프를 찾아 제거 |
resetScope() | 스코프 공간은 유지하되 내부 등록만 초기화 |
수명 주기 인터페이스
| 인터페이스 | 설명 |
|---|---|
ShadowChangeHandlers | 다른 스코프에 의해 가려지거나 다시 나타날 때 알림 수신 |
Disposable | 스코프 파기 시 자동 정리 로직 실행 |