[Android] 멀티 모듈 프로젝트 구성하고 hilt 적용하기
지금까지 app 모듈 안에 data, domain, presentation 폴더를 만들고 그 안에서 작업해 온 사람도 있을 것이다.
그러나 이렇게 하면 삐끗하면 클린 아키텍처를 어길 수 있으니, 실제로 저 이름을 가진 모듈들을 만드는 방법을 포스팅한다.
또한 이 방법이 절대 정답은 아니다. 본인이 설계한 앱에 맞춰 core 모듈을 추가할 수도 있다. 여러 레퍼런스를 찾아보고 필요한 모듈들을 알맞게 추가해 사용하자. 이 포스팅에선 앞서 말한대로 data, domain, presentation 모듈을 만들고 앱을 실행하는 것까지만 확인한다. 안드로이드 디벨로퍼에서 권장하는 앱 아키텍처가 저 3가지 모듈을 바탕으로 하기 때문인 것도 있다.
아래는 사용하는 안드로이드 스튜디오 버전이다.
먼저 File > New > New Module 순으로 클릭한다.
그러면 만들 모듈을 설정할 수 있는 팝업이 표시된다.
모듈을 만들기 전에, 생성할 모듈 별 특징을 나열하면 아래와 같다.
- data : 레트로핏으로 외부 데이터 소스로부터 데이터를 땡겨 오거나, Room 같은 내부 데이터 소스로부터 데이터를 가져오는 모듈이다. domain을 알고 있어야 필요 시 mapper를 추가하는 등 추가 작업이 가능하므로 data 모듈은 domain 모듈을 알아야 한다
- domain : 순수 자바 / 코틀린 코드들로 구성돼야 하는 옵셔널 모듈이다. 안드로이드 관련 의존성들이 존재해선 안 된다
- presentation : domain 모듈의 유즈케이스를 통해 UI에 뿌릴 데이터를 가져와야 하기 때문에 domain 모듈을 알아야 한다. 이를 통해 data 모듈에 간접적으로 의존하게 되면서 의존성 역전 원칙(DIP)을 지킬 수 있게 된다.
그리고 기존에 있던 app 모듈은 모든 모듈을 알고 있어야 한다.
hilt 같은 DI 라이브러리를 쓸 경우 의존성을 한 곳에서 관리할 필요가 있다. 레트로핏, Room 설정 같은 전역적으로 유지돼야 하는 설정들도 한 곳에서 관리될 필요가 있다. 이걸 수행하기에 딱 좋은 모듈이 앱의 진입점인 app 모듈이기 때문에 app 모듈은 data, domain, presentation 모듈들을 알고 있어야 한다.
위 내용에 근거해 각 모듈은 아래 템플릿을 선택한다.
- data, presentation : Android Library
- domain : Java or Kotlin Library
Android Library를 선택하면 아래와 같이 화면이 바뀐다.
모듈 이름에 :data를 입력하면 저렇게 바뀐다. 패키지명은 자동으로 바뀌니 신경쓰지 않는다.
그 외에 minSdk나 다른 설정할 부분이 있으면 설정한 후 Finish를 눌러 적용해준다.
그러면 잠시 싱크가 진행된다. 싱크가 완료되면 settings.gradle.kts 파일에 추가한 data 모듈이 표시된다.
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "TestApplication"
include(":app")
include(":data")
마찬가지로 presentation 모듈도 추가한다. domain 모듈은 Java or Kotlin Library를 선택해서 진행한다.
클래스명은 신경쓰지 않는다. 어차피 없애거나 이름을 바꿔서 다르게 사용할 클래스다.
모두 완료됐다면 왼쪽의 폴더 리스트가 아래처럼 보일 것이다.
이제 data, presentation이 domain 모듈을 알도록 설정해야 한다.
별 건 없고 data, presentation의 app gradle에 아래 문구를 추가하면 끝이다.
implementation(project(":domain"))
그리고 앞서 말했듯 app 모듈은 3가지 모듈을 모두 알고 있어야 한다.
따라서 app 모듈의 app gradle에 아래 3가지 문구를 추가한다.
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":presentation"))
Sync now를 눌러서 싱크를 한 번 진행한다. 이쯤에서 실행 버튼을 눌러 혹시 생성한(또는 기존) 프로젝트가 변함없이 정상적으로 실행되는지 확인한다.
만약 에러가 발생한다면 클린 -> 리빌드 프로젝트, Invalid Caches를 눌러 캐시들을 지우고 재실행한 다음 실행한다.
방금 프로젝트를 만들었다면 한 가지 수정할 게 있다. 현재 메인 액티비티는 app 모듈에 위치해 있어서 이를 presentation 모듈로 옮겨야 한다.
그리고 presentation 모듈에는 res 폴더가 없어서 만들어야 한다. res 폴더를 만들려면 presentation 모듈을 우클릭한 후 아래 사진처럼 이동해서 생성한다.
그리고 이후 나타나는 화면에서 바로 OK를 누른다.
이렇게 하면 res 폴더 안에 values 폴더가 생성된다. drawable, mipmap, xml 폴더는 복사해서 붙여넣기로 가져와도 상관없다.
그리고 app 모듈의 매니페스트에서도 MainActivity 태그를 제거한다.
아니면 MainActivity와 관련 부분을 제거한 다음 새로 MainActivity를 만들어도 된다. 본인에게 편한 방식대로 수정한 다음 앱을 빌드해서 성공적으로 작동하는지 확인한다.
추가로 hilt도 적용해본다. libs.versions.toml에 hilt 의존성과 hilt를 사용하기 위해 필요한 kapt 의존성을 추가한다.
[versions]
hilt = "2.48.1"
[libraries]
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
[plugins]
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt"}
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version = "1.8.10"}
그리고 프로젝트 gradle에도 추가한다.
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.kotlin.kapt) apply false
}
다음으로 app, presentation 모듈에 hilt 의존성을 추가한다.
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.jetbrains.kotlin.android)
id("kotlin-kapt")
alias(libs.plugins.hilt)
}
...
implementation(libs.hilt)
kapt(libs.hilt.compiler)
app 모듈에 Application을 상속하는 클래스를 만들고 @HiltAndroidApp을 작성한다.
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class App: Application() {
}
이후 presentation 모듈로 옮긴 메인 액티비티에 @AndroidEntryPoint를 추가해서 hilt를 통해 의존성을 주입받을 수 있게 한다.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.presentation.main.ui.theme.TestApplicationTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TestApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "MainActivity",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
TestApplicationTheme {
Greeting("Android")
}
}
이제 실행해서 어떤지 확인한다. 내 핸드폰과 에뮬레이터에선 이상없이 작동했다.
이 화면까지 봤다면 멀티 모듈 설정과 hilt 기본 설정까지 끝났다.