본문 바로가기
Flutter/Flutter

플러터 프로젝트에서 DI 주입은 어떻게 이루어지는가

by 복복씨 2024. 9. 19.

 

이번 프로젝트(리를 가디언) 에서는 DI(Dependency Injection)를 사용해 더 효율적으로 프로젝트를 관리하려고 한다. Firebase가 시그널링 서버로서 작동하지 않을 경우를 대비하고, 앱이 커질 때도 유연하게 대처하기 위해서다. 먼저 간단한 용어설명을 하겠다

DI란?

DI는 객체들이 서로 의존하지 않고, 외부에서 필요한 객체를 주입하는 방식이다.

이를 통해 코드의 결합도를 낮추고, 변경이나 확장이 용이해진다.

DI를 사용하는 이유

  1. 확장성: 앱이 커지거나 다른 서비스로 교체될 때, 코드를 쉽게 수정할 수 있다.
  2. 테스트 용이성: 의존성 주입으로 Mock 객체를 사용해 테스트를 독립적으로 진행할 수 있다.
  3. 유지보수성: 객체 간의 강한 결합을 피하고, 코드를 모듈화하여 관리하기 쉽다.

흠... 

다음과 같은 설명은 사실

너무

뻔하다. 그래 확장성, 테스트 용이성 유지보수성.. 말로만 들으면 대충 다 알겠는데!! 

머리 속에 박히는 느낌은 (완전히 이해되는 느낌) 없다

내가 궁금했던 점은

'객체들이 서로 의존한다는게 도대체 뭔데?', '외부에서 필요한 객체를 주입하는게 도대체 무슨 말인데?'
이거였다.  애초에 저 말의 의미를 몰랐다.

 

자, 이를 이해하기 위해

두 가지 상황을 코드로 비교해가며 천천히 알아보자 

1. 직접 의존성 (서로 의존하는 상태)

class MyViewModel {
  final FirebaseManager _firebaseManager = FirebaseManager();  // 직접 객체 생성

  MyViewModel() {
    // MyViewModel이 FirebaseManager에 의존
  }
}

 

MyViewModel이 직접 FirebaseManager를 생성하므로, 두 클래스가 강하게 결합된다. 의존도가 높다는 뜻이다

의존도가 높다는 것은 한 클래스가 다른 클래스내에서 객체를 직접 생성 하고 있어서, 하나가 바뀌면 다른 것도 바꿔야 하는 상태를 말한다.

 

예를 들어 FirebaseManager가 바뀌면 MyViewModel도 수정해야 한다.

 

근데 그게, 도대체 그가 뭐가 안좋지? 걍 코드 한꺼번에 찾기 해서 바꾸믄 되자나요 하고 물어보신다면

  1. 수정이 힘들어진다: 클래스 B가 바뀌면, 클래스 A도 수정해야 한다.
  2. 테스트하기 어려워진다: 클래스 A를 테스트할 때 B도 같이 테스트해야 해서 복잡해진다.
  3. 교체가 어렵다: A와 B가 너무 연결되어 있으면 다른 클래스로 쉽게 바꿀 수 없다.

아주 복잡스러워진다는거다. 그냥 하나 고치려고해도 너무 많은 자원을 써야한다는것 !! 프로젝트가 커질수록 더더 많은 자원이 낭비된다는 것! 


2. 의존성 주입 (외부에서 객체 주입)

class MyViewModel {
  final FirebaseManager _firebaseManager;  // 외부에서 주입받음

  MyViewModel(this._firebaseManager);  // 생성자로 주입

  // MyViewModel은 이제 FirebaseManager에 직접 의존하지 않음
}

 

FirebaseManager는 외부에서 주입(직접 클래스 내부에서 객체를 생성하지 않음)되기 때문에 MyViewModel직접적인 의존이 없다. 나중에 FirebaseManager 대신 다른 클래스를 쉽게 주입할 수 있다.

 

외부에서 주입하는게 도대체 뭐야??!!!!! 하신다면

외부에서 주입된다는 것은, 한 클래스가 필요한 객체를 직접 만들지 않고, 외부에서 전달받는 것을 의미한다.

예를 들어,위 코드와 같이 뷰모델 클래스 안에서 파이어베이스매니저 객체를 직접! 생성하지 않고 , 뷰모델 클래스의 생성자메서드를 호출할 때 파이어베이스매니저를 외부에서 넘겨주는 방식으로 사용한다. 이렇게 하면 뷰모델 클래스는 파이어베이스매니저에 직접 의존하지 않기 때문에 다른 객체로 쉽게 교체하거나 수정할 수 있다. 


