안녕하세요. 오늘은 프로젝트를 본격적으로 구현하기 전에 머릿속 개념들을 정리하기 위해 글을 적어보겠습니다.

이번 글은 멀티모듈에 대한 글입니다.
멀티모듈에 본격적으로 들어가기에 앞서, 우선 클린 아키텍처에 대해 먼저 이야기해보겠습니다.
클린 아키텍처(Clean Architecture)
모두 이미 알고 있는 것과 같이 가장 중요하게 생각하는 것은 의존성 규칙입니다.
의존성 규칙
간단하게 이야기해볼게요.
우선 의존성이란 무엇인가?
의존성이란
소스 코드 의존성(compile-time dependency)
A 파일이 B타입/함수를 import하고 사용하면 A→B로 의존합니다.
빌드 시점에 결정되고, 의존 화살표가 그려져요.
런타임(run-time dependency)
실제 실행 때 어떤 구현체가 주입되어 돌아가는지에 대한 의존입니다.
클린 아키텍처에서는 소스 코드 의존성의 방향이 특히 중요합니다.
의존성 방향은 항상 바깥 계층(Presentation, Data)에서 안쪽 계층(Domain)으로만 향해야 합니다.

즉, Domain 레이어는 다른 어떤 레이어에 대해서도 알지 못합니다.
한 마디로 바깥(Presentation, Data)에서는 안쪽(Domain)을 import해도 되지만, 안쪽에서는 다른 어떤 것도 import해서는 안된다는 뜻이죠.
계층(Layer)
각 계층은 크게 3개의 핵심 계층으로 나뉩니다.
Presentation Layer
UI(Activity, Fragment, ViewModel)와 관련된 모든 로직을 포함합니다.
안드로이드 프레임워크에 대한 강한 의존성을 가져요.
Domain Layer
핵심 비즈니스 로직(UseCases)과 규칙을 포함합니다.
이 계층의 가장 중요한 특징은 플랫폼 독립성으로, 안드로이드 프레임워크에 대한 의존성이 전혀 없는 순수 Kotlin/Java 모듈로 구성되어야 한다는 점이에요.
Data Layer
데이터의 출처(데이터베이스 등)를 관리하고, Domain 레이어에 정의된 Repository 인터페이스의 구현체를 포함해요.
더 자세한 내용은 아래 이미지를 참고해주세요.

저희는 사실 UI와 Presentation까지의 분리에 대해 고민했었지만, xml이 아닌 compose로 가기로 했기 때문에 UI가 많이 가벼워질 것이라 굳이 둘을 분리해서까지 가지는 않기로 했습니다.
따라서 UI와 Presentation을 합쳐 Feature 모듈로 가져가기로 했죠.
이렇게 클린 아키텍처에 대하여 간단히 핵심 내용만 알아보았습니다.
그럼 이제 본격적으로 멀티모듈에 대해 이야기해볼게요.
멀티모듈(Multi-module)
클린 아키텍처의 핵심은 의존성 규칙이었죠.
하지만 이 규칙을 프로젝트 구조 차원에서 물리적으로 강제하려면 어떻게 해야 할까요?
바로 멀티모듈 구조를 도입하는 것입니다.
단일 모듈 안에 모든 레이어를 넣어두면, 실수로 잘못된 방향의 의존성이 생겨도 컴파일이 통과해버릴 수 있어요.
반면 모듈을 나누면 어느 모듈이 어느 모듈을 import할 수 있는지가 Gradle 레벨에서 명확해집니다.
(여기에 또 하나 큰 장점이 있어요.
바로 증분 빌드가 가능해진다는 점인데요.
모듈이 하나뿐이라면 코드 한 줄만 수정해도 전체 프로젝트를 다시 빌드해야 하지만, 멀티모듈로 나누면 수정된 모듈과 그와 관련된 모듈만 빌드하면 됩니다.
덕분에 빌드 시간이 줄어들고, 개발 효율도 훨씬 높아져요.
글 흐름과는 어울리지 않아 괄호 안에 적어봅니다. 그냥 쓱 읽고 넘어가 주세요.)
모듈 종류와 역할
저희 팀은 클린 아키텍처의 3계층 구조(Presentation, Domain, Data)를 그대로 반영하되, 안드로이드 프로젝트 특성을 고려해 다음과 같이 나누기로 했습니다.
App 모듈(Android Module)
최종적으로 APK를 빌드하는 진입점 역할을 가집니다.
Application 클래스와 Hilt 의존성 그래프의 시작점이 이 모듈에 위치하죠.
각 Feature 모듈, Data 모듈, Domain 모듈을 하나로 묶는 최상위 조립자 역할이라고 생각하면 됩니다.

