본문 바로가기
Flutter

플러터 클린 아키텍처: 작은 앱에서 큰 프로젝트까지의 맞춤 설계

by Maccrey Coding 2024. 11. 4.
728x90
반응형

 

클린 아키텍처는 소프트웨어 엔지니어 로버트 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.

 

Tester Share [테스터쉐어] - Google Play 앱

Tester Share로 Google Play 앱 등록을 단순화하세요.

play.google.com

 

 

728x90
반응형