이번 글은 Android Jetpack 의존성 주입 라이브러리인 Hilt 에 대한 기초를 정리한 글입니다.
의존성 주입의 기초적인 부분을 알고있다는 가정 하에 작성하였습니다. 만약 의존성 주입의 기초에 대해 제가 정리한 글을 읽어보고 싶으시다면 아래 링크를 통해 읽으실 수 있습니다.
Dependency Injection(DI), 의존성 주입에 대해 조금 쉽게? 접근해보기
의존성 주입에는 다음과 같은 세가지 방법이 있습니다.
1. 생성자 주입
2. 메소드 (Setter) 주입
3. 필드 주입
Android MVVM 구조와 Repository Pattern 을 사용하여 개발을 하고 있는 저는 첫번째 방식인 생성자 주입을 Repository
구현체와 ViewModel
에서 다음과 같이 주로 사용합니다.
class InjectTargetRepository(
private val injectedAPI: InjectedAPI
)
class InjectTargetViewModel(
private val injectedRepository: InjectedRepository
)
하지만 위에서 생성한 ViewModel 을 View 에서 사용하기 위해서는 View 에서 Repository 객체를 생성하여 ViewModel 생성 시 함께 주입해주어야 합니다. 이렇게 된다면 View 와 Repository 또한 뗄래야 뗄 수 없는 사이가 되어버립니다.
이러한 문제점을 해결하기 위해서 Koin, Dagger, Hilt 와 같은 의존성 주입을 도와주는 라이브러리를 사용합니다. 이 중 우리는 Hilt 에 대해서 알아보겠습니다.
Hilt 라이브러리는 Dagger 기반의 컴파일 타임에 코드를 생성하는 의존성 주입 라이브러리 입니다. 이전의 Dagger 라이브러리는 많은 Annotation 과 보일러 플레이트로 인해서 러닝커브가 높다는 평을 받아 사용하기 쉽지 않았습니다.
하지만 이러한 단점들을 보완하고 Android 프레임워크에 알맞게 수정된 Hilt 라이브러리가 Android 공식블로그에 따르면 5월 4일부로 Stable Release 를 사용할 수 있는 것을 알 수 있습니다.
자세한 사항을 알아보기 전에, 먼저 어떻게 사용하는지 부터 간단하고 빠르게 알아보도록 하겠습니다.
@HiltAndroidApp
class HiltExampleApplication: Application() {...}
@AndroidEntryPoint
class HiltActivity: Activity() {...}
@HiltViewModel
class HiltViewModel : ViewModel() {...}
위 코드에서 볼 수 있듯이, 기본적인 Annotation 세가지가 존재합니다.
@HiltAndroidApp
@AndroidEntryPoint
@HiltViewModel
그 후에는 아래와 같이 Android class 에 생성자 주입을 진행해주어야 해당 모듈이 정상적으로 주입이 됩니다. 주입 시에는 @Inject
Annotation 을 사용합니다.
@HiltViewModel
class SimpleViewModel
@Inject
constructor(
private val simpleRepository: SimpleRepository
) : ViewModel() { ... }
또한 @HiltViewModel
은 Android AAC ViewModel 주입도 지원합니다. 그리고, Navigation Graph 에 따른 SharedViewModel 또한 지원합니다.
@AndroidEntryPoint
class SimpleFragment: Fragment() {
private val simpleViewModel: SimpleViewModel by viewModels()
private val simpleSharedViewModel: SimpleSharedViewModel by hiltNavGraphViewModels(R.id.simple_graph)
...
}
그렇다면 위 코드에서 생성자 주입을 했는데, 이 주입하려는 모듈들은 어떻게 생성될까요?
@Module
@InstallIn(SingletonComponent::class)
object SimpleModuleObject {
@Provides
fun provideSimpleModule(): SimpleInterface = SimpleInterfaceImpl
}
@Module
@InstallIn(SingletonComponent::class)
abstract class SimpleModuleAbstract {
@Binds
abstract fun bindSimpleInterface(simpleInterfaceImpl: SimpleInterfaceImpl): SimpleInterface
}
위의 코드에서 볼 수 있듯이, 모듈을 생성할 때는 기본적으로 네 가지 Annotation이 필요합니다.
@module
@InstallIn
@Provides
@Binds
@module
Annotation@HiltAndroidApp
, @AndroidEntryPoint
, @HiltViewModel
등, 해당 Object 나 Class 가 Hilt 에서 의존성 주입에 사용하게 될 모듈임을 알려주는 역할을 합니다.
@InstallIn
Annotation해당 모듈을 어떤 곳에 설치할 지를 알려주는 역할을 합니다. 설치할 수 있는 컴포넌트는 다음과 같습니다.
위 컴포넌트들의 lifetime 에 대해서는 다음 링크를 참조하시면 되겠습니다.
@Binds
Annotation만약 주입이 필요한 모듈 중, Interface가 있다면 생성자 주입을 할 수 없기에, 해당 Interface의 구현체를 Binding 하는 역할을 합니다. 주로 Android 에서 Repository Pattern 을 사용하는 프로젝트에서, Repository layer 를 예시로 들 수 있겠습니다.
@Provides
Annotation외부 라이브러리에서 생성되는 인스턴스이거나 Builder Pattern 으로 생성되는 인스턴스에 대한 주입 객체를 생성할 때 필요합니다. 주로 Android 에서는 Retrofit 객체나, OkHttp Client 객체를 Singleton 방식으로 생성하는 데에 사용합니다.
이제는 Hilt 를 사용하여 Test code 에 어떻게 적용하는 지 알아보겠습니다.
사실 Hilt 는 Unit Test 에서는 효용가치가 별로 없다고 볼 수 있습니다. 생성자 주입을 진행할 때, 실제 모듈을 Inject 하는 방식보다는 Mocking 된 의존성을 주입하면 되기 때문입니다.
그렇다면 UI Test 에서는 어떻게 사용되는지 알아보도록 하겠습니다.
Android UI Test 를 진행할 때, @HiltAndroidTest
Annotation 을 사용합니다.
@HiltAndroidTest
class SimpleFragmentTest { ... }
물론, 기본적인 모듈을 생성하는 것처럼 Test 에서 사용할 모듈을 테스트 클래스 내에서 선언하여 사용할 수도 있습니다.
지금까지 Hilt 의 기본 Annotation 과 사용법, 그리고 어디에 사용하는 지에 대해서 알아보았습니다.
Hilt 는 컴파일 타임에 주입할 코드를 생성하기 때문에 컴파일 타임에 오류를 잡아낼 수 있는 장점이 있습니다.
또한 다른 DI 라이브러리도 있지만, 구글 공식 문서에서 DI 의 Best Practice 로 발표한 Hilt 가 다른 라이브러리 보다는 좀 더 안드로이드에서 Stable 할 것으로 생각됩니다. 한 번 써보시는 것도 좋을 것 같습니다.
Hilt is stable! Easier dependency injection on Android
Android DI with Hilt — Hilt를 이용한 의존성 주입
Dagger Hilt로 안드로이드 의존성 주입 시작하기