클린 아키텍처는 소프트웨어 엔지니어 로버트 C. 마틴(Robert C. Martin)이 제안한 설계 원칙을 따릅니다.
이 아키텍처는 계층화된 구조를 사용하여 확장성과 테스트 용이성을 제공합니다.
이번 글에서는 프로젝트의 규모가 커짐에 따라 발생하는 여러 문제를 해결하기 위한 6단계를 소개하겠습니다.
이 과정에서 클린 아키텍처가 어떻게 적용되었는지를 설명드리겠습니다.
이 내용은 Flutter 뿐만 아니라 다른 앱 개발에도 적용할 수 있습니다.
1단계: 첫 번째 아키텍처
가장 처음에는 View에서 직접 서버로 데이터를 요청하여 화면을 구성하는 간단한 구조였습니다.
class PostListViewState extends State<PostListView> {
List<Post> _posts = [];
// 1. 데이터를 받아와 저장합니다.
void fetchPosts() async {
final posts = await httpClient.getPostList();
setState(() {
_posts = posts;
});
}
// 2. 화면을 표시합니다.
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return PostCell(_posts[index]);
},
);
}
}
문제 발생
이 구조에서는 데이터 처리와 UI 구성이 하나의 클래스에 모두 포함되어 있어 코드 이해와 수정이 어렵습니다.
코드 수정 시마다 클래스 전체를 테스트해야 하므로 효율성이 떨어집니다.
2단계: 화면과 데이터 분리
데이터와 UI를 분리하여 View와 ViewModel로 나누었습니다.
아래와 같이 Provider를 사용해 ViewModel을 구현했습니다.
// View
class PostListView extends StatelessWidget {
const PostListView({super.key});
Widget build(BuildContext context) {
final posts = context.select((PostListViewModel viewModel) => viewModel.posts);
return ListView.builder(
itemBuilder: (context, index) {
return PostCell(posts[index]);
},
);
}
}
// ViewModel
class PostListViewModel extends ChangeNotifier {
List<Post> posts = [];
void fetchPosts() async {
posts = await httpClient.getPostList();
notifyListeners();
}
}
문제 발생
즐겨찾기 기능을 추가하자 코드 중복과 동기화 문제가 발생했습니다.
각각의 화면에서 중복된 코드를 수정해야 하므로 비효율적입니다.
3단계: Repository 추가
중복 코드를 줄이기 위해 데이터 중앙 관리를 위한 Repository를 도입했습니다.
class PostRepository {
// 글 목록 데이터 요청
Stream<List<Post>> getPostList() { ... }
// 글 데이터 요청
Stream<Post> getPost(String postId) { ... }
// 즐겨찾기 추가
void setFavorite(String postId) { ... }
}
프레젠테이션 레이어는 Repository에서 데이터를 요청하고 변경사항을 통지받아 UI를 업데이트합니다.
4단계: Repository 인터페이스와 구현체 분리
Repository를 인터페이스와 구현체로 나누어 테스트 용이성과 의존성 관리를 개선했습니다.
// interface
abstract class PostRepository {
Stream<List<Post>> getPostList();
Stream<Post> getPost(String postId);
void setFavorite(String postId);
}
// 구현체
class PostRepositoryImpl implements PostRepository {
@override
Stream<Post> getPost(String postId) { ... }
@override
Stream<List<Post>> getPostList() { ... }
@override
void setFavorite(String postId) { ... }
}
5단계: 모델 분리
서버 데이터와 내부 데이터 모델을 분리하여 프레젠테이션 레이어의 독립성을 높였습니다.
이 구조로 인해 서버의 변경 사항에 의한 영향을 최소화했습니다.
6단계: UseCase 추가
ViewModel이 Repository를 직접 참조하지 않고 UseCase를 사용함으로써 의존성 및 관리가 쉬워졌습니다.
class LoginUseCase {
Future<UserProfile> call(Params params) {
return auth.login(params);
}
}
이 구조는 코드 중복을 줄이고 재사용성을 높였습니다. UseCase를 통해 여러 Repository의 메서드를 순차적으로 호출할 수 있습니다.
최종 구조
최종 구조는 다음과 같습니다.
lib/
├── main.dart
├── core/
│ ├── error/
│ └── utils/
│ ├── usecases/
│ └── widgets/
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ ├── models/
│ │ │ ├── repositories/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ ├── repositories/
│ │ │ ├── usecases/
│ │ ├── presentation/
│ │ │ ├── pages/
│ │ │ └── widgets/
│ ├── post/
│ ├── video/
클린 아키텍처는 모든 문제를 해결하는 만능키는 아닙니다.
작은 프로젝트에서는 오히려 복잡성을 증가시킬 수 있지만, 대규모 프로젝트에서는 비즈니스 로직과 데이터 접근을 명확히 분리할 수 있어 각 모듈을 독립적으로 변경하고 테스트를 쉽게 할 수 있습니다.
앞으로도 이 구조를 활용해 프로젝트의 품질을 유지하고 변화에 잘 대응할 수 있을 것이라고 기대합니다.
구독!! 공감과 댓글,
광고 클릭은 저에게 큰 힘이 됩니다.
Starting Google Play App Distribution! "Tester Share" for Recruiting 20 Testers for a Closed Test.
'Flutter' 카테고리의 다른 글
플러터에서 위젯을 분리 하는 방법 (1) | 2024.11.11 |
---|---|
Flutter에서 Optimistic Response Cache 완벽 가이드 (2) | 2024.11.07 |
플러터 개발을 위한 필수 영단어 학습 (7) | 2024.10.17 |
플러터 탄생 배경과 활용 분야 - 초급자를 위한 쉬운 설명 (4) | 2024.09.21 |
플러터 DevTools를 이용한 메모리 관리: 초보자를 위한 가이드 (1) | 2024.09.09 |