learn-flutter/content/docs/part9/multi-module.md
ChangJoo Park(박창주) 900b0ab68b update markdown
2025-05-14 09:34:13 +09:00

20 KiB

멀티 모듈 아키텍처

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

멀티 모듈 아키텍처란?

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

graph TD
    A[앱 모듈] --> B[코어 모듈]
    A --> C[기능 모듈 1]
    A --> D[기능 모듈 2]
    A --> E[기능 모듈 3]
    C --> B
    D --> B
    E --> B
    style A fill:#f9d5e5
    style B fill:#d5e5f9

멀티 모듈의 장점

  1. 빌드 시간 단축: 전체 앱이 아닌 변경된 모듈만 재빌드하여 개발 속도를 높일 수 있습니다.
  2. 관심사 분리: 각 모듈은 특정 기능에 집중하여 코드 이해와 유지보수가 용이해집니다.
  3. 팀 협업 개선: 각 팀이 독립적인 모듈에서 작업하여 코드 충돌을 줄일 수 있습니다.
  4. 코드 재사용성: 모듈을 다른 프로젝트에서 재사용할 수 있습니다.
  5. 테스트 용이성: 모듈별로 독립적인 테스트가 가능합니다.
  6. 의존성 명확화: 모듈 간 의존성이 명시적으로 정의되어 구조가 명확해집니다.

멀티 모듈의 단점

  1. 초기 설정 복잡성: 프로젝트 구조 설정이 복잡하고 시간이 소요됩니다.
  2. 의존성 관리 어려움: 모듈 간 의존성을 올바르게 관리해야 합니다.
  3. 통합 테스트 복잡성: 모듈 간 통합 테스트가 더 복잡해질 수 있습니다.
  4. 학습 곡선: 팀원들이 모듈 구조에 적응하는 데 시간이 필요합니다.
  5. 오버엔지니어링 위험: 작은 프로젝트에서는 불필요한 복잡성이 추가될 수 있습니다.

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

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

1. 로컬 패키지 사용

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

프로젝트 구조 예시

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 설정

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

2. melos를 사용한 모노레포

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

설치 및 설정

  1. melos 설치:
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. 사용 예시:
# 모든 패키지에서 flutter pub get 실행
melos pub_get

# 모든 패키지에서 flutter analyze 실행
melos analyze

# 모든 패키지에서 flutter test 실행
melos test

3. Flutter Flavors와 조합

멀티 모듈 아키텍처는 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): 데이터 액세스 로직을 담당합니다.

의존성 방향

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

graph LR
    A[앱 모듈] --> B[기능 모듈]
    A --> C[UI 모듈]
    B --> D[코어 모듈]
    B --> C
    C --> D
    style D fill:#d5e5f9
  • 코어 모듈: 다른 모든 모듈의 기반이 되며, 다른 모듈에 의존하지 않습니다.
  • 기능 모듈: 코어 모듈과 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 설계

각 모듈은 명확한 공개 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 컴포넌트를 포함합니다:

// 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,
      ),
    );
  }
}

멀티 모듈 아키텍처의 과제와 해결책

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. 빌드 시간 및 성능 문제

문제

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

해결책

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

3. 디버깅 복잡성

문제

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

해결책

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

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

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

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

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

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

결론

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

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

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