이전 글 : 3. Scopes - 기초 다음 글 : 5. 비동기 객체 - 기초

style : nestedList

Advanced 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: 중복된 백그라운드 작업 방지

Objectshadowed 혹은 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

  1. 스코프 해제 함수 호출 (제공된 경우)
    • 객체 등록 시 dispose 제공한 경우. 사용 시 stream close 작업 등 가능.
  2. 등록된 객체들의 dispose 함수를 등록 역순으로 호출
    • 나중에 등록된 객체가 먼저 등록된 객체를 참조하고 있을 가능성이 높음.
    • 참조하는 객체를 먼저 안전하게 제거하여 Dangling Reference 오류를 방지
  3. 스택에서 해당 스코프를 완전히 제거
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

  1. 로그아웃 재로그인
    • 앱 전체 scope 는 유지
    • 인증 토큰, 프로필 정보 등만 제거하면 재사용하기 적합.
  2. 복잡한 폼 입력
    • 만약 사용자가 입력를 거의 다 하고 전체 취소 를 원한다면?
      • scope 자체를 reset 하는 것이 개발 및 ux 모두 good

popScope

  • 일회성 다이얼로그
    • 해당 다이얼로그 용 service 혹은 다른 객체가 필요하다면? dialogclose 될 때 pop 처리

결론: scope 를 계속 유지할 것인가(reset), 유지하지 않을 것인가(pop).

// resetScope - 현재 스코프의 모든 등록을 삭제하지만 스코프 자체는 유지함 (dispose 실행)
await getIt.resetScope(dispose: true);
 
// popScope - 현재 스코프 전체를 제거하고 이전(부모) 스코프를 복구함
await getIt.popScope();
구분resetScopepopScope
스택 깊이유지 (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스코프 파기 시 자동 정리 로직 실행