의존성 주입 — get_it, injectable로 DI 구현
의존성 주입 — get_it, injectable로 DI 구현
의존성 주입(Dependency Injection)은 객체가 필요한 의존성을 외부에서 주입받는 패턴입니다. 테스트 가능한 코드, 느슨한 결합, 유연한 교체가 핵심 목적입니다.
왜 DI가 필요한가?
// 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
dependencies:
get_it: ^8.0.0
기본 사용
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());
}
사용
// 어디서든 가져오기
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로 어노테이션 기반 자동 등록을 할 수 있습니다.
dependencies:
get_it: ^8.0.0
injectable: ^2.4.0
dev_dependencies:
injectable_generator: ^2.6.0
build_runner: ^2.4.0
설정
// 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();
어노테이션으로 등록
// 싱글톤
@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);
}
환경별 등록
// 환경별 다른 구현체 등록
@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());
}
코드 생성
dart run build_runner build
Bloc/Provider와 함께 사용
// 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(),
)
테스트에서 교체
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을 등록합니다
댓글 로딩 중...