mirror of
https://github.com/ChangJoo-Park/learn-flutter.git
synced 2025-06-14 20:14:21 +00:00
440 lines
17 KiB
Markdown
440 lines
17 KiB
Markdown
# 기능별 vs 계층별 폴더 구조
|
|
|
|
Flutter 프로젝트를 시작할 때 가장 중요한 결정 중 하나는 코드를 어떻게 구성할 것인지 결정하는 것입니다. 적절한 프로젝트 구조는 코드의 가독성을 높이고, 확장성을 향상시키며, 팀 협업을 원활하게 합니다. 이 문서에서는 두 가지 주요 프로젝트 구조 접근 방식인 '기능별 구조'와 '계층별 구조'에 대해 살펴보고, 각각의 장단점 및 적합한 사용 사례를 분석합니다.
|
|
|
|
## 프로젝트 구조의 중요성
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[프로젝트 구조] --> B[코드 가독성]
|
|
A --> C[유지보수성]
|
|
A --> D[확장성]
|
|
A --> E[협업 용이성]
|
|
A --> F[테스트 용이성]
|
|
style A fill:#f9d5e5
|
|
```
|
|
|
|
좋은 프로젝트 구조는 다음과 같은 이점을 제공합니다:
|
|
|
|
1. **새로운 개발자의 온보딩 시간 단축**: 직관적인 구조는 새로운 팀원이 프로젝트를 더 빠르게 이해하도록 돕습니다.
|
|
2. **코드 충돌 감소**: 잘 정의된 구조는 여러 개발자가 동시에 작업할 때 충돌을 줄입니다.
|
|
3. **기능 구현 시간 단축**: 관련 코드를 쉽게 찾고 수정할 수 있습니다.
|
|
4. **테스트 용이성**: 모듈화된 구조는 테스트를 더 쉽게 작성하고 실행할 수 있게 합니다.
|
|
5. **코드 재사용 촉진**: 잘 구성된 구조는 코드 재사용을 장려하고 중복을 줄입니다.
|
|
|
|
## 계층별 폴더 구조
|
|
|
|
계층별 폴더 구조(Layer-based Structure)는 코드를 기술적 관심사 또는 아키텍처 계층에 따라 구성하는 방식입니다.
|
|
|
|
### 계층별 구조의 기본 예시
|
|
|
|
```
|
|
lib/
|
|
├── models/ # 데이터 모델 클래스
|
|
│ ├── user.dart
|
|
│ ├── product.dart
|
|
│ └── order.dart
|
|
├── views/ # UI 컴포넌트 및 화면
|
|
│ ├── home_screen.dart
|
|
│ ├── product_screen.dart
|
|
│ └── profile_screen.dart
|
|
├── controllers/ # 비즈니스 로직 및 상태 관리
|
|
│ ├── auth_controller.dart
|
|
│ ├── product_controller.dart
|
|
│ └── order_controller.dart
|
|
├── services/ # 외부 서비스 통신 (API, 데이터베이스 등)
|
|
│ ├── api_service.dart
|
|
│ ├── storage_service.dart
|
|
│ └── analytics_service.dart
|
|
├── utils/ # 유틸리티 함수 및 상수
|
|
│ ├── constants.dart
|
|
│ ├── extensions.dart
|
|
│ └── helpers.dart
|
|
└── main.dart
|
|
```
|
|
|
|
### 계층별 구조의 변형: MVVM 패턴
|
|
|
|
```
|
|
lib/
|
|
├── data/
|
|
│ ├── models/ # 데이터 모델
|
|
│ ├── repositories/ # 데이터 액세스 계층
|
|
│ └── data_sources/ # 로컬/원격 데이터 소스
|
|
├── domain/
|
|
│ ├── entities/ # 비즈니스 모델
|
|
│ ├── repositories/ # 리포지토리 인터페이스
|
|
│ └── usecases/ # 비즈니스 규칙 및 로직
|
|
├── presentation/
|
|
│ ├── pages/ # 화면
|
|
│ ├── widgets/ # 재사용 가능한 UI 컴포넌트
|
|
│ └── viewmodels/ # 뷰 모델 (상태 관리)
|
|
├── core/
|
|
│ ├── utils/ # 유틸리티 함수
|
|
│ ├── constants/ # 상수
|
|
│ └── theme/ # 앱 테마
|
|
└── main.dart
|
|
```
|
|
|
|
### 계층별 구조의 장점
|
|
|
|
1. **기술적 관심사 분리**: 코드를 역할에 따라 명확하게 구분합니다.
|
|
2. **구조 이해 용이성**: 새로운 개발자가 아키텍처를 쉽게 이해할 수 있습니다.
|
|
3. **역할 기반 작업 분담**: 프론트엔드/백엔드 개발자가 각자 담당 영역에 집중할 수 있습니다.
|
|
4. **유사한 코드 패턴**: 같은 계층에 있는 코드는 유사한 패턴을 따르기 쉽습니다.
|
|
5. **기술 스택 변경 용이성**: 특정 계층의 기술을 교체할 때 영향 범위가 제한적입니다.
|
|
|
|
### 계층별 구조의 단점
|
|
|
|
1. **관련 코드 분산**: 하나의 기능을 구현하기 위해 여러 폴더를 탐색해야 합니다.
|
|
2. **파일 수 증가에 따른 복잡성**: 프로젝트가 커지면 각 폴더의 파일 수가 많아져 찾기 어려워집니다.
|
|
3. **기능 추가 시 여러 폴더 수정**: 새 기능 추가 시 여러 폴더에 걸쳐 파일을 생성/수정해야 합니다.
|
|
4. **관련 코드의 결합도 파악 어려움**: 서로 다른 폴더에 있는 관련 코드 간의 관계를 파악하기 어렵습니다.
|
|
5. **코드 재사용 저해**: 특정 기능에 특화된 코드를 식별하고 재사용하기 어려울 수 있습니다.
|
|
|
|
## 기능별 폴더 구조
|
|
|
|
기능별 폴더 구조(Feature-based Structure)는 코드를 비즈니스 기능이나 앱의 주요 기능에 따라 구성하는 방식입니다.
|
|
|
|
### 기능별 구조의 기본 예시
|
|
|
|
```
|
|
lib/
|
|
├── features/
|
|
│ ├── auth/ # 인증 관련 기능
|
|
│ │ ├── data/
|
|
│ │ │ ├── repositories/
|
|
│ │ │ └── models/
|
|
│ │ ├── domain/
|
|
│ │ │ └── usecases/
|
|
│ │ └── presentation/
|
|
│ │ ├── pages/
|
|
│ │ │ ├── login_page.dart
|
|
│ │ │ └── signup_page.dart
|
|
│ │ ├── widgets/
|
|
│ │ └── providers/
|
|
│ │
|
|
│ ├── products/ # 제품 관련 기능
|
|
│ │ ├── data/
|
|
│ │ ├── domain/
|
|
│ │ └── presentation/
|
|
│ │ ├── pages/
|
|
│ │ │ ├── product_list_page.dart
|
|
│ │ │ └── product_detail_page.dart
|
|
│ │ ├── widgets/
|
|
│ │ └── providers/
|
|
│ │
|
|
│ └── profile/ # 프로필 관련 기능
|
|
│ ├── data/
|
|
│ ├── domain/
|
|
│ └── presentation/
|
|
│
|
|
├── core/ # 공통 기능
|
|
│ ├── network/
|
|
│ ├── storage/
|
|
│ ├── theme/
|
|
│ └── utils/
|
|
│
|
|
└── main.dart
|
|
```
|
|
|
|
### 기능별 구조의 변형: DDD(Domain-Driven Design) 적용
|
|
|
|
```
|
|
lib/
|
|
├── application/ # 애플리케이션 서비스 (UseCase)
|
|
│ ├── auth/
|
|
│ ├── products/
|
|
│ └── profile/
|
|
├── domain/ # 도메인 모델 및 규칙
|
|
│ ├── auth/
|
|
│ ├── products/
|
|
│ └── profile/
|
|
├── infrastructure/ # 인프라 계층 (리포지토리 구현, 데이터 소스)
|
|
│ ├── auth/
|
|
│ ├── products/
|
|
│ └── profile/
|
|
├── presentation/ # UI 계층
|
|
│ ├── auth/
|
|
│ ├── products/
|
|
│ └── profile/
|
|
├── shared/ # 공통 기능
|
|
│ ├── constants/
|
|
│ ├── extensions/
|
|
│ └── widgets/
|
|
└── main.dart
|
|
```
|
|
|
|
### 기능별 구조의 장점
|
|
|
|
1. **관련 코드 근접성**: 하나의 기능과 관련된 모든 코드가 같은 폴더에 위치합니다.
|
|
2. **독립적인 기능 개발**: 각 기능을 독립적으로 개발하고 테스트할 수 있습니다.
|
|
3. **기능 단위의 캡슐화**: 각 기능은 자체 모델, 뷰, 로직을 포함하여 자율적입니다.
|
|
4. **기능별 작업 분담**: 팀원이 특정 기능에 집중하여 작업할 수 있습니다.
|
|
5. **확장성**: 새로운 기능을 추가할 때 기존 코드를 변경할 필요가 적습니다.
|
|
6. **코드 재사용**: 기능별로 특화된 코드를 쉽게 식별하고 재사용할 수 있습니다.
|
|
|
|
### 기능별 구조의 단점
|
|
|
|
1. **중복 코드 가능성**: 여러 기능 간에 유사한 코드가 중복될 수 있습니다.
|
|
2. **일관성 유지 어려움**: 각 기능별로 다른 패턴이 적용될 수 있습니다.
|
|
3. **기능 간 경계 설정 어려움**: 어떤 코드가 어느 기능에 속하는지 결정하기 어려울 수 있습니다.
|
|
4. **공통 코드 관리**: 여러 기능에서 사용하는 공통 코드의 위치를 결정하기 어려울 수 있습니다.
|
|
5. **아키텍처 이해의 어려움**: 전체 아키텍처를 한눈에 파악하기 어려울 수 있습니다.
|
|
|
|
## 실제 프로젝트 구조 예시: Riverpod + GoRouter
|
|
|
|
### 계층별 구조 예시
|
|
|
|
```dart
|
|
lib/
|
|
├── models/ # 데이터 모델
|
|
│ ├── user.dart
|
|
│ ├── product.dart
|
|
│ └── order.dart
|
|
├── providers/ # Riverpod 프로바이더
|
|
│ ├── auth_provider.dart
|
|
│ ├── product_provider.dart
|
|
│ └── cart_provider.dart
|
|
├── repositories/ # 데이터 액세스 계층
|
|
│ ├── auth_repository.dart
|
|
│ ├── product_repository.dart
|
|
│ └── order_repository.dart
|
|
├── screens/ # 화면 위젯
|
|
│ ├── auth/
|
|
│ │ ├── login_screen.dart
|
|
│ │ └── signup_screen.dart
|
|
│ ├── products/
|
|
│ │ ├── product_list_screen.dart
|
|
│ │ └── product_detail_screen.dart
|
|
│ └── profile/
|
|
│ └── profile_screen.dart
|
|
├── widgets/ # 재사용 위젯
|
|
│ ├── product_card.dart
|
|
│ ├── custom_button.dart
|
|
│ └── loading_indicator.dart
|
|
├── router/ # GoRouter 설정
|
|
│ └── router.dart
|
|
├── utils/ # 유틸리티
|
|
│ ├── constants.dart
|
|
│ └── extensions.dart
|
|
└── main.dart
|
|
```
|
|
|
|
### 기능별 구조 예시
|
|
|
|
```dart
|
|
lib/
|
|
├── features/
|
|
│ ├── auth/
|
|
│ │ ├── models/
|
|
│ │ │ └── user.dart
|
|
│ │ ├── repositories/
|
|
│ │ │ └── auth_repository.dart
|
|
│ │ ├── providers/
|
|
│ │ │ └── auth_provider.dart
|
|
│ │ ├── screens/
|
|
│ │ │ ├── login_screen.dart
|
|
│ │ │ └── signup_screen.dart
|
|
│ │ └── widgets/
|
|
│ │ ├── login_form.dart
|
|
│ │ └── social_login_buttons.dart
|
|
│ │
|
|
│ ├── products/
|
|
│ │ ├── models/
|
|
│ │ │ └── product.dart
|
|
│ │ ├── repositories/
|
|
│ │ │ └── product_repository.dart
|
|
│ │ ├── providers/
|
|
│ │ │ └── product_provider.dart
|
|
│ │ ├── screens/
|
|
│ │ │ ├── product_list_screen.dart
|
|
│ │ │ └── product_detail_screen.dart
|
|
│ │ └── widgets/
|
|
│ │ └── product_card.dart
|
|
│ │
|
|
│ └── cart/
|
|
│ ├── models/
|
|
│ │ └── cart_item.dart
|
|
│ ├── repositories/
|
|
│ │ └── cart_repository.dart
|
|
│ ├── providers/
|
|
│ │ └── cart_provider.dart
|
|
│ ├── screens/
|
|
│ │ ├── cart_screen.dart
|
|
│ │ └── checkout_screen.dart
|
|
│ └── widgets/
|
|
│ └── cart_item_widget.dart
|
|
│
|
|
├── core/
|
|
│ ├── router/
|
|
│ │ └── router.dart
|
|
│ ├── theme/
|
|
│ │ └── app_theme.dart
|
|
│ ├── widgets/
|
|
│ │ ├── custom_button.dart
|
|
│ │ └── loading_indicator.dart
|
|
│ └── utils/
|
|
│ ├── constants.dart
|
|
│ └── extensions.dart
|
|
│
|
|
└── main.dart
|
|
```
|
|
|
|
## 하이브리드 접근 방식
|
|
|
|
많은 실제 프로젝트에서는 두 가지 접근 방식을 혼합하여 사용합니다. 다음은 하이브리드 구조의 예시입니다:
|
|
|
|
```
|
|
lib/
|
|
├── features/ # 주요 기능별 구성
|
|
│ ├── auth/
|
|
│ ├── products/
|
|
│ └── cart/
|
|
│
|
|
├── shared/ # 공유 컴포넌트
|
|
│ ├── models/ # 공통 모델
|
|
│ ├── widgets/ # 공통 위젯
|
|
│ ├── services/ # 공통 서비스
|
|
│ └── utils/ # 유틸리티
|
|
│
|
|
├── core/ # 핵심 인프라
|
|
│ ├── network/ # 네트워크 관련
|
|
│ ├── storage/ # 로컬 스토리지
|
|
│ ├── di/ # 의존성 주입
|
|
│ └── router/ # 라우팅
|
|
│
|
|
└── main.dart
|
|
```
|
|
|
|
이 접근 방식은 다음과 같은 이점을 제공합니다:
|
|
|
|
1. **주요 기능의 독립성**: 핵심 기능은 독립적으로 구성
|
|
2. **공통 코드 관리**: 여러 기능에서 공유하는 코드는 중앙에서 관리
|
|
3. **핵심 인프라 분리**: 앱의 기반이 되는 인프라 코드를 명확하게 분리
|
|
4. **유연성**: 프로젝트 요구사항에 맞게 구조를 조정할 수 있음
|
|
|
|
## Riverpod과 함께 사용하는 패턴
|
|
|
|
Riverpod을 사용할 때는 프로바이더를 어떻게 구성할지도 중요한 고려 사항입니다:
|
|
|
|
### 계층별 구조에서의 Riverpod 패턴
|
|
|
|
```dart
|
|
// providers/product_provider.dart
|
|
final productRepositoryProvider = Provider<ProductRepository>((ref) {
|
|
return ProductRepositoryImpl(ref.read(apiServiceProvider));
|
|
});
|
|
|
|
final productsProvider = FutureProvider<List<Product>>((ref) async {
|
|
final repository = ref.watch(productRepositoryProvider);
|
|
return repository.getProducts();
|
|
});
|
|
|
|
final productDetailsProvider = FutureProvider.family<Product, String>((ref, id) async {
|
|
final repository = ref.watch(productRepositoryProvider);
|
|
return repository.getProductById(id);
|
|
});
|
|
```
|
|
|
|
### 기능별 구조에서의 Riverpod 패턴
|
|
|
|
```dart
|
|
// features/products/providers/product_provider.dart
|
|
final productRepositoryProvider = Provider<ProductRepository>((ref) {
|
|
return ProductRepositoryImpl(ref.read(apiServiceProvider));
|
|
});
|
|
|
|
final productsProvider = FutureProvider<List<Product>>((ref) async {
|
|
final repository = ref.watch(productRepositoryProvider);
|
|
return repository.getProducts();
|
|
});
|
|
|
|
final productDetailsProvider = FutureProvider.family<Product, String>((ref, id) async {
|
|
final repository = ref.watch(productRepositoryProvider);
|
|
return repository.getProductById(id);
|
|
});
|
|
```
|
|
|
|
### 추천 패턴: 계층적 프로바이더 구성
|
|
|
|
```dart
|
|
// features/products/providers/product_provider.dart
|
|
|
|
// 1. 데이터 소스 프로바이더 (최하위 계층)
|
|
@riverpod
|
|
ProductDataSource productDataSource(ProductDataSourceRef ref) {
|
|
final apiClient = ref.watch(apiClientProvider);
|
|
return ProductDataSourceImpl(apiClient);
|
|
}
|
|
|
|
// 2. 리포지토리 프로바이더
|
|
@riverpod
|
|
ProductRepository productRepository(ProductRepositoryRef ref) {
|
|
final dataSource = ref.watch(productDataSourceProvider);
|
|
return ProductRepositoryImpl(dataSource);
|
|
}
|
|
|
|
// 3. 유스케이스 프로바이더 (선택적)
|
|
@riverpod
|
|
GetProductsUseCase getProductsUseCase(GetProductsUseCaseRef ref) {
|
|
final repository = ref.watch(productRepositoryProvider);
|
|
return GetProductsUseCase(repository);
|
|
}
|
|
|
|
// 4. 상태 프로바이더
|
|
@riverpod
|
|
class ProductsNotifier extends _$ProductsNotifier {
|
|
@override
|
|
FutureOr<List<Product>> build() async {
|
|
return _fetchProducts();
|
|
}
|
|
|
|
Future<List<Product>> _fetchProducts() {
|
|
final useCase = ref.watch(getProductsUseCaseProvider);
|
|
return useCase.execute();
|
|
}
|
|
|
|
Future<void> refresh() async {
|
|
state = const AsyncValue.loading();
|
|
state = await AsyncValue.guard(_fetchProducts);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 어떤 구조를 선택해야 할까?
|
|
|
|
프로젝트 구조 선택 시 고려해야 할 요소:
|
|
|
|
### 계층별 구조가 적합한 경우
|
|
|
|
1. **소규모 프로젝트**: 기능이 적고 단순한 앱
|
|
2. **명확한 기술적 분리**: 프론트엔드/백엔드 개발자 역할이 명확히 구분된 팀
|
|
3. **아키텍처 패턴 중시**: MVC, MVVM과 같은 아키텍처 패턴을 엄격히 따르고자 할 때
|
|
4. **초보 개발자 팀**: 명확한 폴더 구조가 필요한 경우
|
|
|
|
### 기능별 구조가 적합한 경우
|
|
|
|
1. **중대형 프로젝트**: 다양한 기능이 있는 복잡한 앱
|
|
2. **수직적 팀 구조**: 팀원이 특정 기능을 전담하는 경우
|
|
3. **마이크로서비스 지향**: 각 기능을 독립적으로 개발/테스트하고자 할 때
|
|
4. **기능 단위 배포**: 기능별로 점진적 배포를 계획하는 경우
|
|
5. **도메인 중심 설계**: DDD(Domain-Driven Design) 원칙을 따르는 경우
|
|
|
|
## 실제 업계 사례
|
|
|
|
대형 Flutter 앱들의 구조를 살펴보면 다음과 같은 패턴을 볼 수 있습니다:
|
|
|
|
1. **Google의 Flutter 샘플 앱**: 기능별 구조를 선호 (Flutter Gallery, Flutter Samples)
|
|
2. **Alibaba의 Flutter 앱**: 하이브리드 구조 채택 (일부 공통 모듈은 계층별, 주요 기능은 기능별)
|
|
3. **중소규모 앱**: 초기에는 계층별 구조로 시작하여 점차 기능별 또는 하이브리드 구조로 전환하는 경향
|
|
|
|
## 결론
|
|
|
|
프로젝트 구조는 정답이 없는 주제입니다. 중요한 것은 팀과 프로젝트 특성에 맞는 구조를 선택하고, 일관성 있게 유지하는 것입니다.
|
|
|
|
- **소규모/단순 앱**: 계층별 구조가 직관적이고 빠르게 구성 가능
|
|
- **중대형/복잡한 앱**: 기능별 구조 또는 하이브리드 구조가 유지보수와 확장성에 유리
|
|
- **성장 예상 앱**: 처음부터 기능별 구조 또는 하이브리드 구조로 시작하는 것이 장기적으로 유리
|
|
|
|
어떤 구조를 선택하든, 코드 가독성, 유지보수성, 확장성, 팀 협업 용이성이라는 핵심 목표를 기억하며 구조를 설계해야 합니다. 또한 프로젝트가 발전함에 따라 구조를 재평가하고 필요에 따라 조정하는 유연성을 유지하는 것이 중요합니다.
|