Flutter

클린 아키텍처 심화

hamiric 2024. 12. 19. 11:28

< 클린 아키텍쳐 >

클린 아키텍쳐에서는 이와같은 레이어(모듈)를 구상하는것을 추천한다.

  • Presentation Layer(UI)
  • Domain Layer
  • Data Layer

각각의 레이어(모듈)에 들어갈 파일들의 특징을 살펴보자면 다음과 같다.

features/            # 각 기능별 모듈 (Todo와 같은)
│   ├── todo/            # Todo 기능 (예시)
│   │   ├── data/        # 데이터 레이어
│   │   │   ├── models/  # API 응답 모델, DB 모델
│   │   │   ├── sources/ # 데이터 소스 (API, DB)
│   │   │   └── repositories/ # 실제 Repository 구현체들
│   │   ├── domain/      # 도메인 레이어
│   │   │   ├── entities/    # 도메인 엔티티
│   │   │   ├── repositories/ # 추상화된 Repository 인터페이스
│   │   │   └── usecases/    # 비즈니스 로직을 담당하는 UseCase
│   │   └── presentation/ # UI 레이어 (MVVM)
│   │       ├── viewmodels/   # ViewModel
│   │       ├── views/        # UI (화면)
│   │       └── widgets/      # UI 위젯들

 

각 폴더에 들어가는 코드들의 종류는 다음과 같다!

 

[ Data ]

데이터 레이어는 외부에서 들어오는 데이터를 다루는 레이어로써

데이터 소스(예: 데이터베이스, API)와 관련된 작업을 처리하는 계층이다.

 

  • Models

Models 폴더는

API 응답을 파싱하거나 데이터베이스의 스키마를 표현하는 모델 클래스들을 포함하는 모델들이 모여있는 폴더 (DTO)
데이터 레이어에서 사용할 수 있도록 데이터를 표준 형식으로 변환하는 역할을 한다.

< 예시 >
API를 통해 들어오는 데이터를 받아오기 위한 형식 구현

class ExamDto {
    String data;
    DateTime time;
    List<String> list;

    ExamDto(required this.data, required this.time, required this.list);

    ExamDto.fromJson(Map<String, dynamic> json) : this(
        data: json['data'],
        time: DateTime.parse(json['time']),
        list: (json['list'] as List).map((item) => item as String,).toList(),
    );

    Map<String,dynamic> toJson(){
        return {
            'data': data,
            'time': time.toIso8601String(),
            'list': list,
        };
    }
}

 

 

  • Repositories

Repositories 폴더는

sources의 구체적 구현체를 조합하여 데이터를 제공하는 리포지토리 클래스들이 포함된다.
데이터 소스(API, DB 등) 간의 논리를 조율하며, 상위 도메인 레이어에 제공할 준비된 데이터를 반환한다.

< 예시 >
Firestore 소스에서 데이터를 가져와 사용할수 있는 데이터로 변환하고, 범용적으로 사용될 수 있는 로직을 처리

class ExampleRepository implements ExampleImplements {

    final ExampleSource exampleSource;

    ExampleRepository({required this.exampleSource});

    // 읽어오기
    @override
    Future<List<examDto>> getExample(String id) async {
        final snapshot = await exampleSource.get(id);
        final docs = snapshot.docs();

        return docs.map((e){
            return ExamDto.fromJson(e.data()).toList();
        });
    }

    .. 등등
}

 

 

  • Sources

Sources 폴더는

데이터의 실제 소스를 다루는 모듈로, 외부 API 호출, 데이터베이스 작업, 로컬 스토리지 접근 등을 처리한다.
API 통신 및 데이터 CRUD 작업의 세부 구현이 주로 포함된다.

< 예시 >
파이어베이스와의 직접적인 상호작용 부분을 담당 ( 컬렉션에서 데이터를 읽거나 쓰는 작업만 수행 )

class ExampleSource {
    
    final FirebaseFirestroe firestore;

    ExampleSource(FirebaseFirestroe firestore) : firestore = FirebaseFirestore.instance;

    // 읽어오기
    Future<QuerySnapshot> get(String id) async {
        CollectionReference collectionRef = firestore.collection(id);
        return await collectionRef.get();
    }

    .. 등등
}

 

 

[ Domain ]

  • Entities

Entities 폴더는

도메인 레이어에서 사용하는 순수한 비즈니스 로직 모델(도메인 엔티티)을 정의한다.
데이터 레이어의 모델(models/)과는 분리되어, 외부 데이터 소스와 독립적인 상태를 유지한다.

