의존성 주입 — get_it, injectable로 DI 구현

의존성 주입(Dependency Injection)은 객체가 필요한 의존성을 외부에서 주입받는 패턴입니다. 테스트 가능한 코드, 느슨한 결합, 유연한 교체가 핵심 목적입니다.


왜 DI가 필요한가?

DART
// DI 없이 — 직접 생성 (강한 결합)
class UserService {
  final apiClient = ApiClient();  // 직접 생성 → 교체 불가
  final db = Database();          // 테스트 시 Mock 불가
}

// DI 적용 — 외부에서 주입 (느슨한 결합)
class UserService {
  final ApiClient apiClient;
  final Database db;

  UserService({required this.apiClient, required this.db});
}

// 테스트에서 Mock 주입 가능
final service = UserService(
  apiClient: MockApiClient(),
  db: MockDatabase(),
);

get_it — Service Locator

YAML
dependencies:
  get_it: ^8.0.0

기본 사용

DART
import 'package:get_it/get_it.dart';

// 전역 인스턴스
final getIt = GetIt.instance;

// 등록 (보통 main에서)
void setupDependencies() {
  // 싱글톤: 앱 전체에서 하나의 인스턴스
  getIt.registerSingleton<ApiClient>(ApiClient());

  // 레이지 싱글톤: 처음 사용할 때 생성
  getIt.registerLazySingleton<Database>(() => Database());

  // 팩토리: 매번 새 인스턴스 생성
  getIt.registerFactory<UserRepository>(
    () => UserRepositoryImpl(
      apiClient: getIt<ApiClient>(),
      database: getIt<Database>(),
    ),
  );

  // 비동기 싱글톤
  getIt.registerSingletonAsync<SharedPreferences>(
    () => SharedPreferences.getInstance(),
  );
}

void main() async {
  setupDependencies();
  await getIt.allReady();  // 비동기 등록 완료 대기
  runApp(const MyApp());
}

사용

DART
// 어디서든 가져오기
final apiClient = getIt<ApiClient>();
final userRepo = getIt<UserRepository>();

// 위젯에서 사용
class UserScreen extends StatelessWidget {
  const UserScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final userRepo = getIt<UserRepository>();
    // ...
  }
}

등록 방법 비교

방법생성 시점인스턴스 수
registerSingleton즉시1개
registerLazySingleton첫 사용 시1개
registerFactory매번 호출 시N개
registerSingletonAsync비동기 즉시1개

injectable — 코드 생성 기반 DI

get_it을 수동으로 설정하면 등록 코드가 길어집니다. injectable로 어노테이션 기반 자동 등록을 할 수 있습니다.

YAML
dependencies:
  get_it: ^8.0.0
  injectable: ^2.4.0

dev_dependencies:
  injectable_generator: ^2.6.0
  build_runner: ^2.4.0

설정

DART
// lib/injection.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';

import 'injection.config.dart';

final getIt = GetIt.instance;

@InjectableInit()
void configureDependencies() => getIt.init();

어노테이션으로 등록

DART
// 싱글톤
@singleton
class ApiClient {
  final Dio dio;

  ApiClient() : dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
}

// 레이지 싱글톤
@lazySingleton
class Database {
  // ...
}

// 팩토리
@injectable
class UserRepository {
  final ApiClient apiClient;

  // 생성자 주입 — get_it이 자동으로 ApiClient를 주입
  UserRepository(this.apiClient);
}

// 인터페이스 구현체 등록
@LazySingleton(as: AuthRepository)
class AuthRepositoryImpl implements AuthRepository {
  final ApiClient apiClient;

  AuthRepositoryImpl(this.apiClient);
}

환경별 등록

DART
// 환경별 다른 구현체 등록
@dev
@LazySingleton(as: ApiClient)
class DevApiClient implements ApiClient {
  // 개발 서버 연결
}

@prod
@LazySingleton(as: ApiClient)
class ProdApiClient implements ApiClient {
  // 운영 서버 연결
}

// 초기화 시 환경 지정
@InjectableInit()
void configureDependencies(String environment) =>
    getIt.init(environment: environment);

// main
void main() {
  configureDependencies(Environment.prod);
  runApp(const MyApp());
}

코드 생성

BASH
dart run build_runner build

Bloc/Provider와 함께 사용

DART
// Bloc에 DI 적용
@injectable
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final LoginUseCase loginUseCase;

  // injectable이 LoginUseCase를 자동 주입
  AuthBloc(this.loginUseCase) : super(AuthInitial()) {
    on<LoginRequested>(_onLogin);
  }
}

// 위젯에서 사용
BlocProvider(
  create: (_) => getIt<AuthBloc>(),
  child: const LoginScreen(),
)

테스트에서 교체

DART
void main() {
  setUp(() {
    // 테스트용 Mock 등록
    getIt.registerSingleton<AuthRepository>(MockAuthRepository());
  });

  tearDown(() {
    getIt.reset();  // 등록 초기화
  });

  test('로그인 테스트', () async {
    final bloc = getIt<AuthBloc>();
    // ...
  });
}

정리

  • DI는 테스트 가능한 코드와 느슨한 결합을 위한 핵심 패턴입니다
  • get_it은 Flutter에서 가장 많이 쓰이는 Service Locator입니다
  • injectable로 어노테이션 기반 자동 등록을 하면 보일러플레이트가 줄어듭니다
  • 싱글톤, 레이지 싱글톤, 팩토리 중 용도에 맞게 선택하세요
  • 환경별(dev, prod) 구현체를 분리하여 유연한 설정이 가능합니다
  • 테스트 시 getIt.reset()으로 초기화하고 Mock을 등록합니다
댓글 로딩 중...