이번 영상에서는 안드로이드에서 의존성 주입을 편하게 구현할 수 있도록 구글에서 제작한 Hilt 라이브러리를 사용하는 법에 대해 알아보도록 하겠습니다.
2007년 구글에서는 자바의 의존성 주입을 위한 라이브러리인 Guice를 발표합니다. 그러나 느린 속도와 메모리 문제가 있었기에 2012년 Square에서 Dagger v1을 발표하게 됩니다. Dagger v1은 안드로이드 앱에 잘 적용되었지만, 속도가 느렸고 런타임 중에 DI 처리를 수행하는 단점이 있었습니다. 그래서 구글은 프로젝트를 포크하여 2016년에 Dagger v2 를 발표했고 저장소의 관리자가 됩니다.
Dagger v2는 속도를 개선했고, Dependency graph를 작성하는데에 Annotations를 사용함으로써 컴파일 타임에 오류를 잡아낼 수 있게 되었습니다.
문제는 Dagger를 사용하는게 너무 복잡하고 어렵다는 점이었습니다. 그래서 구글은 개발자들의 의견을 참고하여 Dagger를 더 쓰기 쉽게 하는 Hilt를 2020년에 발표하게 됩니다. 날카로운 단검(Dagger)를 다루기 쉽게 하는 손잡이(Hilt)라는 뜻에서 Hilt가 되었다고 하네요.
Hilt의 동작구조는 다음과 같습니다. 우선 어플리케이션 내부에 생명주기와 연동되는 Component라는 이름의 보관함을 만들고 그 안에 의존객체를 생성합니다. 이때 Component 안에는 또 하위의 Component를 둘 수 있습니다. 그리고 의존객체들끼리 관계를 정의한 Dependency graph를 정의한 뒤 앱 실행중에 의존객체의 요청이 오면 graph를 참조하여 의존객체를 반환합니다.

