Flutter Calculator 예제로 정리한 Clean Architecture 3계층 구조 (Presentation → Domain → Data) + 의존성 주입(DI) 노트

전체 구조 한눈에 보기

User ↔ View ↔ Presenter(Presentation) ↔ UseCase ↔ Repository(Domain) ↔ RepositoryImpl ↔ DataSource(Data)
계층 핵심 요소 역할
Presentation View, Presenter(ViewModel) 사용자 입력을 받아 UseCase 호출, 상태 갱신
Domain Entity, Repository(interface), UseCase 비즈니스 로직, 다른 계층에 의존하지 않는 순수 계층
Data Model, RepositoryImpl, DataSource 실제 데이터 저장/조회 (로컬, 서버 등)

의존성 방향은 항상 바깥 → 안쪽 (Presentation → Domain ← Data) 으로 향하며, Domain 계층은 다른 계층을 몰라야 하는 것이 핵심.


1. Presentation Layer

폴더 구조

presentation/
└─ view_model/
   ├─ calculator.dart
   └─ view_model.dart
└─ presentation.dart

최초 구현 (Data 계층 직접 참조 - 문제점)

class CalculatorViewModel extends ValueNotifier<Calculator> {
  CalculatorViewModel(super.calculator);

  Future<void> load() async {
    final String result = await CalculatorDataSource(CalculatorLocalDataSource()).fetch();
    value = Calculator(result: result);
  }

  Future<void> save() async {
    await CalculatorDataSource(CalculatorLocalDataSource()).save(value.result);
  }

  void calculate(String buttonText) {
    value.calculate(buttonText);
    notifyListeners();
  }
}

⚠️ 문제: Presenter(ViewModel)가 Data 계층(CalculatorDataSource)을 직접 참조 → 클린 아키텍처 위반 (Presentation이 Domain을 거치지 않고 Data에 직접 의존)

개선: UseCase를 거치도록 수정

class FetchCalculatorUseCase {
  Future<Calculator> execute() async {
    final String value = await CalculatorDataSource(CalculatorLocalDataSource()).fetch();
    return Calculator(result: value);
  }
}

class SaveCalculatorUseCase {
  Future<void> execute(Calculator value) {
    return CalculatorDataSource(CalculatorLocalDataSource()).save(value.result);
  }
}
class CalculatorViewModel extends ValueNotifier<Calculator> {
  final FetchCalculatorUseCase _fetchCalculatorUseCase;
  final SaveCalculatorUseCase _saveCalculatorUseCase;

  CalculatorViewModel(
    this._fetchCalculatorUseCase,
    this._saveCalculatorUseCase,
    super.calculator,
  );

  Future<void> load() async { /* ... */ }
  Future<void> save() async { /* ... */ }
  void calculate(String buttonText) { /* ... */ }
}

✅ ViewModel(Presenter)이 UseCase에만 의존하도록 변경 → Presentation ↔ UseCase 관계 확립


2. Data Layer

폴더 구조

data/
├─ model/
│  ├─ calculator.dart
│  └─ model.dart
├─ repository/
│  ├─ calculator.dart
│  └─ repository.dart
├─ source/
│  ├─ local/
│  │  ├─ calculator.dart
│  │  └─ local.dart
│  ├─ calculator.dart
│  └─ source.dart
└─ data.dart