Flutter - 확장 가능한 앱 아키텍처

JAMEe_·2026년 1월 14일

flutter

목록 보기
3/3

의존성 주입

기본 패턴

의존성이 없는 객체는 생성 시 스스로 정해짐
무기에 대한 선택 불가

class Adventurer {
  final Weapon = Gun();
  
  const Adventurer();

  void attack() {
    // 무기로 때립니다
  }
}

생성자 주입

가장 일반적으로 사용되며 불변성 강조에 사용
객체를 생성할 때 필요한 의존성을 명시적으로 전달하여
무기에 대한 선택이 가능 ( 단, 무기는 객체 생성 시 필수로 전달 )
코드의 가독성과 유지보수성을 높임

final Weapon weapon = Gun();

final Adventurer adventurer = Adventurer(weapon);

adventurer.attack();

속성 주입

무기가 없이도 어드벤처는 존재 가능하고 이후에 장착
일부 의존성이 필수가 아닌 선택적으로 주입되어야 할 때 유용

final Adventurer adventurer = Adventurer();

final Weapon weapon = Axe();

adventurer.equip(weapon);

메서드 주입

행동을 할 때 무기를 주입
특정 메서드에서만 의존성이 필요한 경우에 사용

final Adventurer adventurer = Adventurer();

final Weapon weapon = Axe();

final Weapon weapon2 = Gun();

adventurer.attack(weapon2);

정리해보자면,
생성자 주입: 기본적인 필수 의존성
속성 주입: 선택적인 의존성 ( 주입된 객체 필드에 저장 )
메서드 주입: 선택적이면서 특정 메서드 ( 매개변수에서 주입된 객체 1회성 사용 )
※ 메서드 주입 과정에서, 주입된 객체를 필드에 저장하는 순간
그건 더 이상 메서드 주입이 아닌 속성 주입

의존성 주입 왜 필요해?
빼빼로 클래스에 대해 내부에서 초코 코팅을 선언하면, 빼빼로 클래스는 초코만 생성 가능. 즉, 딸기, 아몬드와 같은 맛을 동시에 생성할 수 없음

get_it 이론

서비스 로케이터 기반 간단하고 가벼운 의존성 주입 라이브러리

서비스 로케이터
객체의 인스턴스를 관리하고 검색하는 디자인 패턴
의존성 주입이 적용되지 않은 상황에 객체간의 의존성 해결에 사용
장점
싱글톤 패턴이며, 모든 의존성이 중앙 집중식 관리.
단점
객체가 자신에게 필요한 의존성을 클래스 내부에서 직접 조회하는 방식이기 때문에,
외부에서 객체를 생성하거나 사용하는 입장에서는 해당 객체가 어떤 의존성에 의존하고 있는지 명확히 드러나지 않는다.

get_it 실습

enum
컴파일 타임에 고정된(const) 값들의 집합이며,
내부에는 가변 상태를 가질 수 없다.

// 아래 코드에 int counter 넣는건 부적절 (가변이어서)
enum CounterMode {
  plus,
  minus;

  CounterMode next() {
    switch (this) {
      case CounterMode.plus:
        return CounterMode.minus;
      case CounterMode.minus:
        return CounterMode.plus;
    }
  }
}

상태에 대한 이해를 돕기 위한 추가 코드

enum TrafficLight { 
	red,
	yellow,
	green;

    TrafficLight next() {
      switch (this) {
      	...
      }
    }
  }
}

TrafficLight light = TrafficLight.red;

TrafficLight 는 총 3개의 상태를 가지고 있으며 현재 red 로 초기화
next() 안에 switch 의 this 는 red

주로 enum 으로 상태를 선언하고 해당 enum 을 컨트롤하는 class 를 따로 구현해 비즈니스 로직 구현

get_it 실습코드

// main.dart
final GetIt locator = GetIt.instance;

void main() {
  locator.registerSingleton<CounterModel>(CounterModel());
  locator.registerSingleton<CounterModeModel>(CounterModeModel());
  runApp(const CounterApp());
}

// screen.dart
locator.get<CounterModel>().counter.toString();

위 코드에서 알 수 있듯이 Dart 에서는 Type 값으로 해당하는 인스턴스를 불러올 수 있다
Dart 는 런타임 후에도 타입 정보가 유지되기 때문
( TS 에서는 컴파일 후 삭제되어서 Type 값으로 특정 값 리턴 불가 )

injectable 이론

@Annotation 을 이용한 의존성 주입. 주로 get_it 과 함께 사용
injectable: 클래스나 함수에 어노테이션 추가하여 의존성을 자동으로 등록
get_it: 불러오기만 담당

// dependency.dart

import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'dependency.config.dart';

final GetIt locator = GetIt.instance;

@injectableInit
void configureDependencies() => locator.init();

위와 같이 파일 생성 후 아래 명령어 실행
build_runner 는 모든 작성된 코드를 훑어보면서 @Annotation 이 발견되면 그에 맞는 .g.dart 혹은 .config.dart 파일을 자동으로 생성
injectable 라이브러리와 build_runner 사이에는 어떤 파일에 @injectableInit 이라고 써놓여져있으면 build_runner 실행 시 .config.dart 를 붙인 설정 파일을 만들어줌

flutter pub run build_runner build --delete-conflicting-outputs