Hilt는 현재 다음 안드로이드 클래스에 대해서만 의존성을 주입할 수 있습니다.
Application에는 @HiltAndroidApp, ViewModel에는 @HiltViewModel, 그리고 나머지 클래스에는 @AndroidEntryPoint를 붙여주면 Hilt가 각 구성요소에 상응하는 의존성을 보관하기 위한 Component를 작성하게 되고, Component 내부의 의존객체에 대해서는 의존성을 주입할 수 있는 상태가 됩니다.
그런데 Hilt가 제공하는 의존객체는 기본적으로 unscoped 상태입니다. 앱이 객체를 요청할 때마다 새로운 객체가 만들어진다는 말이죠. 이를 방지하기 위해 각 의존객체에는 스코프라는 이름의 생명주기를 지정할 수 있는데 이 스코프의 범위를 나타내는 것이 다음 그림의 @ScopeAnnotation입니다.
예를 들어 의존객체에 @Singleton을 붙이면 앱 전체에서 하나의 객체만 만들어지게 되고, @ActivityScoped가 붙으면 해당 액티비티에서 하나의 객체만이 만들어지게 됩니다. 이 때 컴포넌트 종속관계에 따라 하위의 컴포넌트는 상위 스코프를 가진 의존객체에 접근할 수 있습니다.
의존객체를 모아둔 컴포넌트를 만들었으면 이제 이 의존객체를 어딘가에 주입해야겠죠. 컴포넌트 안의 의존객체와, 그 의존객체를 주입받을 객체를 연결하는 행위를 binding 한다고 합니다. Hilt에서는 의존성을 제공할 객체와 받을 객체 양 쪽에 @Inject 어노테이션을 붙임으로써 binding이 수행됩니다. 다만 의존객체를 주입받기 위해서는 Component가 있어야 하므로 @AndroidEntryPoint를 추가로 붙여주어야 합니다.
Hilt에서는 Field와 Constructor에 대해 의존성 주입을 수행할 수 있습니다. 우선 Field injection에 대해 설명하겠습니다. 예를 들어 AnalyticsAdapter 인스턴스를 ExampleActivity의 analytics Field에 injection하기 위해서는 다음과 같이 @Inject을 붙이고 주입받을 클래스에 @AndroidEntryPoint를 붙여주면 됩니다.
class AnalyticsAdapter @Inject constructor() { }
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapter
...
}
그리고 Constructor injection을 수행하기 위해서는 다음과 같이 ExampleActivity 에 @Inject constructor를 붙여서 의존성을 주입하게 됩니다.
class AnalyticsAdapter @Inject constructor() { }
@AndroidEntryPoint
class ExampleActivity @Inject constructor(
var analytics: AnalyticsAdapter
) : AppCompatActivity() { }
의존객체를 담는 클래스를 Hilt에서는 Module이라는 이름으로 정의합니다. Hilt의 용어를 따르면 컴포넌트 안에 모듈을 설치한 뒤, 모듈내부의 의존객체를, 필요로 하는 곳에 주입하게 되는 것입니다. 모듈의 정의와 설치에는 @Module과 @InstallIn 어노테이션을 사용합니다.
기본적으로 모듈 안의 모든 의존객체는 다른 곳에 주입 가능한데 이 때 외부 라이브러리로부터 만들어지는 인스턴스와, 인터페이스의 인스턴스는 Hilt가 생성하는게 아니므로 그대로는 주입할 수 없습니다. 그래서 구글은 @Provides와 @Binds 두가지 어노테이션을 준비했습니다.
Hilt를 통하지 않고 빌드패턴이나 외부 라이브러리를 이용해서 객체를 생성해야 할 경우에는 @Provides를 사용하면 됩니다. 예제를 보겠습니다. AnalyticsModule 클래스는 @Module을 붙임으로써 Hilt의 모듈이 됩니다. 이 모듈은 @InstallIn(ActivityComponent::class)에 의해 ActivityComponent 안에 설치되면서 스코프가 Activity로 설정되게 됩니다. 모듈 안에는 AnalyticsService 타입을 반환하는 provideAnalyticsService가 있는데요, 이 함수에 @Provides를 붙임으로써 AnalyticsModule이 AnalyticsService 의존성을 외부에 제공할 수 있게 됩니다.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
@Provides
fun provideAnalyticsService(
// Potential dependencies of this type
): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}
@Binds는 abstract 메소드에만 붙일 수 있고 파라미터는 하나만을 사용할 수 있으며 이 때 파라미터의 타입은 반환타입이 될 수 있는것만 사용할 수 있다는 제한사항이 있습니다. @Binds는 그래서 사실상 제한된 조건하에서만 사용가능한 @Provides 와 같은데요, hilt가 신경써야 할 부분이 적어지기 때문에 내부적으로 자동생성하는 코드 양이 감소한다는 특징이 있습니다.
그래서 인터페이스의 구현체를 의존객체로 제공할 경우 @Provides 대신 @Binds를 사용하면 오버헤드를 줄일 수 있습니다. 또한 뷰모델에 인터페이스가 주입되도록 코드를 작성할 수 있으므로, 필요할 경우 주입하는 구현체 종류를 뷰모델 코드를 건드리지 않고 외부에서 변경할 수 있다는 장점도 있습니다.
예제를 보겠습니다. AnalyticsService 인터페이스의 구현체인 AnalyticsServiceImpl 인스턴스를 bindAnalyticsService 내부에서 매개변수로 받습니다. 그리고 bindAnalyticsService가 인터페이스를 반환하게 하고 @Binds를 붙입니다. 모듈과 메소드가 abstract여야 한다는 점에 주의하세요.
interface AnalyticsService {
fun analyticsMethods()
}
// Constructor-injected, because Hilt needs to know how to
// provide instances of AnalyticsServiceImpl, too.
class AnalyticsServiceImpl @Inject constructor(...) : AnalyticsService { ... }
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
이렇게 해서 Hilt의 구조와 사용법에 대해 알아보았습니다. Hilt 여러가지 annotation과 사용예는 다음 치트시트에서 확인하실 수 있습니다.