Feature 모듈(Android Module)
화면 단위 기능을 담당해요.
예를 들어 로그인, 홈, 상세 페이지 같은 화면이 각각 하나의 Feature 모듈이 되는거죠.
안드로이드 의존성을 가지기 때문에 Android Module로 생성합니다.
클린 아키텍처의 관점에서는 Presentation Layer에 해당합니다.
Domain 모듈(Kotlin Module)
UseCase, Repository 인터페이스, Domain Model 등 핵심 비즈니스 로직이 들어갑니다.
안드로이드 SDK에 의존하지 않는 순수 Kotlin 모듈로 구성하여 빌드 속도를 높이고 재사용성도 확보합니다.
클린 아키텍처의 Domain Layer가 그대로 들어오는 모듈입니다.
Data 모듈(Android Module)
Retrofit, Room 같은 데이터 소스 구현체가 들어갑니다.
Domain 모듈에 정의된 Repository 인터페이스의 실제 구현체를 작성합니다.
즉, 클린 아키텍처의 Data Layer를 담당합니다.
공통 모듈(Core Module)
여러 모듈에서 재사용될 UI 컴포넌트, 유틸 클래스, 확장 함수를 모아둡니다.
중복 코드를 줄이고 유지보수를 쉽게 하기 위해 별도로 분리했습니다.
이렇게 멀티모듈을 도입함으로써 클린 아키텍처의 의존성 규칙을 프로젝트 구조 레벨에서 강제할 수 있게 되었어요.
하지만 또 다른 문제가 하나 남아있습니다.
모듈 간 객체를 어떻게 주입하고 공유할 것인가?
의존성 주입(Dependency Injection, DI)
멀티모듈 구조에서는 모듈 간에 필요한 객체를 서로 주고받아야 합니다.
예를 들어, :app 모듈에서 생성한 OkHttpClient를 :feature:home 모듈의 ViewModel에서 사용하려면 어떻게 해야할까요?
이 문제를 해결하기 위해 의존성 주입(Dependency Injection, DI)을 사용합니다.
안드로이드에서는 Hilt(Dagger 기반)이 사실상 표준처럼 자리잡았어요.
Hilt는 어노테이션을 기반으로 컴파일 타임에 의존성 그래프를 생성하기 때문에 런타임 오버헤드가 적고, 멀티모듈 환경에서도 객체 연결을 깔끔하게 관리할 수 있습니다.
1. Component & Scope
Hilt에는 여러 종류의 컴포넌트(Component)가 있고, 각 컴포넌트는 lifecycle에 맞춰 객체를 보관합니다.
- @Singleton → 앱이 실행되는 동안 객체를 하나만 유지합니다.
- @ActivityRetainedScoped → Activity 재생성(configuration change)에도 살아남는 객체를 제공합니다.
- @ViewModelScoped → ViewModel의 lifecycle 동안만 살아있는 객체를 제공합니다.
즉, 어떤 컴포넌트에 의존성을 설치(Install)하느냐에 따라 객체의 생명주기가 달라집니다.
2. @Module, @Provides, @Binds
의존성을 어떻게 만들고 주입할지를 정의하는 것이 Module입니다.
- @Module → 의존성 제공자를 모아두는 클래스에 붙입니다.
- @Provides → 직접 생성 로직을 정의할 때 사용합니다.
- @Binds → 이미 구현체가 있는 인터페이스를 주입할 때 사용합니다.
3. @InstallIn
Hilt 모듈을 어느 컴포넌트에 설치할지를 명시합니다.
예를 들어, @InstallIn(SingletonComponent::class)라고 하면, 해당 객체는 앱 전체에서 공유되는 싱글톤으로 동작합니다.
이렇게 하면 OkHttpClient 같은 네트워크 객체를 여러 모듈에서 쉽게 공유할 수 있어요.
4. 멀티모듈에서의 DI
멀티모듈에서는 특히 다음 문제가 중요합니다.
- :data 모듈에 정의된 UserRepositoryImpl을
- :domain 모듈이 정의한 UserRepository 인터페이스와 연결해서
- :feature:home 모듈의 ViewModel에서 주입받아야 하는 경우
이때는 app 모듈의 Hilt 바인딩을 통해 인터페이스 ↔ 구현체를 연결해 주면 되겠죠.
Hilt는 멀티모듈 환경에서 객체를 안전하고 효율적으로 공유할 수 있는 도구입니다.
어떤 객체를 어디에 설치할지(@InstallIn), 어떤 라이프사이클로 관리할지(Scope), 어떻게 주입할지(@Provides/@Binds)만 잘 이해하면 됩니다.
네비게이션 전략
멀티모듈을 적용하고, DI(Hilt)로 모듈 간 객체 주입까지 해결했으니 이제 구조가 꽤 단단해졌습니다.
그런데 실제로 앱을 만들다 보면 또 하나 중요한 문제가 남습니다.
바로 화면 간 이동(navigation)을 어떻게 처리할 것인가?입니다.
멀티모듈 환경에서는 feature 모듈이 각각 독립적으로 존재하다 보니, 한 화면에서 다른 화면으로 이동하려 할 때 모듈 간 의존성을 잘못 설정하면 순환 참조(circular dependency)가 쉽게 발생할 수 있습니다.
예를 들어 로그인 화면 모듈에서 홈 화면 모듈로 바로 이동 코드를 작성하면,
결국 홈 모듈도 로그인 모듈을 참조해야 하는 상황이 생길 수 있겠죠.
이렇게 되면 멀티모듈의 장점이 무너져버립니다.
그래서 보통은 다음 두 가지 전략 중 하나를 선택하게 됩니다.

