Flutter 개발을 시작하며 공부했던 Clean Architecture에 관한 내용이다.
혼자 공부하면서 메모하듯이 적은 내용에 가깝다.
Model | ||
Data Layer | Domain Layer | Presentation Layer |
Data Source Repository (Implements) |
UseCase Repository (Interface) Entity (Model Class) |
View View Model |
(이미지를 불펌하기는 좀 그렇고 계층 구분이라도 한눈에 보기 쉬우라고 표로 구역 나눠봤다.)
1. Usecases & Repository & DataSource
Usecases
UseCases는 애플리케이션의 비즈니스 로직을 캡슐화합니다. UseCase는 Domain 레이어에서 정의됩니다. 뷰모델은 UseCase를 호출하고 UseCase는 Repository를 사용하여 데이터를 가져옵니다. 이를 통해 뷰모델은 비즈니스 로직과 데이터 액세스를 분리할 수 있습니다.
Repository
Repository는 데이터를 가져오거나 저장하는 데 사용됩니다. Repository는 다른 데이터 원본, 예를 들어 로컬 데이터베이스, 원격 데이터베이스 또는 API와 상호 작용할 수 있습니다. Repository는 데이터 액세스에 대한 인터페이스를 제공하며, 실제 데이터 원본에 대한 구현 세부 정보를 숨깁니다.
DataSource
DataSource는 Repository에서 데이터를 가져오는 데 사용되는 실제 데이터 원본입니다. DataSource는 데이터를 검색하고, 필터링하고, 정렬하는 등의 일을 수행합니다. Repository는 다양한 DataSource를 가질 수 있으며, 이는 애플리케이션의 성능과 확장성에 큰 영향을 미칩니다.
→ Usecase는, 특정한 하나의 기능만을 담당한다. 예를 들면 이미지 전송, 로드, 삭제 각각의 Usecase를 만들어 뷰모델에서는 Usecase 호출만 하면 된다.
Usecase의 각 기능들은 또 Repository의 함수를 호출한다.
Repository는 다양한 기능의 함수들이 모여있다. 이것 역시 Data Source의 함수를 호출한다.
마지막으로 Date Source에서는 직접적으로 API나 파이어베이스에 접근하는 메서드를 작성하면 된다
ViewModel {
CreateUsecase
LoadUsecase
DeleteUsecase
}
CreateUsecase {
repository.create
}
LoadUsecase {
repository.load
}
DeleteUsecase {
repository.delete
}
Repository {
datasource.create
datasource.load
datasource.delete
}
DataSource {
create
load
delete
} // 실제 데이터 통신
이런 형태가 된다는 것 같다.
2. 근데..레포지토리 단계는 굳이 필요한가?
데이터 소스의 메소드들을 레포지토리에 넣으면 안돼?
추상화와 모듈성: 데이터 원본에 대한 구현 세부정보를 숨기면, 다른 부분에서 이를 참조할 때 추상화된 인터페이스만 사용하면 됩니다. 이는 코드의 모듈성을 높여줍니다. 또한, 추상화된 인터페이스를 이용해 다른 데이터 원본으로의 교체가 용이해집니다.
보안: 실제 데이터 원본에 대한 구현 세부정보를 외부에 노출시키지 않으면, 보안상의 이슈가 줄어듭니다.
단일책임의 원칙: Repository는 데이터 원본에서 데이터를 가져오는 역할만 담당합니다. 따라서, 이를 구현하는 세부적인 부분은 Repository의 책임이 아닙니다. 이를 구현하는 세부적인 부분은 DataSource에서 처리하게 되므로, 코드의 단일책임의 원칙을 지킬 수 있습니다.
추상 클래스는 굳이 왜 써야하지?
추상 클래스를 사용하는 이유 중 하나는 다형성을 구현하기 위해서입니다. 즉, 여러 클래스가 동일한 기능을 수행하지만 세부적인 구현 방법이 다를 때, 추상 클래스를 상속하여 기능을 구현하면 코드의 재사용성과 유지보수성을 높일 수 있습니다.
예를 들어, 여러 데이터 소스가 있을 때 (예: 로컬 데이터베이스, 원격 서버, 캐시 등), 이 데이터 소스들은 같은 데이터를 다루지만, 각각의 데이터 소스는 그 데이터를 다루는 방식이 조금씩 다릅니다. 추상 클래스를 사용하여 이러한 데이터 소스들이 동일한 메서드를 구현하도록 강제하고, 실제 데이터 소스 클래스에서 추상 클래스를 상속하여 구체적인 기능을 구현하면 코드의 일관성과 가독성을 유지할 수 있습니다.
→ FirebaseDataSource, LocalServerDataSource 같은 걸 만들어도 같은 형식의 클래스로 함수 모양만 달라지게 해서 사용할 수 있음.
= 테스트가 용이해진다.
3. Provider의 역할과 사용법
3-a. Ephemeral state
Ephemeral state는 임시적인 상태라는 뜻이다. Local state와 의미가 같다. Local state는 조금 더 이해하기가 쉽다. 한 개의 위젯이 가지는 상태를 Ephemeral state라고 한다. 가령,
- 현재 PageView
- 현재 진행 중인 애니메이션
- BottomNavigationBard의 현재 탭
을 일컫는다. 이러한 경우에는 상태 관리법을 사용하는 게 필요 없다. 조금 더 단순하게 생각을 하자면, 현재 페이지에서만 사용하는 상태이며, 다른 곳에서는 영향을 주지 않는 경우(Local)를 말한다. 그래서 이러한 경우에는 상태 관리가 크게 필요하지 않다.
하지만 앱 전체에 영향을 주는 경우에는 상태 관리를 해야 한다는 뜻이다.
3-b. App state
App state는 어떠한 상태가 앱 전체에 영향을 주는 경우를 말한다. 예를 들어,
- 로그인 정보
- 유저의 설정
- 쿠팡의 장바구니
- 인스타그램 혹은 페이스북의 알림
- 네이버 혹은 구글의 메일 읽음과 읽지 않음 처리
→ 여기서 Provider로 3-b 상태를 효과적으로 관리할 수 있다.
3-c. Provider 사용법
같은 스크린단에서 watch를 쓰게 되면, 자기가 보고 있는 value가 아닌 다른 value가 업데이트 되어도 같이 rebuild 된다.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// This is a reimplementation of the default Flutter application using provider + [ChangeNotifier].
void main() {
runApp(
/// Providers are above [MyApp] instead of inside it, so that tests
/// can use [MyApp] while mocking the providers
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
],
child: const MyApp(),
),
);
}
/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool
// ignore: prefer_mixin
class Counter with ChangeNotifier, DiagnosticableTreeMixin {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
/// Makes `Counter` readable inside the devtools by listing all of its properties
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('count', count));
}
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
/// Extracted as a separate widget for performance optimization.
/// As a separate widget, it will rebuild independently from [MyHomePage].
///
/// This is totally optional (and rarely needed).
/// Similarly, we could also use [Consumer] or [Selector].
Count(),
],
),
),
floatingActionButton: FloatingActionButton(
key: const Key('increment_floatingActionButton'),
/// Calls `context.read` instead of `context.watch` so that it does not rebuild
/// when [Counter] changes.
onPressed: () => context.read<Counter>().increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class Count extends StatelessWidget {
const Count({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
/// Calls `context.watch` to make [Count] rebuild when [Counter] changes.
'${context.watch<Counter>().count}',
key: const Key('counterState'),
style: Theme.of(context).textTheme.headlineMedium,
);
}
}
<도움 받은 링크들>
- Flutter MVVM Architecture
- MVVM Pattern in Flutter 2 - Domain, Repository, DataSource
- provider 6.1.2 Package Example
- 8. Dart, Flutter 상태관리 그리고 Riverpod (1)
'개발노트 > Flutter' 카테고리의 다른 글
Flutter 특정 버전으로 업데이트 하기 (0) | 2025.01.16 |
---|---|
Flutter에서 iOS TableView Section Header 같은 UI 구현하기 (0) | 2024.11.30 |
Flutter에서 Firebase Authentication으로 이메일 로그인 구현하기 (2) | 2024.11.24 |
Flutter에서 로컬 DB 구현하기 (SQFlite) (2) | 2024.11.23 |
Flutter에서 FCM 설정하기 (1) | 2024.11.21 |