이 글의 원문은 여기서 확인 가능하며 블로그 리뷰에는 노현석(GDE)가 참여해주셨습니다.
Hilt에 대한 MAD Skills 게시물 시리즈입니다! 이번 게시물에서는 종속성 주입(DI)이 앱과 Hilt에 중요한 이유를 살펴보겠습니다. Hilt는 Android에서 DI를 구현하기 위한 Jetpack의 권장 솔루션입니다.
이 콘텐츠를 동영상으로 보려면 아래를 클릭하세요.
Android 앱에서 종속성 주입 원칙을 따르면 좋은 앱 아키텍처에 필요한 기초를 마련할 수 있습니다. 코드 재사용성과 리팩터링 용이성, 테스트 용이성에 도움이 되죠! 여기에서 DI의 이점에 대해 자세히 알아보세요.
프로젝트에서 클래스의 인스턴스를 만들 때, 클래스에 필요한 종속성과 전이 종속성을 충족하여 종속성 그래프를 수동으로 실행할 수 있습니다.
하지만 이 작업을 매번 수동으로하면 일부 상용구 코드가 포함되어 오류가 쉽게 발생할 수 있습니다. 예를 들어 오픈소스 Google I/O 앱인 iosched에 있는 ViewModel 중 하나를 생각해 봅시다. 종속성과 전이 종속성이 있는 FeedViewModel을 만드는 데 얼마나 많은 코드가 필요한지 상상이 가시나요?
어렵고 반복적인 일인 데다 종속성을 잘못 이해하기도 쉽습니다. 종속성 주입 라이브러리를 사용하면, 라이브러리에서 필요한 코드가 전부 자동으로 생성되므로 종속성을 수동으로 제공할 필요 없이 DI의 이점을 얻을 수 있습니다. 여기에서 Hilt가 역할을 합니다.
Hilt는 Google에서 개발한 종속성 주입 라이브러리로, 힘든 작업을 수행하고 개발자가 일일이 작성해야 했을 모든 상용구를 대신 생성해 줌으로써 앱에서 DI 모범 사례를 최대한 구현하는 데 도움을 줍니다.
Hilt는 Annotation을 사용하여 컴파일 시간에 해당 코드를 생성함으로써 런타임에 매우 빠르게 합니다. 이 작업에는 Hilt를 빌드할 때 기초가 된 JVM DI 라이브러리인 Dagger가 활용됩니다.
Hilt는 Android 앱을 위한 Jetpack의 권장 DI 솔루션으로, 도구 및 기타 Jetpack 라이브러리 지원이 함께 제공됩니다.
Hilt를 사용하는 모든 앱은 컴파일 시간에 Hilt의 코드 생성을 트리거해야 하므로 @HiltAndroidApp으로 Annotation이 달린 Application 클래스를 포함해야 합니다. Hilt가 Activity에 종속성을 주입하려면 Activity에 @AndroidEntryPoint로 Annotation을 달아야 합니다.
종속성을 주입하려면 Hilt가 @Inject로 주입할 변수에 Annotation을 답니다. 모든 Hilt 주입 변수는 super.onCreate가 호출될 때 사용할 수 있습니다.
이 예제에서는 MusicPlayer를 PlayActivity에 주입합니다. 그런데 Hilt는 MusicPlayer 타입의 인스턴스 제공 방법을 어떻게 아는 걸까요? 지금은 Hilt가 그 방법을 모릅니다. 그래서 Hilt에 그 방법을 알려줘야 하죠. 물론 Annotation을 사용해서 말이죠!
@Inject로 클래스의 생성자에 Annotation을 달아 Hilt에 해당 클래스의 인스턴스 생성 방법을 알려줍니다.
Activity에 종속성을 주입하기 위해서는 이 정도만 하면 됩니다. 꽤 쉽죠! MusicPlayer가 다른 타입에 의존하지 않으므로, 간단한 예제로 시작해 보았습니다. 하지만 매개변수로 전달된 다른 종속성이 있다면, Hilt가 MusicPlayer의 인스턴스를 제공할 때 그 매개변수를 처리하여 종속성을 충족할 것입니다.
이건 사실 매우 간단하고 기본적인 예제였습니다. 하지만 지금까지 수행한 작업을 수동으로 해야 한다면 어떻게 해야 할까요?
DI를 수동으로 수행할 때는 종속성 컨테이너 클래스를 가질 수 있는데, 이는 타입을 제공하고 인스턴스의 수명 주기를 관리하는 역할을 합니다. 간단히 얘기하자면, 이게 바로 Hilt가 내부적으로 수행하는 일이죠.
@AndroidEntryPoint로 Activity에 Annotation을 달면, 종속성 컨테이너가 자동으로 생성 및 관리되고 PlayActivity에 연결됩니다. 그 작업을 수동으로 구현한 것을 PlayActivityContainer로 합니다. @Inject로 MusicPlayer에 Annotation을 달면 컨테이너에 MusicPlayer 타입의 인스턴스 제공 방법을 기본적으로 알려주는 셈입니다.
그리고 Activity에서 컨테이너의 인스턴스를 만들고 그걸 사용해 Activity의 종속성을 채워야 합니다. @AndroidEntryPoint로 Activity에 Annotation을 달 때도 Hilt가 그 작업을 수행합니다.
지금까지 @Inject를 사용하여 클래스 생성자에 Annotation을 달면 Hilt에 해당 클래스의 인스턴스 제공 방법을 알려주게 된다는 점을 살펴보았습니다. @AndroidEntryPoint Annotation이 달린 클래스의 변수에 Annotation을 달면 Hilt가 해당 타입의 인스턴스를 클래스에 주입합니다.
Activity뿐만 아니라 대부분의 Android 프레임워크 클래스에 Annotation을 달 수 있는 @AndroidEntryPoint는 해당 클래스에 대한 종속성 컨테이너의 인스턴스를 생성하고 @Inject Annotation이 달린 모든 변수를 채웁니다.
@HiltAndroidApp은 Application 클래스에 Annotation을 달고, Hilt의 코드 생성을 트리거하는 것과는 별개로 Application 클래스와 연결된 종속성 컨테이너도 만듭니다.
이제 Hilt의 기초 사항을 다루었으므로 좀 더 복잡한 예제를 살펴봅시다. 이번에는 MusicPlayer가 생성자인 MusicDatabase에서 종속성을 사용합니다.
따라서 우리는 Hilt에 MusicDatabase의 인스턴스 제공 방법을 알려줘야 합니다. 예를 들어 타입이 인터페이스이거나, 클래스를 라이브러리에서 가져온 탓에 소유하지 않은 경우, 생성자에 @Inject로 Annotation을 달 수 없습니다.
앱에서 Room을 지속성 라이브러리로 사용한다고 생각해봅시다. PlayActivityContainer의 수동 구현을 다시 떠올려 보자면, MusicDatabase를 제공할 때 Room 사용 시 이는 추상 클래스가 되며, 개발자는 종속성을 제공할 때 어떤 코드를 실행하고 싶을 것입니다. 그러면 MusicPlayer의 인스턴스를 제공할 때 MusicDatabase 종속성을 제공하거나 충족하는 메서드를 호출해야 합니다.
Hilt는 모든 전이 종속성을 자동으로 연결하므로, Hilt의 전이 종속성에 대해서는 걱정할 필요가 없습니다. 그러나 MusicDatabase 타입의 인스턴스 제공 방법은 Hilt에 알려줘야 합니다. 그 목적으로 사용하는 것이 Hilt 모듈입니다.
Hilt 모듈은 @Module Annotation이 달린 클래스입니다. 그리고 클래스 내에서 Hilt에 특정 타입의 인스턴스 제공 방법을 알려주는 함수를 가질 수 있습니다. Hilt에 의해 알려진 이 정보를 Hilt 전문 용어로 바인딩 이라고도 합니다.
@Provides로 Annotation이 달린 함수가 Hilt에 MusicDatabase 타입의 인스턴스 제공 방법을 알려줍니다. 본문에는 Hilt가 실행해야 하는 코드 블록이 포함되는데, 이는 수동 구현에 있었던 것과 정확히 똑같습니다.
반환 타입인 MusicDatabase는 이 함수가 어떤 타입을 제공하는지 Hilt에 알립니다. 그리고 함수 매개변수는 해당 타입의 종속성(이 경우에는 Hilt에서 이미 사용할 수 있는 ApplicationContext)을 Hilt에 알려줍니다. 이 코드는 MusicDatabase 타입의 인스턴스 제공 방법을 Hilt에 알려줍니다. 즉 개발자는 MusicDatabase에 대한 바인딩을 가지게 됩니다.
Hilt 모듈에는 어떤 종속성 컨테이너 또는 구성 요소에서 이 정보를 사용할 수 있는지를 나타내는 @InstallIn Annotation도 있습니다. 그런데 구성 요소란 무엇일까요? 이에 대해 더 자세히 살펴봅시다.
구성요소 Hilt에 의해 생성되며, 우리가 수동으로 프로그래밍한 컨테이너와 같은 타입의 인스턴스를 제공하는 역할을 하는 클래스입니다. 컴파일 시간에 Hilt는 애플리케이션의 종속성 그래프를 탐색하고 모든 타입에 전이 종속성을 제공하는 코드를 생성합니다.
구성요소는 Hilt에 의해 생성되며, 타입의 인스턴스를 제공하는 역할을 하는 클래스입니다.
Hilt는 대부분의 Android 프레임워크 클래스에 대한 구성요소 또는 종속성 컨테이너를 생성합니다. 각 구성 요소의 정보 또는 바인딩은 구성 요소 계층 구조를 통해 전파됩니다.
Hilt의 구성 요소 계층 구조
MusicDatabase 바인딩이 Application 클래스에 해당하는 SingletonComponent에서 사용 가능한 경우, 나머지 구성 요소에서도 사용할 수 있습니다.
이러한 구성 요소는 컴파일 시간에 Hilt에 의해 자동으로 생성되며 클래스에 @AndroidEntryPoint Annotation을 추가할 때 생성 및 관리되고 해당 Android 프레임워크 클래스와 연결됩니다.
모듈에 대한 @InstallIn Annotation은 이러한 바인딩을 어디서 사용할 수 있는지, 사용할 수 있는 다른 바인딩은 무엇인지 제어하는 데 유용합니다.
수동으로 생성한 PlayActivityContainer 코드로 돌아가보면, 우리가 알아차리지 못하더라도 MusicDatabase 종속성이 필요할 때마다 각각 다른 인스턴스가 생성되고 있습니다.
앱 전체에서 MusicDatabase의 똑같은 인스턴스를 재사용하고 싶을 수도 있으므로, 이렇게 다른 인스턴스가 생성되는 상황은 바람직하지 않습니다. 함수 대신 변수에 그 모든 것을 포함하면 똑같은 인스턴스를 공유할 수 있습니다.
기본적으로, 우리는 항상 동일한 인스턴스를 종속성으로 제공하므로 MusicDatabase 타입의 범위를 이 컨테이너에 맞춰 지정합니다. 어떻게 Hilt로 이 작업을 수행할 수 있을까요? 별다른 건 없습니다. 또 다른 Annotation을 사용하면 되죠!
@Provides 메서드에서 @Singleton Annotation을 사용하여 항상 해당 구성 요소에서 이 타입의 동일한 인스턴스를 공유하도록 Hilt에 알려줍니다.
@Singleton은 범위 Annotation입니다. 그리고 각 Hilt 구성 요소에는 범위 Annotation이 연결되어 있습니다.
각 Hilt 구성 요소에 대한 범위 Annotation
타입의 범위를 ActivityComponent로 지정하려면 ActivityScoped Annotation을 사용합니다. 이런 Annotation은 모듈에서 사용할 수 있지만, 생성자에 @Inject Annotation이 달린 클래스에 Annotation을 달 수도 있습니다.
두 가지 타입의 바인딩이 있습니다.
범위 Annotation이 달리지 않은 바인딩은 범위가 지정되지 않은 바인딩(예: MusicPlayer)이라고 하며, 이런 바인딩은 모듈에 설치되지 않은 경우에 모든 구성 요소에서 사용할 수 있습니다.
범위 Annotation이 달린 범위가 지정된 바인딩(예: MusicDatabase) 또는 모듈에 설치되고 범위가 지정되지 않은 바인딩은 해당 구성 요소와 계층 구조상 그 아래에 있는 구성 요소에서 사용할 수 있습니다.
Hilt는 가장 인기있는 Jetpack 라이브러리인 ViewModel, Navigation, Compose, WorkManager와의 통합을 제공합니다.
ViewModel 외에도, 각각의 통합에는 프로젝트에 추가할 다른 라이브러리가 필요합니다. 이에 대한 자세한 내용은 문서를 참조하세요. 이 블로그 게시물의 시작 부분에서 봤던 iosched의 FeedViewModel 코드를 기억하세요? Hilt 지원으로 어떻게 보이는지 살펴보고 싶으세요?
@Inject로 생성자에 Annotation을 다는 것 외에도, 이 ViewModel의 인스턴스 제공 방법을 Hilt에 알려주려면 @HiltViewModel로 클래스에 Annotation을 달아야 합니다.
이렇게만 하면 됩니다! Hilt가 자동으로 처리해주므로, ViewModel 공급자를 수동으로 만들 필요가 없습니다.
Hilt는 널리 쓰이는 또 다른 종속성 주입 라이브러리인 Dagger를 기반으로 만들어졌습니다. 앞으로 업로드 될 게시물에서 Dagger가 자주 언급될 것입니다. Dagger를 쓰신다면 Hilt와 함께 사용할 수도 있습니다. 이 가이드에서 마이그레이션 API에 대해 자세한 내용을 읽어보세요.
Hilt에 대한 자세한 내용은 가장 자주 쓰이는 Annotation과 이 Annotation들의 역할 및 사용 방법을 담은 요약본에서 살펴보세요. Hilt에 대한 문서 외에도, 보다 실무적인 경험을 통해 배울 수 있는 코드랩도 있습니다.
이것으로 이번 게시물을 마치겠습니다. 하지만 이게 끝이 아닙니다! 앞으로 MAD 기술 관련 포스트가 더 많이 게시될 예정이니 Android 개발자 Medium 게시물을 팔로우하여 게시 일정을 살펴보세요.