앱 모듈에서 네비게이션 처리
앱 모듈은 모든 feature 모듈을 알고 있는 최상위 조립자이기 때문에, 네비게이션 로직을 전담하도록 하는 방식입니다.
- feature 모듈 → “홈화면으로 갈래”라는 요청만 전달
- app 모듈 → 실제 화면 이동(NavController or Intent) 처리
이렇게 하면 feature 모듈끼리는 서로 전혀 몰라도 되고, 앱 모듈이 모든 네비게이션의 관문이 되어 구조가 단순해져요.
별도 네비게이션 모듈 생성
또 다른 접근은 아예 Navigation 전용 모듈을 만드는 것입니다.
- navigation 모듈 → home으로 이동하는 방법 - 같은 인터페이스 정의
- app 모듈 → 실제 구현 제공
- feature 모듈 → 다른 모듈을 직접 참조하지 않고, 인터페이스만 호출
이 방식은 구조가 더 명확해지겠죠. 하지만 모듈이 하나 더 생기고 DI 구성이 복잡해질 수 있어요.
참고로 말씀드리자면 저희는 앱 모듈에서 네비게이션을 처리하는 방식을 선택했습니다.
멀티모듈 구조를 단순하게 유지할 수 있고, 공식 샘플 프로젝트(Now in Android)에서도 검증된 방법이라 안정적이라고 판단했어요.
이렇게 클린 아키텍처 → 멀티모듈 → DI → 네비게이션 전략까지 차례로 살펴보았습니다.
멀티모듈 구조를 고민 중이신 분들께도 이번 글이 작은 도움이 되었으면 합니다.

감사합니다.
'🤖안드로이드🤖' 카테고리의 다른 글
| [안드로이드] 스레드 기반 작업의 한계와 코루틴의 등장 (4) | 2025.09.08 |
|---|---|
| [안드로이드] 테스트 코드에 대하여 (2) | 2025.08.26 |
| [안드로이드] 프로젝트 구조 뜯어보기 - Android SDK에 대하여 (5) | 2025.08.19 |
| [안드로이드] WindowInsets 조정하기 (2) | 2025.08.10 |
| [안드로이드] 카카오 로그인 세팅, 구현법 (7) | 2025.08.01 |