멀티 모듈 아키텍처
대규모 Flutter 프로젝트에서는 코드베이스가 커짐에 따라 빌드 시간 증가, 유지보수 복잡성, 팀 협업 어려움 등의 문제가 발생할 수 있습니다. 이러한 문제를 해결하기 위한 방법 중 하나가 멀티 모듈 아키텍처입니다. 이 문서에서는 Flutter에서 멀티 모듈 아키텍처를 구현하는 방법, 장단점, 그리고 실제 적용 사례를 살펴보겠습니다.
멀티 모듈 아키텍처란?
Section titled “멀티 모듈 아키텍처란?”멀티 모듈 아키텍처는 하나의 큰 애플리케이션을 여러 개의 독립적인 모듈(또는 패키지)로 분리하는 접근 방식입니다. 각 모듈은 특정 기능이나 도메인에 초점을 맞추고, 명확하게 정의된 인터페이스를 통해 다른 모듈과 통신합니다.
멀티 모듈의 장점
Section titled “멀티 모듈의 장점”- 빌드 시간 단축: 전체 앱이 아닌 변경된 모듈만 재빌드하여 개발 속도를 높일 수 있습니다.
- 관심사 분리: 각 모듈은 특정 기능에 집중하여 코드 이해와 유지보수가 용이해집니다.
- 팀 협업 개선: 각 팀이 독립적인 모듈에서 작업하여 코드 충돌을 줄일 수 있습니다.
- 코드 재사용성: 모듈을 다른 프로젝트에서 재사용할 수 있습니다.
- 테스트 용이성: 모듈별로 독립적인 테스트가 가능합니다.
- 의존성 명확화: 모듈 간 의존성이 명시적으로 정의되어 구조가 명확해집니다.
멀티 모듈의 단점
Section titled “멀티 모듈의 단점”- 초기 설정 복잡성: 프로젝트 구조 설정이 복잡하고 시간이 소요됩니다.
- 의존성 관리 어려움: 모듈 간 의존성을 올바르게 관리해야 합니다.
- 통합 테스트 복잡성: 모듈 간 통합 테스트가 더 복잡해질 수 있습니다.
- 학습 곡선: 팀원들이 모듈 구조에 적응하는 데 시간이 필요합니다.
- 오버엔지니어링 위험: 작은 프로젝트에서는 불필요한 복잡성이 추가될 수 있습니다.
Flutter에서의 멀티 모듈 구현 방법
Section titled “Flutter에서의 멀티 모듈 구현 방법”Flutter에서 멀티 모듈 아키텍처를 구현하는 여러 방법이 있습니다. 가장 일반적인 방법은 다음과 같습니다:
1. 로컬 패키지 사용
Section titled “1. 로컬 패키지 사용”같은 저장소 내에서 여러 패키지를 관리하는 방법입니다.
프로젝트 구조 예시
Section titled “프로젝트 구조 예시”my_flutter_project/├── app/ # 메인 앱 모듈│ ├── lib/│ ├── pubspec.yaml│ └── ...├── packages/│ ├── core/ # 핵심 기능 모듈│ │ ├── lib/│ │ └── pubspec.yaml│ ├── feature_auth/ # 인증 기능 모듈│ │ ├── lib/│ │ └── pubspec.yaml│ ├── feature_home/ # 홈 기능 모듈│ │ ├── lib/│ │ └── pubspec.yaml│ └── feature_profile/ # 프로필 기능 모듈│ ├── lib/│ └── pubspec.yaml└── pubspec.yaml # 루트 pubspec.yaml (옵션)
각 모듈의 pubspec.yaml 설정
Section titled “각 모듈의 pubspec.yaml 설정”app/pubspec.yaml:
name: my_appdescription: Main application moduleversion: 1.0.0+1
environment: sdk: ">=3.0.0 <4.0.0" flutter: ">=3.10.0"
dependencies: flutter: sdk: flutter # 로컬 패키지 의존성 core: path: ../packages/core feature_auth: path: ../packages/feature_auth feature_home: path: ../packages/feature_home feature_profile: path: ../packages/feature_profile
packages/core/pubspec.yaml:
name: coredescription: Core module with shared functionalityversion: 0.0.1
environment: sdk: ">=3.0.0 <4.0.0" flutter: ">=3.10.0"
dependencies: flutter: sdk: flutter # 코어 모듈의 의존성 http: ^1.1.0 shared_preferences: ^2.2.0
packages/feature_auth/pubspec.yaml:
name: feature_authdescription: Authentication feature moduleversion: 0.0.1
environment: sdk: ">=3.0.0 <4.0.0" flutter: ">=3.10.0"
dependencies: flutter: sdk: flutter # 코어 모듈에 의존 core: path: ../core # 기타 의존성 firebase_auth: ^4.6.0
2. melos를 사용한 모노레포
Section titled “2. melos를 사용한 모노레포”melos는 Dart/Flutter 프로젝트에서 모노레포를 관리하기 위한 도구로, 여러 패키지를 효율적으로 관리할 수 있게 해줍니다.
설치 및 설정
Section titled “설치 및 설정”- melos 설치:
dart pub global activate melos
- 프로젝트 루트에
melos.yaml
파일 생성:
name: my_flutter_project
packages: - app - packages/**
scripts: analyze: run: melos exec -- "flutter analyze" description: Run flutter analyze in all packages
test: run: melos exec -- "flutter test" description: Run flutter test in all packages
pub_get: run: melos exec -- "flutter pub get" description: Run flutter pub get in all packages
- 사용 예시:
# 모든 패키지에서 flutter pub get 실행melos pub_get
# 모든 패키지에서 flutter analyze 실행melos analyze
# 모든 패키지에서 flutter test 실행melos test
3. Flutter Flavors와 조합
Section titled “3. Flutter Flavors와 조합”멀티 모듈 아키텍처는 Flutter Flavors와 결합하여 다양한 앱 버전(개발, 스테이징, 프로덕션 등)을 관리할 수 있습니다:
import 'package:core/config.dart';import 'package:flutter/material.dart';import 'app.dart';
void main() { AppConfig.initialize( env: Environment.dev, apiUrl: 'https://dev-api.example.com', ); runApp(const MyApp());}
import 'package:core/config.dart';import 'package:flutter/material.dart';import 'app.dart';
void main() { AppConfig.initialize( env: Environment.prod, apiUrl: 'https://api.example.com', ); runApp(const MyApp());}
모듈 구조 및 설계 방법론
Section titled “모듈 구조 및 설계 방법론”멀티 모듈 아키텍처에서 일반적으로 사용되는 모듈 유형은 다음과 같습니다:
- 앱 모듈(App Module): 애플리케이션의 진입점이며 다른 모든 모듈을 통합합니다.
- 코어 모듈(Core Module): 공통 기능, 유틸리티, 핵심 컴포넌트 등을 포함합니다.
- 기능 모듈(Feature Module): 특정 기능 또는 도메인에 집중한 모듈입니다.
- UI 모듈(UI Module): 재사용 가능한 UI 컴포넌트를 포함합니다.
- 데이터 모듈(Data Module): 데이터 액세스 로직을 담당합니다.
의존성 방향
Section titled “의존성 방향”모듈 간 의존성 방향은 명확하게 설정해야 합니다:
- 코어 모듈: 다른 모든 모듈의 기반이 되며, 다른 모듈에 의존하지 않습니다.
- 기능 모듈: 코어 모듈과 UI 모듈에 의존할 수 있지만, 다른 기능 모듈에 직접 의존하지 않는 것이 좋습니다.
- 앱 모듈: 모든 모듈을 통합하고 의존합니다.
모듈 간 통신
Section titled “모듈 간 통신”모듈 간 통신은 다음과 같은 방법으로 이루어질 수 있습니다:
- 인터페이스 기반 통신: 모듈은 인터페이스를 정의하고 구현체는 필요한 모듈에서 제공합니다.
- 이벤트 기반 통신: 이벤트 버스나 스트림을 통해 모듈 간 이벤트를 전달합니다.
- 의존성 주입: Riverpod이나 GetIt 같은 도구를 사용하여 모듈 간 의존성을 관리합니다.
인터페이스 기반 통신 예시
Section titled “인터페이스 기반 통신 예시”abstract class AuthService { Future<User?> getCurrentUser(); Future<User> signIn(String email, String password); Future<void> signOut();}
// feature_auth/lib/src/services/firebase_auth_service.dartclass FirebaseAuthService implements AuthService { @override Future<User?> getCurrentUser() { // Firebase 구현 }
@override Future<User> signIn(String email, String password) { // Firebase 구현 }
@override Future<void> signOut() { // Firebase 구현 }}
// app/lib/di/service_locator.dartvoid setupServiceLocator() { GetIt.I.registerSingleton<AuthService>(FirebaseAuthService());}
모듈 내부 구조
Section titled “모듈 내부 구조”각 모듈 내부는 클린 아키텍처나 MVVM 같은 아키텍처 패턴을 따를 수 있습니다.
기능 모듈 예시
Section titled “기능 모듈 예시”feature_auth/├── lib/│ ├── src/│ │ ├── data/│ │ │ ├── models/│ │ │ ├── repositories/│ │ │ └── datasources/│ │ ├── domain/│ │ │ ├── entities/│ │ │ ├── usecases/│ │ │ └── repositories/│ │ ├── presentation/│ │ │ ├── pages/│ │ │ ├── widgets/│ │ │ └── providers/│ │ └── di/│ │ └── auth_module.dart│ ├── feature_auth.dart # 공개 API│ └── testing.dart # 테스트 지원 API (선택사항)├── test/└── pubspec.yaml
공개 API 설계
Section titled “공개 API 설계”각 모듈은 명확한 공개 API를 정의해야 합니다. 모듈 내부 구현은 숨기고 필요한 기능만 노출하는 것이 좋습니다.
library feature_auth;
// 공개 APIexport 'src/presentation/pages/login_page.dart';export 'src/presentation/pages/register_page.dart';export 'src/domain/entities/user.dart';export 'src/di/auth_module.dart';
import 'package:get_it/get_it.dart';import '../data/repositories/auth_repository_impl.dart';import '../data/datasources/auth_remote_datasource.dart';import '../domain/repositories/auth_repository.dart';import '../domain/usecases/sign_in.dart';import '../domain/usecases/sign_out.dart';
class AuthModule { static void init() { final GetIt sl = GetIt.instance;
// Data sources sl.registerLazySingleton<AuthRemoteDataSource>( () => AuthRemoteDataSourceImpl(client: sl()), );
// Repositories sl.registerLazySingleton<AuthRepository>( () => AuthRepositoryImpl(remoteDataSource: sl()), );
// Use cases sl.registerLazySingleton(() => SignIn(sl())); sl.registerLazySingleton(() => SignOut(sl())); }}
실제 적용 사례: 전자상거래 앱
Section titled “실제 적용 사례: 전자상거래 앱”실제 전자상거래 앱에 멀티 모듈 아키텍처를 적용해보겠습니다.
프로젝트 구조
Section titled “프로젝트 구조”ecommerce_app/├── app/ # 메인 앱 모듈├── packages/│ ├── core/ # 핵심 기능 모듈│ ├── ui_kit/ # UI 컴포넌트 모듈│ ├── feature_auth/ # 인증 기능 모듈│ ├── feature_products/ # 상품 기능 모듈│ ├── feature_cart/ # 장바구니 기능 모듈│ ├── feature_checkout/ # 결제 기능 모듈│ └── feature_profile/ # 프로필 기능 모듈└── melos.yaml
코어 모듈은 다른 모든 모듈에서 사용하는 공통 기능을 포함합니다:
library core;
export 'src/config/app_config.dart';export 'src/network/api_client.dart';export 'src/storage/local_storage.dart';export 'src/utils/extensions.dart';export 'src/di/service_locator.dart';export 'src/navigation/router.dart';
enum Environment { dev, staging, prod }
class AppConfig { static late Environment _environment; static late String _apiUrl;
static Environment get environment => _environment; static String get apiUrl => _apiUrl;
static void initialize({ required Environment env, required String apiUrl, }) { _environment = env; _apiUrl = apiUrl; }
static bool get isDev => _environment == Environment.dev; static bool get isStaging => _environment == Environment.staging; static bool get isProd => _environment == Environment.prod;}
UI 키트 모듈
Section titled “UI 키트 모듈”UI 키트 모듈은 앱 전체에서 사용되는 공통 UI 컴포넌트를 포함합니다:
library ui_kit;
export 'src/buttons/primary_button.dart';export 'src/cards/product_card.dart';export 'src/theme/app_theme.dart';export 'src/inputs/text_field.dart';
import 'package:flutter/material.dart';
class PrimaryButton extends StatelessWidget { final String text; final VoidCallback onPressed; final bool isLoading;
const PrimaryButton({ Key? key, required this.text, required this.onPressed, this.isLoading = false, }) : super(key: key);
@override Widget build(BuildContext context) { return ElevatedButton( onPressed: isLoading ? null : onPressed, child: isLoading ? const CircularProgressIndicator(color: Colors.white) : Text(text), ); }}
기능 모듈: 상품
Section titled “기능 모듈: 상품”상품 기능 모듈은 상품 목록, 상세 정보, 검색 등의 기능을 담당합니다:
library feature_products;
export 'src/presentation/pages/product_list_page.dart';export 'src/presentation/pages/product_detail_page.dart';export 'src/domain/entities/product.dart';export 'src/di/products_module.dart';
import 'package:core/core.dart';import 'package:flutter/material.dart';import 'package:flutter_riverpod/flutter_riverpod.dart';import 'package:ui_kit/ui_kit.dart';import '../providers/products_provider.dart';
class ProductListPage extends ConsumerWidget { const ProductListPage({Key? key}) : super(key: key);
@override Widget build(BuildContext context, WidgetRef ref) { final productsAsync = ref.watch(productsProvider);
return Scaffold( appBar: AppBar(title: const Text('상품 목록')), body: productsAsync.when( data: (products) => GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.7, ), itemCount: products.length, itemBuilder: (context, index) { final product = products[index]; return ProductCard( product: product, onTap: () => Navigator.of(context).pushNamed( '/product/${product.id}', ), ); }, ), loading: () => const Center(child: CircularProgressIndicator()), error: (error, stackTrace) => Center( child: Text('오류가 발생했습니다: $error'), ), ), ); }}
통합: 앱 모듈
Section titled “통합: 앱 모듈”앱 모듈은 모든 기능 모듈을 통합하고 앱의 진입점 역할을 합니다:
import 'package:core/core.dart';import 'package:feature_auth/feature_auth.dart';import 'package:feature_products/feature_products.dart';import 'package:feature_cart/feature_cart.dart';import 'package:feature_checkout/feature_checkout.dart';import 'package:feature_profile/feature_profile.dart';import 'package:flutter/material.dart';import 'app.dart';
void main() { // 앱 설정 초기화 AppConfig.initialize( env: Environment.dev, apiUrl: 'https://dev-api.example.com', );
// 의존성 주입 설정 setupServiceLocator();
// 모듈별 의존성 초기화 AuthModule.init(); ProductsModule.init(); CartModule.init(); CheckoutModule.init(); ProfileModule.init();
runApp(const MyApp());}
import 'package:core/core.dart';import 'package:ui_kit/ui_kit.dart';import 'package:flutter/material.dart';import 'package:flutter_riverpod/flutter_riverpod.dart';
class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return ProviderScope( child: MaterialApp.router( title: 'E-Commerce App', theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: ThemeMode.system, routerConfig: appRouter, ), ); }}
멀티 모듈 아키텍처의 과제와 해결책
Section titled “멀티 모듈 아키텍처의 과제와 해결책”1. 모듈 간 의존성 순환 문제
Section titled “1. 모듈 간 의존성 순환 문제”모듈 간 의존성이 순환 구조를 형성하면 복잡성이 증가하고 빌드 문제가 발생할 수 있습니다.
- 의존성 방향을 엄격하게 설정하고 준수합니다.
- 필요한 경우 이벤트 기반 통신을 사용합니다.
- 공통 코드를 코어 모듈로 이동시킵니다.
// 이벤트 기반 통신 예시 (core 모듈)class AppEvent { // 이벤트 정의}
class EventBus { static final EventBus _instance = EventBus._internal(); factory EventBus() => _instance; EventBus._internal();
final _eventController = StreamController<AppEvent>.broadcast();
Stream<AppEvent> get events => _eventController.stream;
void fire(AppEvent event) { _eventController.add(event); }
void dispose() { _eventController.close(); }}
2. 빌드 시간 및 성능 문제
Section titled “2. 빌드 시간 및 성능 문제”여러 모듈이 많은 의존성을 가지면 빌드 시간이 길어질 수 있습니다.
- 필요한 의존성만 추가합니다.
- melos와 같은 도구를 사용하여 빌드 프로세스를 최적화합니다.
- 의존성 트리를 주기적으로 검토하고 정리합니다.
3. 디버깅 복잡성
Section titled “3. 디버깅 복잡성”여러 모듈에 걸친 문제를 디버깅하기 어려울 수 있습니다.
- 각 모듈에 적절한 로깅을 추가합니다.
- 테스트 커버리지를 높게 유지합니다.
- 통합 테스트를 작성하여 모듈 간 상호작용을 검증합니다.
언제 멀티 모듈 아키텍처를 적용해야 하는가?
Section titled “언제 멀티 모듈 아키텍처를 적용해야 하는가?”다음과 같은 경우에 멀티 모듈 아키텍처를 고려해 볼 수 있습니다:
- 대규모 프로젝트: 코드베이스가 크고 복잡한 경우
- 여러 팀이 협업: 다수의 개발자가 동시에 작업하는 경우
- 빌드 시간 문제: 빌드 시간이 과도하게 길어지는 경우
- 코드 재사용 요구: 여러 프로젝트에서 코드를 재사용해야 하는 경우
- 독립 배포 필요: 특정 모듈만 독립적으로 업데이트해야 하는 경우
그러나 다음과 같은 경우에는 적용을 신중하게 검토해야 합니다:
- 소규모 프로젝트: 간단한 앱은 오히려 복잡성만 증가할 수 있습니다.
- 작은 팀: 소수의 개발자만 있는 경우 이점이 제한적일 수 있습니다.
- 빠른 프로토타이핑: 빠르게 개발해야 하는 경우 초기 설정에 시간을 투자하기 어려울 수 있습니다.
멀티 모듈 아키텍처는 대규모 Flutter 프로젝트의 복잡성을 관리하고 개발 효율성을 높이는 강력한 방법입니다. 모듈 간 명확한 경계와 잘 정의된 인터페이스를 통해 코드베이스의 확장성, 유지보수성, 테스트 용이성을 개선할 수 있습니다.
그러나 모든 프로젝트에 적합한 것은 아니며, 특히 작은 프로젝트나 초기 단계에서는 오버엔지니어링이 될 수 있습니다. 프로젝트의 규모, 팀 구성, 미래 확장 계획 등을 고려하여 적용 여부를 결정해야 합니다.
멀티 모듈 아키텍처는 초기 설정의 복잡성이 있지만, 장기적으로는 개발 효율성과 코드 품질의 향상을 가져올 수 있습니다. 특히 여러 팀이 협업하는 대규모 프로젝트에서 그 이점이 더욱 두드러집니다.