클린 아키텍처 심화
< 클린 아키텍쳐 >
클린 아키텍쳐에서는 이와같은 레이어(모듈)를 구상하는것을 추천한다.
- 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