Skip to content

멀티 모듈 아키텍처

대규모 Flutter 프로젝트에서는 코드베이스가 커짐에 따라 빌드 시간 증가, 유지보수 복잡성, 팀 협업 어려움 등의 문제가 발생할 수 있습니다. 이러한 문제를 해결하기 위한 방법 중 하나가 멀티 모듈 아키텍처입니다. 이 문서에서는 Flutter에서 멀티 모듈 아키텍처를 구현하는 방법, 장단점, 그리고 실제 적용 사례를 살펴보겠습니다.

멀티 모듈 아키텍처는 하나의 큰 애플리케이션을 여러 개의 독립적인 모듈(또는 패키지)로 분리하는 접근 방식입니다. 각 모듈은 특정 기능이나 도메인에 초점을 맞추고, 명확하게 정의된 인터페이스를 통해 다른 모듈과 통신합니다.

  1. 빌드 시간 단축: 전체 앱이 아닌 변경된 모듈만 재빌드하여 개발 속도를 높일 수 있습니다.
  2. 관심사 분리: 각 모듈은 특정 기능에 집중하여 코드 이해와 유지보수가 용이해집니다.
  3. 팀 협업 개선: 각 팀이 독립적인 모듈에서 작업하여 코드 충돌을 줄일 수 있습니다.
  4. 코드 재사용성: 모듈을 다른 프로젝트에서 재사용할 수 있습니다.
  5. 테스트 용이성: 모듈별로 독립적인 테스트가 가능합니다.
  6. 의존성 명확화: 모듈 간 의존성이 명시적으로 정의되어 구조가 명확해집니다.
  1. 초기 설정 복잡성: 프로젝트 구조 설정이 복잡하고 시간이 소요됩니다.
  2. 의존성 관리 어려움: 모듈 간 의존성을 올바르게 관리해야 합니다.
  3. 통합 테스트 복잡성: 모듈 간 통합 테스트가 더 복잡해질 수 있습니다.
  4. 학습 곡선: 팀원들이 모듈 구조에 적응하는 데 시간이 필요합니다.
  5. 오버엔지니어링 위험: 작은 프로젝트에서는 불필요한 복잡성이 추가될 수 있습니다.

Flutter에서의 멀티 모듈 구현 방법

Section titled “Flutter에서의 멀티 모듈 구현 방법”

Flutter에서 멀티 모듈 아키텍처를 구현하는 여러 방법이 있습니다. 가장 일반적인 방법은 다음과 같습니다:

같은 저장소 내에서 여러 패키지를 관리하는 방법입니다.

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 (옵션)

app/pubspec.yaml:

name: my_app
description: Main application module
version: 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: core
description: Core module with shared functionality
version: 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_auth
description: Authentication feature module
version: 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

melos는 Dart/Flutter 프로젝트에서 모노레포를 관리하기 위한 도구로, 여러 패키지를 효율적으로 관리할 수 있게 해줍니다.

  1. melos 설치:
Terminal window
dart pub global activate melos
  1. 프로젝트 루트에 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
  1. 사용 예시:
Terminal window
# 모든 패키지에서 flutter pub get 실행
melos pub_get
# 모든 패키지에서 flutter analyze 실행
melos analyze
# 모든 패키지에서 flutter test 실행
melos test

멀티 모듈 아키텍처는 Flutter Flavors와 결합하여 다양한 앱 버전(개발, 스테이징, 프로덕션 등)을 관리할 수 있습니다:

app/lib/main_dev.dart
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());
}
app/lib/main_prod.dart
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());
}

멀티 모듈 아키텍처에서 일반적으로 사용되는 모듈 유형은 다음과 같습니다:

  1. 앱 모듈(App Module): 애플리케이션의 진입점이며 다른 모든 모듈을 통합합니다.
  2. 코어 모듈(Core Module): 공통 기능, 유틸리티, 핵심 컴포넌트 등을 포함합니다.
  3. 기능 모듈(Feature Module): 특정 기능 또는 도메인에 집중한 모듈입니다.
  4. UI 모듈(UI Module): 재사용 가능한 UI 컴포넌트를 포함합니다.
  5. 데이터 모듈(Data Module): 데이터 액세스 로직을 담당합니다.

모듈 간 의존성 방향은 명확하게 설정해야 합니다:

  • 코어 모듈: 다른 모든 모듈의 기반이 되며, 다른 모듈에 의존하지 않습니다.
  • 기능 모듈: 코어 모듈과 UI 모듈에 의존할 수 있지만, 다른 기능 모듈에 직접 의존하지 않는 것이 좋습니다.
  • 앱 모듈: 모든 모듈을 통합하고 의존합니다.

모듈 간 통신은 다음과 같은 방법으로 이루어질 수 있습니다:

  1. 인터페이스 기반 통신: 모듈은 인터페이스를 정의하고 구현체는 필요한 모듈에서 제공합니다.
  2. 이벤트 기반 통신: 이벤트 버스나 스트림을 통해 모듈 간 이벤트를 전달합니다.
  3. 의존성 주입: Riverpod이나 GetIt 같은 도구를 사용하여 모듈 간 의존성을 관리합니다.