즉, 필요한 객체는 외부에서 주입되어 더 유연하고 관리하기 쉬운 코드가 된다.

의존성 주입을 사용하면 다음과 같은 장점이 있다:

  1. 수정이 용이해진다: 나중에 FirebaseManager 대신 다른 클래스를 쉽게 주입할 수 있어 코드를 수정할 때 변경 범위가 줄어든다.
  2. 테스트하기 쉬워진다: MyViewModel을 테스트할 때, 실제 FirebaseManager 대신 Mock 객체를 주입해 독립적인 테스트가 가능하다.
  3. 유연성이 높아진다: 객체가 직접 다른 객체에 의존하지 않으므로, 요구사항에 따라 유연하게 변경할 수 있다.

대충 감이온다

 

 

그렇다면 내 프로젝트에 적용해보자 
프로젝트 내에서의 main.dart 와 di.dart 파일을 까보도록 하겠다 

 

DI를 적용하여 의존성 관리를 쉽게 하기 위해 GetIt 패키지를 사용했다. (getit패키지 없이도 진행할 수 있다) 


1. DI 설정 코드 (di/di.dart)

import 'package:get_it/get_it.dart';
import '../services/firebase_manager.dart';
import '../services/session_manager.dart';
import '../viewmodels/sound_listening_viewmodel.dart';
import '../viewmodels/pairing_viewmodel.dart';

final getIt = GetIt.instance;

void setupDI() {
  // FirebaseManager 등록
  getIt.registerLazySingleton<FirebaseManager>(() => FirebaseManager());

  // SessionManager 등록, FirebaseManager 의존성 주입
  getIt.registerLazySingleton<SessionManager>(() => SessionManager(getIt<FirebaseManager>()));

  // ViewModel 등록
  getIt.registerFactory<SoundListeningViewModel>(() => SoundListeningViewModel(getIt<SessionManager>()));
  getIt.registerFactory<PairingViewModel>(() => PairingViewModel(getIt<FirebaseManager>()));
}

 

이 파일을 만든 이유는 필요한 객체들을 미리 등록하고 관리하기 위해서다. 여기서는 FirebaseManager, SessionManager 등의 객체를 미리 생성하여, 다른 클래스들이 필요할 때 쉽게 주입받을 수 있도록 설정한다.

 

즉, 주입받을 객체들을 정의하고, 이를 앱의 다른 부분에서 사용할 수 있도록 공유하는 역할을 한다. 이로써 클래스 간의 의존성을 줄이기 위해 노력했다.


2. main.dart에서 DI 사용

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'di/di.dart';
import 'viewmodels/sound_listening_viewmodel.dart';
import 'viewmodels/pairing_viewmodel.dart';
import 'views/pairing_screen.dart';

void main() {
  setupDI();  // DI 설정 호출

  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => getIt<SoundListeningViewModel>()),
        ChangeNotifierProvider(create: (_) => getIt<PairingViewModel>()),
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sound Monitoring App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PairingScreen(),  // 앱의 초기 화면 설정
    );
  }
}

 

  1. di.dart에서 DI 설정: GetIt을 이용해 필요한 클래스들을 LazySingleton 또는 Factory로 등록.
    • LazySingleton: 한 번만 인스턴스화되어 앱 전역에서 공유됨.
    • Factory: 필요할 때마다 새로운 인스턴스를 생성.
  2. main.dart에서 사용:
    • 앱이 시작될 때 setupDI()를 호출하여 DI 설정을 먼저 수행.
    • MultiProvider를 통해 ViewModel을 화면에 주입하여 UI에서 사용할 수 있게 함.

 

뷰 모델을 주입하는 이유는 뭘까? 

: ViewModel을 주입하는 이유는 UI와 비즈니스 로직을 분리하고, 상태 관리를 효율적으로 하기 위해서다. ViewModel은 UI와 데이터 간의 중간 역할을 한다. 

이를 주입하면, UI는 필요한 데이터를 직접 관리하지 않고 ViewModel을 통해 상태와 로직을 처리하게 된다. 이렇게 하면 UI가 더 단순해진다. MVVM에 더 가깝게 다가갈수있다는것. 

 

 


 

 

이러한 고민과 방식들이 < 깔끔한 MVVM,  코드의 재사용성 높이기, 코드간 결합도를 떨어뜨리기 >  위함이지만 속을 까보면저 역할을 훌륭히 수행하고 있을지가 의문이다. 괜히 코드가 복잡해진건 아닐까? 하는 고민이든다. 저 역할을 훌륭히 수행하기 좀 더 깊이 고민해봐야겠다