내부 데이터에서 사용되는 모델들의 객체를 선언하는 장소
즉, 앱 내부에서만 사용되는 데이터 객체들이 있는곳

(ex 내부 DB용 모델, API 에서 받아온 데이터중 사용할것만 따로 떼어낸것)

< 예시 >
ExamDto 에서 list값만 사용하기 위한 새로운 모델 만들기

class ExamModel {
    List<String> list;

    ExamModel(required this.list);

    ExamModel.fromdata(ExamDto dto) : this(
        list: dto.list,
    );
}

 

 

  • Repositories

Repositories 폴더는

데이터를 가져오기 위한 추상화된 인터페이스를 정의한다.
도메인 레이어와 데이터 레이어를 분리하기 위해 사용된다.

< 예시 >
UseCase에서 사용될 인터페이스 작성

abstract class ExampleImplements {

    Future<List<ExamModel>> getExample(String id);

    .. 등등
}

 

 

  • UseCase

UseCase 폴더는

특정 기능에 대한 비즈니스 로직을 처리하는 유스케이스 클래스들이 포함된다.
리포지토리를 호출하고 도메인 로직을 구현하며, 프레젠테이션 레이어에 데이터를 전달한다.

< 예시 >
data의 레포지토리를 호출하여 실제 사용될 비지니스 로직(실 사용 로직)을 구현

class ExampleUseCase {
    final ExampleImplements repository;

    ExampleUseCase({required this.repository});

    // data 값이 'good'이 포함된 값만 반환하는 로직
    Future<List<ExamModel>> readGood(String id) async {
        final datalist = repository.getExample(id);

        final data = datalist.where((e){
            return e.data.contains('good');
        });

        return data;
    }
    
    .. 등등
}

 

 

[ Presentation ]

  • UI (Views)

UI 폴더는

화면(UI)의 구조를 정의하는 클래스들이 포함된다.
ViewModel로부터 데이터를 받아 UI 요소를 배치하고, 상태를 표시

 

 

  • ViewModels

ViewModels 폴더는

MVVM 아키텍처에서 UI(Views)와 UseCase 사이를 연결하는 로직을 포함한다.
UI 상태와 이벤트를 관리하며, 데이터를 View에 노출시킨다.

< 예시 >
ViewModel은 UseCase를 호출하여 데이터를 가져오고, 그 데이터를 UI로 전달

class ExamState{
  ExamModel exam;
  
  ExamState(this.exam);
}

class ExamViewModel extends Notifier<ExamState> {
  final ExampleSource exampleSource;

  @override
  ExamState build(){
    return ExamState(ExamModel(0,DateTime.now(),[]));
  }

  Future<void> updateState(String id) async {
    final newExam = await exampleSource.get(id);
    state = HomeState(newExam);
  }
}

final examViewModelProvider = NotifierProvider<ExamViewModel, ExamState>(
  () {
    return ExamViewModel();
  }
);

 

 

  • Widgets

Widgets 폴더는

화면에서 재사용 가능한 작은 UI 컴포넌트들을 정의한다.
특정 화면에 종속적이지 않고, 여러 화면에서 활용 가능한 위젯들이 주로 위치한다.

 

 

[ Presentation 에 추가로 고려할 만한 것들 ]

  • Theme

테마 관련 파일들이 위치한 장소

 

  • App

엡의 네비게이션 및 라우팅을 관리하는 장소

 

 

 

이 글도 나쁘지 않은듯

 

[Flutter] Repository 패턴과 아키텍처feat.Riverpod

로그인 회원가입 등 구현에 대해 이야기하기 전, 아키텍처에 관해 이야기를 먼저 해야 구현 부분을 매끄럽게 이어갈 수 있을거 같아 글을 쓰게 되었습니다. 아키텍처에 대해 진지하게 생각한

nomal-dev.tistory.com

 

그래서 위의 아키텍처 방식이 클린 아키텍처의 정답인가?

아니다.

위의 아키텍처 방식은 일반적으로 추천되는 방식의 아키텍처 구조일뿐, 클린 아키텍처라는 것은 사용되는 프로젝트마다 달라질 수 있다는 것을 알아두자!

 

밑의 참고자료는, 또 다른 방식의 클린 아키텍처를 소개하고 있으니, 한번 읽어보는것도 좋다!

 

Flutter Project Structure: Feature-first or Layer-first?

An overview of the feature-first and layer-first approaches when choosing a project structure for medium/large Flutter apps, along with their tradeoffs and common pitfalls.

codewithandrea.com