core/lib/src/auth/auth_service.dart
abstract class AuthService {
Future<User?> getCurrentUser();
Future<User> signIn(String email, String password);
Future<void> signOut();
}
// feature_auth/lib/src/services/firebase_auth_service.dart
class 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.dart
void setupServiceLocator() {
GetIt.I.registerSingleton<AuthService>(FirebaseAuthService());
}

각 모듈 내부는 클린 아키텍처나 MVVM 같은 아키텍처 패턴을 따를 수 있습니다.

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를 정의해야 합니다. 모듈 내부 구현은 숨기고 필요한 기능만 노출하는 것이 좋습니다.

feature_auth/lib/feature_auth.dart
library feature_auth;
// 공개 API
export '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';
feature_auth/lib/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()));
}
}

실제 전자상거래 앱에 멀티 모듈 아키텍처를 적용해보겠습니다.

ecommerce_app/
├── app/ # 메인 앱 모듈
├── packages/
│ ├── core/ # 핵심 기능 모듈
│ ├── ui_kit/ # UI 컴포넌트 모듈
│ ├── feature_auth/ # 인증 기능 모듈
│ ├── feature_products/ # 상품 기능 모듈
│ ├── feature_cart/ # 장바구니 기능 모듈
│ ├── feature_checkout/ # 결제 기능 모듈
│ └── feature_profile/ # 프로필 기능 모듈
└── melos.yaml

코어 모듈은 다른 모든 모듈에서 사용하는 공통 기능을 포함합니다:

core/lib/core.dart
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';
core/lib/src/config/app_config.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 키트 모듈은 앱 전체에서 사용되는 공통 UI 컴포넌트를 포함합니다:

ui_kit/lib/ui_kit.dart
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';
ui_kit/lib/src/buttons/primary_button.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),
);
}
}

상품 기능 모듈은 상품 목록, 상세 정보, 검색 등의 기능을 담당합니다:

feature_products/lib/feature_products.dart
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';
feature_products/lib/src/presentation/pages/product_list_page.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'),
),
),
);
}
}

앱 모듈은 모든 기능 모듈을 통합하고 앱의 진입점 역할을 합니다:

app/lib/main_dev.dart
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());
}
app/lib/app.dart
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 “멀티 모듈 아키텍처의 과제와 해결책”

모듈 간 의존성이 순환 구조를 형성하면 복잡성이 증가하고 빌드 문제가 발생할 수 있습니다.

  • 의존성 방향을 엄격하게 설정하고 준수합니다.
  • 필요한 경우 이벤트 기반 통신을 사용합니다.
  • 공통 코드를 코어 모듈로 이동시킵니다.
// 이벤트 기반 통신 예시 (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();
}
}

여러 모듈이 많은 의존성을 가지면 빌드 시간이 길어질 수 있습니다.

  • 필요한 의존성만 추가합니다.
  • melos와 같은 도구를 사용하여 빌드 프로세스를 최적화합니다.
  • 의존성 트리를 주기적으로 검토하고 정리합니다.

여러 모듈에 걸친 문제를 디버깅하기 어려울 수 있습니다.

  • 각 모듈에 적절한 로깅을 추가합니다.
  • 테스트 커버리지를 높게 유지합니다.
  • 통합 테스트를 작성하여 모듈 간 상호작용을 검증합니다.

언제 멀티 모듈 아키텍처를 적용해야 하는가?

Section titled “언제 멀티 모듈 아키텍처를 적용해야 하는가?”

다음과 같은 경우에 멀티 모듈 아키텍처를 고려해 볼 수 있습니다:

  1. 대규모 프로젝트: 코드베이스가 크고 복잡한 경우
  2. 여러 팀이 협업: 다수의 개발자가 동시에 작업하는 경우
  3. 빌드 시간 문제: 빌드 시간이 과도하게 길어지는 경우
  4. 코드 재사용 요구: 여러 프로젝트에서 코드를 재사용해야 하는 경우
  5. 독립 배포 필요: 특정 모듈만 독립적으로 업데이트해야 하는 경우

그러나 다음과 같은 경우에는 적용을 신중하게 검토해야 합니다:

  1. 소규모 프로젝트: 간단한 앱은 오히려 복잡성만 증가할 수 있습니다.
  2. 작은 팀: 소수의 개발자만 있는 경우 이점이 제한적일 수 있습니다.
  3. 빠른 프로토타이핑: 빠르게 개발해야 하는 경우 초기 설정에 시간을 투자하기 어려울 수 있습니다.

멀티 모듈 아키텍처는 대규모 Flutter 프로젝트의 복잡성을 관리하고 개발 효율성을 높이는 강력한 방법입니다. 모듈 간 명확한 경계와 잘 정의된 인터페이스를 통해 코드베이스의 확장성, 유지보수성, 테스트 용이성을 개선할 수 있습니다.

그러나 모든 프로젝트에 적합한 것은 아니며, 특히 작은 프로젝트나 초기 단계에서는 오버엔지니어링이 될 수 있습니다. 프로젝트의 규모, 팀 구성, 미래 확장 계획 등을 고려하여 적용 여부를 결정해야 합니다.

멀티 모듈 아키텍처는 초기 설정의 복잡성이 있지만, 장기적으로는 개발 효율성과 코드 품질의 향상을 가져올 수 있습니다. 특히 여러 팀이 협업하는 대규모 프로젝트에서 그 이점이 더욱 두드러집니다.