[Android] Hilt - Android DI Library
Hilt
Hilt는 Google Dagger를 기반으로 만든 안드로이드 전용 DI 라이브러리다.
프로젝트에 Hilt를 적용하는 과정에서 Hilt에 대해 자세하게 알아보려고 한다.
DI 라이브러리를 접해본 사람이라면, Dagger를 많이 들어봤을 것이다.
물론 안드로이드에서도 Dagger를 사용할 수 있고 실제로 가장 많이 사용되던 라이브러리였다.
왜 Hilt를 사용했는지 알아보기 전에 세가지 DI 라이브러리를 비교해보겠다.
안드로이드에서 사용하는 DI(Dependency Injection)는 Dagger, Koin, Hilt 세가지가 있다.
- Dagger
구글에서 개발된 의존성 주입 프레임워크, Java 기반 프로젝트에서 가장 많이 쓰이는 도구이다.
러닝 커브가 크지만 많은 사람들이 사용하는 만큼 안정성이 보장된다. - Koin
Kotlin DSL(Domain Specific Language, 도메인 특화 언어)로 만들어진 의존성 주입 도구이다.
Kotlin 개발 환경에서 사용할 수 있다는 장점이 있지만 DSL을 사용하기 때문에 Runtime에 의존성을 주입한다.
따라서 Compile 시점에서 오류를 확인하기 어렵다. - Hilt
Dagger를 기반으로 만들어진 프레임워크이고 낮은 러닝 커브가 장점이다.
Jetpack 라이브러리에 포함되어 있으며 현재 가장 인기있는 의존성 주입 도구이다.
셋 중에 비교적 최근에 나온 라이브러리이고 Dagger의 장점을 모아 안드로이드 전용으로 만들어진 프레임워크이다.
간단하게 표로 정리하면 아래와 같다.
Dagger | Koin | Hilt | |
러닝커브 | 높음 | 낮음 | 낮음 |
Java | O | X | O |
Kotlin | O | O | O |
에러 검출 시점 | Compile Time | Runtime | Compile Time |
실제 프로젝트 검증 | 많은 앱에서 검증됨 | 많은 앱에서 검증됨 | 정식버전 출시 얼마 안됨 |
Hilt를 사용하는 이유는 구글에서 Jetpack 라이브러리에 포함시키고 구글 자사 샘플앱을 Hilt를 적용할 만큼 Hilt를 권장하고 있다.
물론 프로젝트에 맞는 기술을 선택해서 개발하는 것이 최선이지만 많은 기업들이 Hilt로 마이그레이션 하는 만큼 Hilt의 장점이 많기 때문이다.
Hilt 적용을 위한 프로젝트 설정
Hilt를 사용하기 위해 프로젝트에서 아래와 같은 항목들을 설정해야 한다.
종속 항목 추가
먼저 Hilt-android-gradle-plugin 플러그인을 프로젝트 루트 build.gradle 파일에 추가한다.
plugins {
...
id 'com.google.dagger.hilt.android' version '2.44' apply false
}
Gradle 플러그인을 적용하고 app/build.gradle 파일에 다음 종속 항목을 추가한다.
...
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
}
// Allow references to generated code
kapt {
correctErrorTypes true
}
이제 Hilt를 사용하기 위한 프로젝트 세팅은 끝났다.
Hilt의 주요 기능과 사용 방법
@HiltAndroidApp
Hilt를 사용하기 위해서는 @HiltAndroidApp Annotation이 지정된 Application 클래스를 포함해야 한다.
해당 기능을 통해 Singleton Component가 생성된다.
어플리케이션이 살아있는 동안 Dependency를 제공하는 역할을 하는 어플리케이션 레벨의 Component인 것이다.
@AndroidEntryPoint
Hilt에서 제공하는 주요 기능 중 하나로, 앱 내에서 Hilt가 의존성 주입을 적용하기 위해 필요한 설정을 자동으로 해준다.
해당 Annotation은 다음 Android 클래스에서 사용할 수 있다.
- Application (@HiltAndroidApp)
- ViewModel (@HiltViewModel)
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
위 클래스들에서 Component를 생성할 수 있고 Dependency를 해당 Component에 제공할 수 있게 해준다.
생성된 Component들은 아래와 같은 계층 구조를 가지게 된다.
각 Component들은 그 부모로부터 Dependency를 받을 수 있다.
@Inject
의존성을 주입하기 위한 Annotation으로, 클래스의 생성자, 필드, 메소드에 추가하여 사용한다.
Field Injection
@AndriodEntryPoint를 붙인 클래스와 같이 Component에서 사용할 수 있다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var exampleClass: ExampleClass
...
}
해당 클래스에서 생성하지 않고도 객체를 주입받아 사용할 수 있는 것이다.
여기서 주의할 점은 Hilt에 의해 주입받은 변수 객체는 private를 사용할 수 없다.
Constructor Injection
생성자로 객체를 선언해서 의존성을 주입시킬 수 있다.
class ExampleClass @Inject constructor(private val example: Example) {
...
}
ExampleClass는 Example 객체를 주입받고 있다.
여기서 Interface를 Constructor Injection에 사용할 수 없다.
Hilt가 Interface와 같은 구현체 타입의 객체를 어떻게 생성해야 할 지 알 수 없기 때문이다.
또한 Retrofit과 같은 외부 라이브러리 객체를 Constructor Injection에 사용할 수 없다.
Hilt가 외부 라이브러리 객체를 어떻게 생성해야 할 지 알 수 없기 때문이다.
Interface와 외부 라이브러리에 의존성을 주입하고 싶으면 @Modules를 보면 된다.
@Modules - @Provides
외부 라이브러리처럼 Hilt가 객체를 어떻게 생성해야할지 모르는 경우에 사용할 수 있다.
@Module Annotation을 통해 Module이라고 알려주어야 한다.
다음으로 InstallIn Annotation을 통해 해당 모듈이 어디서 사용할 수 있는지 선언을 할 수 있다.
(SingletonComponent::class)를 사용하면 application 전역에서 사용할 수 있는 것이다.
만약 Activity에서 사용할 경우엔 (ActivityComponent::class)를 사용해서 Activity 생명주기와 함께 해야 한다.
다른 Component에서 사용할 경우엔 위에 나온 Component 계층 구조를 보고 사용하면 된다.
@Module
@InstallIn(SingletonComponent::class)
object Module {
@Provides
fun moduleFuntion(): String {
...
}
}
이렇게 Module을 작성하면 외부 라이브러리를 Constructor Injection처럼 사용할 수 있게 된다.
class ExampleClass @Inject constructor(private val module: Module) {
...
}
@Modules - @Qualifier
@Provides로 설정한 객체가 같은 타입일 때 Hilt가 어떤 타입의 객체를 Inject할 지 모르기 때문에 구분 짓기 위해 사용할 수 있다.
Dagger에서 @Named("Identifier") Annotation을 사용하는 것과 같은 방식이다.
@Qualifier Annotation과 @Retention(AnnotationRetention.BINARY) Annotation을 통해 구분할 Identifier라고 Hilt에게 알려준다.
그다음 annotation class를 사용해 이름을 정해주면 된다.
@Module
@InstallIn(SingletonComponent::class)
object Module {
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Funtion1
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Funtion2
@Funtion1
@Provides
fun moduleFuntion1(): String {
...
}
@Funtion2
@Provides
fun moduleFuntion2(): String {
...
}
}
이렇게 이름을 지정해주면 다음과 같이 사용할 수 있다.
class ExampleClass @Inject constructor(
@Funtion1 private val module1: Module,
@Funtion2 private val module2: Module
) {
...
}
@Modules - @Binds
Interface처럼 Hilt가 객체를 어떻게 생성해야할지 모르는 경우에 사용할 수 있다.
Binds는 Interface 객체를 어떻게 만드는지 Hilt에게 알려주기 위한 용도이기에 외부라이브러리에서는 사용할 수가 없다.
@Provides와 동일하게 @Module과 @InstallIn Annotation을 사용한다.
그리고 abstract class로 생성된 클래스에 @Binds Annotation을 사용하고 abstract 함수를 정의한다.
@Module
@InstallIn(SingletonComponent::class)
abstract class Module {
@Binds
abstract fun moduleFuntion(): Interface {
...
}
}
이렇게 Module을 작성하면 Interface 또한 Constructor Injection처럼 사용할 수 있게 된다.
class ExampleClass @Inject constructor(private val module: Module) {
...
}
@Scope
Hilt에서는 Component와 Scope를 같이 유지해서 매번 객체를 주입할 때마다 새로운 객체를 생성하는 것이 아니라 해당 Scope 내에서 사용할 수 있도록 하고 있다.
클래스의 Scope를 위의 표에 있는 것으로 설정하면 해당 Scope 주기를 따라간다.
@HiltViewModel
HiltViewModel을 사용하기 위해서는 아래의 라이브러리를 추가해야 한다.
implementation "androidx.fragment:fragment-ktx:last-version"
@HiltViewModel Annotation을 사용해서 ViewModel을 작성하면 된다.
savedStateHandle도 필요한 경우에 넣으면 Hilt가 주입해준다.
@HiltViewModel
class MainActivityViewModel @Inject constructor(
private val mainRepository: MainRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
...
}
이렇게 ViewModel을 작성했으면 @AndroidEntryPoint Annotation이 붙은 액티비티나 프래그먼트에서 사용할 수 있다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mainActivityViewModel: MainActivityViewModel by viewModels()
...
}
또한 Jetpack Compose를 사용하는 경우에는 아래와 같이 ViewModel을 주입할 수 있다.
@Composable
fun ExampleView(
mainActivityViewModel: MainActivityViewModel = hiltViewModel()
...
)
@ApplicationContext
클래스에서 ApplicationContext가 필요할 때 @ApplicationContext Annotation을 사용할 수 있다.
@Module
@InstallIn(SingletonComponent::class)
object Module {
@Provides
fun moduleFuntion(@ApplicationContext context: Context): String {
...
}
}
마치며
취업 과제전형으로 프로젝트를 제작하면서 더 많은 기술을 집어넣기 위해 닥치는대로 Hilt를 사용해보려고 했었다.
요구했던 기능은 아니였지만 더 잘하고 싶은 마음으로 도전해봤는데 Hilt 사용법을 제대로 익히지 않아서 에러만 보다 적용하지 않았다.
이번 기회를 통해 Hilt 사용법에 대해 자세하게 공부하게 된 계기가 되었고 조만간 진행하고 있는 프로젝트에 적용해보면서 Hilt 사용법을 완벽하게 익혀야겠다.
참고
https://developer.android.com/training/dependency-injection/hilt-android?hl=ko#groovy
https://developer88.tistory.com/349