팀 단위로 프로젝트를 진행하다 보면 앱의 코드베이스가 점점 방대해집니다. 초기에는 빠른 개발 속도가 장점이었지만, 규모가 커질수록 유지보수성과 확장성이 점점 더 중요해졌습니다. 결국 이러한 문제를 해결하기 위해 저희 팀은 현재 Clean Architecture를 도입하게 되었습니다.
오늘은 Clean Architecture가 무엇인지 소개해 보겠습니다.
클린 아키텍처는 로버트 C. 마틴(일명 Uncle Bob)이 제안한 아키텍처 원칙으로, “의존성은 항상 안쪽으로 흐른다”라는 규칙을 핵심으로 합니다.
여기서 안쪽(Inner Circle)은 도메인 규칙과 핵심 로직이고, 바깥쪽(Outer Circle)은 UI, DB, 프레임워크 같은 기술적 세부사항입니다.
즉,도메인 로직(비즈니스 규칙)은 어떤 기술에도 의존하지 않고,
기술 스택(UI, DB, 네트워크 등)이 도메인 로직에 의존하도록 만드는 구조입니다.
Clean Architecture에는 크게 3가지 Layer가 존재합니다. 그림으로 나타내면 위와 같습니다. 각 Layer에 대해서 자세하게 알아보겠습니다.
Data Layer
위 계층에서는 Models, Data Sources 그리고 Repository의 구현체가 존재합니다.
- Models : 네트워크 통신을 위한 객체로 JSON Parsing을 담당.
class TodoModel {
final int id;
final String title;
TodoModel({required this.id, required this.title});
factory TodoModel.fromJson(Map<String, dynamic> json) {
return TodoModel(
id: json['id'],
title: json['title'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
};
}
}
- Data Sources: 로컬 혹은 원격 API 상호작용을 담당.
로컬 데이터베이스 혹은 백엔드와의 API 통신 엔드포인트로 구성됩니다.
- Repository Impl: Domain Layer의 Repository 구현체.
Repository의 실질적인 구현체로, 데이터 캐싱, 로컬, 원격 Data Sources를 통합하는 역할을 합니다. 보통 Repository가 API 통신을 하는것인가라고 생각할 수 있습니다만, Repository는 엄연하게 데이터소스와 상호작용하는 객체입니다.
Domain Layer
위 계층에서는 Entity, Repository, Usecase가 존재합니다.
- Entity: 비즈니스 규칙을 담은 내부 모델.
class TodoEntity {
final int id;
final String title;
TodoEntity({required this.id, required this.title});
...
// 비즈니스 로직 존재 가능함.
}
앱 내부 비즈니스 로직에서 사용되는 객체로, API에서 분리되어 있기에 비즈니스 로직을 담을 수 있습니다.
- Repository: Repository를 정의.
데이터소스와 상호작용할 인터페이스를 정의하는 객체입니다. Domain Layer의 종속되지 않아야합니다.
- Usecase: 특정 비즈니스 요구사항을 수행.
특정 요구사항을 수행하는 객체로 실질적인 비즈니스 로직입니다. Usecase는 Presentation Layer의 ViewModel 혹은 Bloc, Cubit, Provider가 주입받아 사용하며 필요한 모든 Repository를 주입받아 사용할 수 있기에 순차적으로 Repository의 메소드를 수행할수도 있습니다.
class GetTodos {
...
Future<List<Todo>> call(Param param) async {
// 사용자 인증 먼저 확인
final auth = await _authRepository.authenticate();
if (!auth) {
return AuthError();
}
// todo 목록 가져오기
final todos = await _todoRepository.getTodos();
return todos;
}
}
이렇게되면, ViewModel, Provider, Bloc, Cubit 등등이 직접 Repository를 참조하지 않고 비즈니스 로직에 집중되기에 흐름을 파악할 수도 있습니다.
Presentation Layer
사실 위에서는 여러 요소가 있으나 Presentation Layer는 MVP, MVVM 등 아키텍처 패턴을 적용하여 UI와 비즈니스 로직을 연결하는 계층입니다. 상태관리 라이브러리에 따라서 GetX, Provider, Riverpods, Bloc 등의 아키텍처 패턴이 적용될 수 있습니다.
사실 여기서는 구성요소가 어떤 아키텍처 패턴을 사용하냐에 따라서 계속 달라집니다. UI 규모, 분리 기준에 따라서 적절하게 결정할 수 있습니다.
지금까지 구성 요소에 대해서 자세히 알아보았습니다.
저희팀은 초기 단계에서 빠른 개발을 위해 10000줄이 넘는 많은 코드를 작성했습니다. 하지만, 작성된 코드는 어떠한 분리 구분이 존재하지 않았으며, 오직 BLoC 아키텍처만을 따를 뿐이었고, 이후 전체 요구사항이 변경되면서 이미 작성된 코드는 보일러 플레이트 코드와 비슷해졌습니다. 그에 따라 만장일치로 현재 아키텍처만으로는 좋은 개발 환경을 만들 수 없다는 결론에 도달했습니다.
Clean Architecture를 도입하게 되면 리팩토링 과정에서 많은 작업시간이 소요되지만 저희팀이 얻는 장점은 아래와 같았습니다.
- 명확한 역할 분리
UI, Bloc, Repository간의 코드가 얽히던 것이 Presentation, Domain, Data Layer로 구분되면서 각 계층의 책임이 집중됩니다.
- 유지보수성과 확작성, 재사용성 증가
기능이 커질수록 방대해지는 Bloc의 크기가 Usecase로 분리되어 해결됩니다. 또한, 새로운 기능도 Usecase로 제작하면 되기에 기존 모듈에 대한 영향이 최소화되고, Usecase의 재사용이 가능해집니다.
- 테스트 용이성
Usecase 단위로 독립적인 테스트 작성이 가능하여 비즈니스 로직 검증이 쉬워집니다.
- 유연한 의존성 관리
추상화된 인터페이스로 수정이 크게 감소합니다. 예를 들어, Dio기반의 REST API 통신 엔드포인트가 변경되더라도 추상화 인터페이스를 의존하는 다른 모듈은 수정할 필요 없이 구현체만 수정이 가능해집니다.이러한 장점이 단점보다 훨씬 더 크게 작용한다고 판단하여 현재 도입하여 사용하고 있습니다. 하지만 장점이 있다면 단점도 존재합니다.
- 초기 개발 속도 저하
- 학습 곡선
- 보일러플레이트 코드 증가
- 오버 엔지니어링 초래 가능
실제로 저희 팀에서도 초기 개발단계에서는 MVVM 구조는 문제가 없었으나 점점 코드가 방대해지며, Clean Architecture를 도입하게 되었습니다. 작은 규모에서는 MVVM도 충분히 사용할 수 있습니다. 또한, 이후 도입과정에서 많은 시간 비용이 발생했습니다. 또한, 팀원 전체가 Clean Architecture를 학습하는 시간도 필요해졌습니다.
아키텍처는 팀 프로젝트에서 중요한 역할을 하게됩니다. 정답은 존재하지 않습니다. 현재 팀의 상황에 장점이 극대화 될 수 있는 아키텍처를 선택하여 문제를 해결하는 것이 좋다고 생각됩니다.
Flutter 클린 아키텍처: 작은 앱부터 대규모 프로젝트까지 맞춤 설계
Flutter의 Clean Architecture 클린아키텍처, 각 Layer에 대하여