아래 코드는 build_runner 실행으로 인해 생성된 코드

// dependency.config.dart

import 'package:get_it/get_it.dart' as _i1;
import 'package:injectable/injectable.dart' as _i2;

extension GetItInjectableX on _i1.GetIt {
// initializes the registration of main-scope dependencies inside of GetIt
  _i1.GetIt init({
    String? environment,
    _i2.EnvironmentFilter? environmentFilter,
  }) {
    _i2.GetItHelper(
      this,
      environment,
      environmentFilter,
    );
    return this;
  }
}

dependency.dart 파일에 @singleton, @injectable 등으로 마킹해둔 클래스들이 dependency.config.dart 에 정의됨
이후 locator.init() 을 호출하면, dependency.config.dart 안의 로직이 실행되면서 Get_It 에 모든 객체가 담김

순서 흐름
1. dependency.dart 파일 생성
2. 의존성 부분들에 @Annotation 선언
2. build_runner 실행
3. 자동으로 @Annotation 선언된 부분들에 대한 코드 생성
3. init() 함수 호출하여 get_it 에 자동 주입

ㅡㅡㅡㅡㅡㅡㅡㅡㅡ 변경 전 ㅡㅡㅡㅡㅡㅡㅡㅡㅡ
void main() {
  locator.registerSingleton<CounterModel>(CounterModel());
  locator.registerSingleton<CounterModeModel>(CounterModeModel());
  runApp(const CounterApp());
}

// model.dart
class CounterModel {
  ...
}
class CounterModeModel {
  ...
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡ 변경 후 ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
void main() {
  configureDependencies();
  runApp(const CounterApp());
}

// model.dart
@singleton
class CounterModel {
  ...
}
@singleton
class CounterModeModel {
  ...
}

provider 이론

InheritedWidget 을 더 쉽게 사용할 수 있게 해주는 wrapper
위젯트리 전반에 걸쳐 데이터와 객체를 전달하고 관리해주는 상태 관리 라이브러리

// listen: 등록된 객체의 값이 변경될때마다 해당 위젯 재빌드 여부
Provider.of<Axe>(context, listen: true);

// Provider.of 에서 listen: false 와 동일
context.read<Axe>()

// Provider.of 에서 listen: true 와 동일
context.watch<Axe>()

아래와 같은 방식으로 상위 위젯에 등록 후 하위 위젯들이 사용

void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider(create: (context) => CounterModel()),
        Provider(create: (context) => CounterModeModel()),
      ],
      child: const CounterApp(),
    ),
  );
}

의존성 주입 라이브러리가 필요한 이유
화면을 만들때마다

ViewModel(FetchUseCase(Repository(DataSource(LocalDB())))

이런식으로 코드를 가져오는게 아닌

context.read<ViewModel>()

한줄이면 끝


아키텍처 학습

여러 기능과 상태를 어떻게 구성하고 관리할지

MVC 이론

Model
데이터와 비즈니스 로직을 관리하는 부분

class Model {
	int _count = 0;
    int get count => _count;
    
    void increment() {
  		_count++;
    }
}

view
사용자 인터페이스 부분

class _ViewState extends State<View>{
	...
    
    @override
    Widget build(BuildContext context) {
    	return Scaffold(
        	...
        )
    }
}

controller
Model 과 View 사이의 상호작용 관리

class Controller {
	final Model _model;
    
    Controller(this._model);
    
    void increment(){
    	_model.increment();
    }
}

위 코드를 통해 알게된 점은 class 에 변수 선언된 경우 get 으로 가져오는게 한 세트이며, 변수 선언할 때 어떤 값이 들어가야 할지 정의된 경우 enum 사용
controller 는 model 의 데이터와 로직을 호출하여 동작하므로, 두 계층 간의 결합이 발생한다

MVVM 이론

주로 UI 가 많은 애플리케이션 개발에 적용되는 패턴
MV 까지는 MVC 와 동일

viewModel
view 와 model 의 중재자 역할
MVC 에서 model 은 변수, 비즈니스 로직을 담당
viewModel 은 변수, 비즈니스 로직 + 상태( 화면에 반영 ) 담당

MVC 와 MVVM 의 가장 큰 차이

MVC 패턴: View 가 중심이 되어 Controller 의 메서드를 호출하고, 그 결과를 setState() 를 통해 직접 UI에 반영. 즉, View 가 상태 변화와 화면 갱신을 직접 트리거해야 하므로 주로 StatefulWidget 구조를 가짐

MVVM 패턴: ValueNotifier 와 ValueListenableBuilder 가 데이터 바인딩 역할을 수행. ViewModel 이 변수, 상태, 비즈니스 로직을 전담하며, View 는 이를 관찰할 뿐 직접적인 화면 반영 로직(setState)을 갖지 않음. 결과적으로 View 는 로직으로부터 자유로운 StatelessWidget 중심의 선언적 구조

ValueNotifier: 데이터를 보관하고 변하는지 감시
ValueListenableBuilder: 해당하는 ValueNotifier 값이 변경되면 해당 부분만 다시 그림

ValueListenableBuilder<int>(
   valueListenable: counterViewModel.counter,
   
   builder: (context, counter, child) => Text(
      counter.toString(),
      style: Theme.of(context).textTheme.headlineMedium,
   ),
),

클린 아키텍처

profile
안녕하세요

0개의 댓글