앞서 배웠던 내용으로 Flutter에 Clean architecture를 적용해 보도록 하자.
드디어 Layer별 어떤 폴더들을 가지게되는지 설명하게 되었다.
앞서 설명한 OOP(객체 지향 프로그래밍)과 SOLID원칙을 기초로 하여 구현을 하는 게 목표이다. 하나하나씩 살펴보자.
Domain Layer
Domain Layer는 애플리케이션의 비즈니스 로직을 담고 있는 계층이다. 이 계층은 프레임워크에 독립적이며, 순수한 Dart 코드로 작성되며, 테스트가 용이하고, 다른 기술 스택으로 교체할 때도 영향을 최소화하도록 한다.
- Entities
- Use Cases
- Repositories(Interfaces)
Domain Layer에서는 위 3가지 요소들이 포함이 되어있다.
하나하나씩 살펴보도록 하자.
Entities
Entities는 애플리케이션의 주요 비즈니스 객체를 표현한다.
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
Use Cases
유즈 케이스는 특정 비즈니스 요구사항을 수행하는 단위 작업을 정의하며, 각 유즈 케이스는 하나의 메서드만을 가져야 하며, 비즈니스 로직을 수행한다.
class GetUserDetails {
final UserRepository repository;
GetUserDetails(this.repository);
Future<User> excute(int userId) {
return repository.getUserById(userId);
}
}
Repositories(Interfaces)
Repositories는 데이터 소스와 상호작용하는 인터페이스를 정의한다. 리포지토리 구현체가 Domain Layer에 종속되지 않도록 해야 한다.
abstract class UserRepository {
Future<User> getUserById(int id);
Future<List<User>> getAllUsers();
}
Data Layer
데이터 소스와 레포지토리 구현체를 포함한다. 예를 들어, API 호출이나 로컬 데이터베이스와의 상호작용을 담당하게 된다.
- Models
- Repositories
- Data Sources
Data Layer에서는 위 3가지로 이루어져 있다. 여기서 Domain Layer에서 Repositories가 중복된다고 생각할 수 있지만, Data Layer에서 Repositories는 Domain Layer의 Repositories에서 정의된 것을 구현하고 실제 데이터 소스와 상호작용하는 곳이다.
Models
엔터티를 표현하는 데이터 구조체로, 일반적으로 JSON 등의 데이터를 파싱하고 변환하는 역할을 한다.
class UserModel {
final int id;
final String name;
final String email;
UserModel({required this.id, required this.name, required this.email});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
Repositories
Domain Layer에서 정의된 Repositories interface를 구현하여 실제 데이터소스와 상호작용한다.
mport 'package:dartz/dartz.dart';
import 'package:flutter_clean_architecture/core/error/failures.dart';
import 'package:flutter_clean_architecture/features/user/domain/entities/user.dart';
import 'package:flutter_clean_architecture/features/user/domain/repositories/user_repository.dart';
import 'package:flutter_clean_architecture/features/user/data/datasources/user_remote_datasource.dart';
import 'package:flutter_clean_architecture/features/user/data/datasources/user_local_datasource.dart';
import 'package:flutter_clean_architecture/features/user/data/models/user_model.dart';
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
final UserLocalDataSource localDataSource;
UserRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
});
@override
Future<Either<Failure, User>> getUserById(int id) async {
try {
final remoteUser = await remoteDataSource.getUserById(id);
localDataSource.cacheUser(remoteUser);
return Right(remoteUser);
} on Exception {
try {
final localUser = await localDataSource.getUserById(id);
return Right(localUser);
} on Exception {
return Left(ServerFailure());
}
}
}
@override
Future<Either<Failure, List<User>>> getAllUsers() async {
try {
final remoteUsers = await remoteDataSource.getAllUsers();
localDataSource.cacheUsers(remoteUsers);
return Right(remoteUsers);
} on Exception {
try {
final localUsers = await localDataSource.getAllUsers();
return Right(localUsers);
} on Exception {
return Left(ServerFailure());
}
}
}
}
Data Sources
실제 데이터소스와 상호작용을 담당하며, API호출하는 Remote부분과 Local부분이 따로 정의된다.
abstract class UserRemoteDataSource {
Future<UserModel> getUserById(int id);
Future<List<UserModel>> getAllUsers();
}
class UserRemoteDataSourceImpl implements UserRemoteDataSource {
final http.Client client;
UserRemoteDataSourceImpl({required this.client});
@override
Future<UserModel> getUserById(int id) async {
final response = await client.get(Uri.parse('https://api.example.com/users/$id'));
if (response.statusCode == 200) {
return UserModel.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to load user');
}
}
@override
Future<List<UserModel>> getAllUsers() async {
final response = await client.get(Uri.parse('https://api.example.com/users'));
if (response.statusCode == 200) {
Iterable jsonResponse = json.decode(response.body);
return List<UserModel>.from(jsonResponse.map((model) => UserModel.fromJson(model)));
} else {
throw Exception('Failed to load users');
}
}
}
------------------------------
abstract class UserLocalDataSource {
Future<UserModel> getUserById(int id);
Future<List<UserModel>> getAllUsers();
Future<void> cacheUser(UserModel user);
Future<void> cacheUsers(List<UserModel> users);
}
class UserLocalDataSourceImpl implements UserLocalDataSource {
final Database database;
UserLocalDataSourceImpl({required this.database});
@override
Future<UserModel> getUserById(int id) async {
final List<Map<String, dynamic>> maps = await database.query(
'users',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isNotEmpty) {
return UserModel.fromJson(maps.first);
} else {
throw Exception('No user found');
}
}
@override
Future<List<UserModel>> getAllUsers() async {
final List<Map<String, dynamic>> maps = await database.query('users');
return List<UserModel>.from(maps.map((map) => UserModel.fromJson(map)));
}
@override
Future<void> cacheUser(UserModel user) async {
await database.insert(
'users',
user.toJson(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
@override
Future<void> cacheUsers(List<UserModel> users) async {
for (var user in users) {
await cacheUser(user);
}
}
}
Presentation Layer
UI 관련 코드와 상태 관리를 포함한다. Bloc, Riverpod, Provider, GetX 등 상태 관리 라이브러리를 사용하여 UI와 비즈니스 로직을 연결한다. 여기서 MVC, MVVM, MVP 등 디자인패턴을 적용하는 곳이다.
- Providers
- Pages
- Widgets
Presentation Layer에서는 위 세 가지로 나눌 수 있다. 물론 MVC, MVVM 등 다른 디자인패턴을 적용하거나 어떤 상태관리를 쓰냐에 따라 구조가 달라질 수 있다. 그래서 여기서는 riverpod을 적용했다는 가정하에 작성하도록 하겠다.
Providers
상태 관리 및 의존성 주입을 위한 프로바이더들을 정의한다. 어떤 상태관리 및 디자인패턴을 적용했냐에 따라 폴더명이 달라질 수 있고 코드도 달라질 수 있다.
// lib/features/user/presentation/providers/user_providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_clean_architecture/features/user/domain/entities/user.dart';
import 'package:flutter_clean_architecture/features/user/domain/usecases/get_user_details.dart';
import 'package:flutter_clean_architecture/core/error/failures.dart';
import 'package:flutter_clean_architecture/features/user/presentation/state/user_state.dart';
import 'package:flutter_clean_architecture/features/user/data/models/user_model.dart';
part 'user_providers.g.dart';
@riverpod
class UserProvider extends _$UserProvider {
@override
AsyncValue<User> build() => const AsyncData(User(id: 0, name: '', email: ''));
Future<void> loadUser(int userId) async {
if (state.isLoading) return;
state = const AsyncLoading();
try {
final Either<Failure, User> result = await GetUserDetails().execute(userId);
result.fold(
(failure) => state = AsyncError(failure, StackTrace.current),
(user) => state = AsyncData(user),
);
} catch (error) {
state = AsyncError(
CustomErrorModel.errorModel(
errorCode: '700',
errorDescription: 'User provider error $error',
errorMessage: '사용자 정보를 불러오는데 실패했습니다.',
),
StackTrace.current,
);
}
}
}
Pages
주요 화면(페이지) 컴포넌트들을 정의하며, 각 페이지는 일반적으로 앱 내의 주요 내비게이션 단위이다.
// lib/features/user/presentation/pages/user_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_clean_architecture/features/user/presentation/providers/user_providers.dart';
import 'package:flutter_clean_architecture/features/user/presentation/widgets/user_details.dart';
class UserPage extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final userState = watch(userStateNotifierProvider);
return Scaffold(
appBar: AppBar(
title: Text('User Details'),
),
body: userState.isLoading
? Center(child: CircularProgressIndicator())
: userState.error != null
? Center(child: Text(userState.error!))
: userState.user != null
? UserDetails(user: userState.user!)
: Center(child: Text('No user loaded')),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read(userStateNotifierProvider.notifier).loadUser(1);
},
child: Icon(Icons.refresh),
),
);
}
}
// lib/features/user/presentation/widgets/user_details.dart
import 'package:flutter/material.dart';
import 'package:flutter_clean_architecture/features/user/domain/entities/user.dart';
class UserDetails extends StatelessWidget {
final User user;
UserDetails({required this.user});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text('ID: ${user.id}'),
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
],
),
);
}
}
Widgets
재사용 가능한 UI 위젯들을 정의한다. 이러한 위젯들은 여러 페이지에서 재사용될 수 있고, UI 구성 요소를 모듈화 하는데 용이하다.
// lib/features/user/presentation/widgets/user_details.dart
import 'package:flutter/material.dart';
import 'package:flutter_clean_architecture/features/user/domain/entities/user.dart';
class UserDetails extends StatelessWidget {
final User user;
UserDetails({required this.user});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text('ID: ${user.id}'),
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
],
),
);
}
}
폴더구조
lib/
├── features/
│ ├── user/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── user.dart
│ │ │ ├── usecases/
│ │ │ │ └── get_user_details.dart
│ │ │ └── repositories/
│ │ │ └── user_repository.dart
│ │ ├── data/
│ │ │ ├── models/
│ │ │ │ └── user_model.dart
│ │ │ ├── repositories/
│ │ │ │ └── user_repository_impl.dart
│ │ │ └── datasources/
│ │ │ ├── user_remote_datasource.dart
│ │ │ └── user_local_datasource.dart
│ │ └── presentation/
│ │ ├── providers/
│ │ │ └── user_providers.dart
│ │ ├── pages/
│ │ │ └── user_page.dart
│ │ ├── widgets/
│ │ │ └── user_details.dart
└── core/
├── error/
├── network/
└── util/
이렇게 폴더구조까지 작성하면 Clean Architecture를 적용할 수 있다.
개인적인 견해
개인적으로 클린아키텍처는 무조건 좋다. 100% 좋다.라고 생각하지는 않는다. 우리가 클린아키텍처를 적용하는 이유는 "장기적인 유지보수 및 확장성" 때문이다.
하지만 우리는 개발을 하면 "장기적인 유지보수 및 확장성" 보다 더 우선시 되는 것들이 많다. 예를 들면 단기적인 효율성 및 생산성을 중시하여 빠른 개발을 원하는 경우에는 반복되는 코드들과 boiler plate들이 생성이 되어 오히려 걸림돌이 되는 경우가 발생할 수 있다. 그래서 클린아키텍처를 무조건 적용하는 것보다 그에 맞는 상황에 따라 적용하는 것이 가장 중요할 것이다.
'Study > 기타' 카테고리의 다른 글
Clean Architecture 클린아키텍처, 객체지향 디자인 5원칙 SOLID[5부] (1) | 2024.03.23 |
---|---|
Clean Architecture 클린아키텍처, 객체와객체지향 프로그래밍[4부] (0) | 2024.03.20 |
Clean Architecture 클린아키텍처, Flutter 폴더구조에 대하여[3부] (0) | 2024.02.25 |
Clean Architecture 클린아키텍처, Domain에 대하여 [2부] (0) | 2024.01.09 |
Clean Architecture 클린아키텍처에 대하여 [1부] (0) | 2024.01.08 |