개발일기

멀티 모듈 + Clean Architecture로 Compose 앱 설계하기

뱅우 2025. 9. 19. 09:36
반응형
멀티 모듈 + Clean Architecture로 Compose 앱 설계하기

대형 Compose 앱을 설계할 때는 모듈 분리(멀티 모듈)Clean Architecture를 결합하면 유지보수성과 빌드 속도, 테스트 용이성이 크게 개선됩니다. 이번 글에서는 권장 모듈 구조, Gradle 설정 팁, 의존성 주입(예: Hilt) 적용 방식, 모듈 간 경계 규칙과 테스트 전략까지 실무 관점에서 정리합니다.


1. 왜 멀티 모듈 + Clean Architecture인가?

  • 빌드 시간 단축: 변경 범위를 작은 모듈로 제한하면 빌드/증분 컴파일이 빨라집니다.
  • 명확한 책임 분리: presentation / domain / data 레이어가 물리적으로 분리되어 코드 품질 향상.
  • 재사용성 증가: 공통 UI, 유틸, 네트워크 모듈을 여러 앱에서 공유 가능.
  • 테스트 편의성: 작은 단위 모듈을 독립적으로 단위 테스트/통합 테스트 수행.

2. 권장 모듈 구조

일반적인 모듈 구조 예시는 아래와 같습니다.


/app                    // Android application 모듈 (Compose 호스트, DI 초기화)
 /build.gradle.kts
/domain                 // 순수 비즈니스 로직 (플랫폼 비의존)
 /build.gradle.kts
 /src/main/java/...     // UseCase, Entity, Repository 인터페이스
/data                   // 구현체 (Retrofit, Room 등) - domain의 Repository 인터페이스 구현
 /build.gradle.kts
 /src/main/java/...     // ApiService, Dao, RepositoryImpl
/ui-common              // 재사용 가능한 Compose 컴포넌트, 테마, 스타일
 /build.gradle.kts
 /src/main/java/...     // Buttons, ListItems, Theme
:feature-         // 기능별(Feature) 모듈 - presentation + viewModel
 /build.gradle.kts
 /src/main/java/...     // Compose 화면, ViewModel (domain에 의존)

3. 의존성 규칙 (권장)

  1. app → feature 모듈, ui-common
  2. feature → domain, ui-common
  3. data → domain (구현체 제공)
  4. domain은 다른 모듈에 의존하지 않음 (플랫폼/프레임워크 독립)

이 규칙을 지키면 순환 의존성을 피하고 각 모듈의 책임이 명확해집니다.


4. Gradle 설정 (Kotlin DSL 예)

각 모듈의 build.gradle.kts에서 api/implementation 의존성을 명확히 관리하세요.

plugins {
    id("com.android.library")
    kotlin("android")
}

android {
    compileSdk = 34
    // ...
}

dependencies {
    api(project(":domain"))        // domain을 외부에 노출할 필요가 있는 경우만 api
    implementation(project(":ui-common"))
    implementation("androidx.compose.ui:ui:1.5.0")
    // feature 모듈은 domain 인터페이스에 의존
}

5. DI (Hilt) 적용 요령

Hilt를 사용하면 모듈 간 구현 바인딩이 쉬워집니다. 중요한 점은 바인딩을 data 모듈에서만 수행하고, domain은 인터페이스만 제공하는 것입니다.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    fun provideApiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java)
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository  // UserRepository는 domain에 정의된 인터페이스
}

그리고 App 모듈은 Hilt Android Application을 선언해서 DI를 초기화합니다.


6. Compose + Navigation + 모듈 라우팅

각 Feature 모듈은 자체적으로 NavGraph를 제공하고 App 모듈에서 조합하는 방식이 좋습니다.

// feature-profile 모듈
@Composable
fun ProfileNavGraph(navController: NavHostController) {
    NavHost(navController, startDestination = "profile/main") {
        composable("profile/main") { ProfileScreen() }
    }
}

// app 모듈에서 통합
NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen() }
    navigation(startDestination = "profile/main", route = "profile") {
        // feature 모듈에서 NavGraph 제공하는 helper를 호출하거나, route만 연결
    }
}

7. 테스트 전략

  • domain 유닛 테스트: UseCase, 도메인 로직 검증 (Mockito/MockK / kotest)
  • data 단위 테스트: MockWebServer로 API 응답 시뮬레이션
  • feature 통합 테스트: ViewModel + fake repository 로 UI 상태 검증
  • CI: 변경된 모듈만 테스트/빌드하도록 설정하면 효율적

8. 빌드 성능 최적화 팁

  • 불필요한 annotation processor 사용 줄이기 (kapt 대상 최소화)
  • 공용 의존성은 버전 충돌을 피하기 위해 libs.versions.toml로 관리
  • gradle.properties에 org.gradle.parallel=true, kotlin.incremental=true 설정
  • 큰 모듈은 기능별로 쪼개기 (feature 모듈 분리)

9. 장단점 요약

장점

  • 유지보수성과 협업 생산성 향상
  • 테스트가 쉬워지고 빌드 시간이 향상될 수 있음
  • 모듈 재사용(라이브러리화) 가능

단점 / 주의사항

  • 초기 설계 난이도 증가 — 모듈 경계 설계가 중요
  • 프로젝트가 작은 경우 오버엔지니어링이 될 수 있음
  • 모듈간 버전/의존성 관리가 추가로 필요

10. 시작 체크리스트

  1. 도메인/데이터/프레젠테이션 책임을 문서화
  2. 공통 UI 모듈(ui-common) 설계 (테마, 컴포넌트)
  3. DI 바인딩은 data에서만 구현, domain은 인터페이스 유지
  4. Gradle 의존성 규칙을 린트로 강제 (detekt, gradle-lint-plugin 등)
  5. CI 파이프라인에서 모듈 단위 빌드/테스트 단계 구성

마무리: 멀티 모듈과 Clean Architecture를 적용하면 대규모 Compose 앱의 확장성과 유지보수성이 크게 개선됩니다. 처음에는 설계 비용이 들지만, 팀과 코드베이스가 커질수록 그 이점이 명확해집니다. 원하시면 이 구조로 실제 샘플 프로젝트의 Gradle 설정과 디렉터리 생성 스크립트도 작성해드릴게요.


반응형