안녕하세요! 오늘은 안드로이드 개발에 자주 사용되고 있는 의존성 라이브러리 Dagger Hilt에 대해 알아보도록 하겠습니다.
Hilt에 대해 소개하기 앞서 먼저 의존성이 무엇인지 이해하는 것이 중요합니다.
의존성이란? A클래스가 자체적인 B클래스를 구성하는 것을 의미합니다.
단지 이 문장만으로는 이해하기 어려우니 간단한 예제를 통해 알아보도록 하겠습니다😊
의존성 주입(DI) 이란?🤔
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
위 코드를 살펴보면 Car 클래스는 내부에서 Engine 이라는 클래스를 만들어 사용하고 있습니다. 이렇게 사용하게 되면 서브 클래스의 구현이 쉽지않고, 테스트가 어렵게 됩니다. 만약에 Car 클래스에서 사용하기 위한 Engine 의 서브클래스인 ElectricEngine을 생성해서 사용하려면 Car 클래스를 재사용하지 않고 ElectriceEngine 을 사용하는 또다른 Car 클래스를 만들어 사용해야합니다. 그래서 Engine 을 외부에서 선언해서 Car 에 삽입하는 것을 의존성 주입(DI)이라고 합니다.
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
위 코드는 앞서 설명한 의존성 주입을 적용하는 예시입니다. 메인 함수에서 Engine 이 무엇인지 미리 정의하고 Car 클래스에 삽입하여 사용하는 것을 볼 수 있습니다. 이렇게 하면 Engine 의 서브 클래스인 ElectriceEngine 을 만들어 사용하더라도 Car 클래스에 인스턴스만 전달해주면 되기때문에 서브 클래스를 사용할 수 있습니다!
또한 테스트도 용이하게 되겠죠!
Dagger Hilt 사용하기
Gradle(app)
// Hilt
def hilt_version = "2.41"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
kapt 'androidx.hilt:hilt-compiler:1.0.0'
Gradle(project)
plugins {
...
id "com.google.dagger.hilt.android" version '2.41' apply false // hilt
...
}
Hilt Application
@HiltAndroidApp
class ExampleApplication : Application() { ... }
Hilt 을 사용하는 모든 앱은 반드시 @HiltAndroidApp 어노테이션을 Application 클래스에 포함해야 합니다.
생성된 이 Hilt 구성요소는 Application 객체의 수명주기에 연결되며 이와 관련한 종속 항목을 제공합니다. 또 이는 앱의 상위 구성요소이므로 다른 구성요소는 이 상위 구성요소에서 제공하는 종속 항목에 엑세스할 수 있습니다!
Component hierachy
Hilt 에서는 안드로이드 환경에서 표준적으로 사용되는 컴포넌트들을 기본적으로 제공합니다.
Hilt 컴포넌트 구조는 아래 사진과 같습니다.
위 컴포넌트들은 생성 이후 파괴 전까지 injection(주입)이 가능합니다.
아래 그림은 Hilt 구성요소별 주입대상을 나타내고 있는 표입니다.
위에서 언급했다시피 컴포넌트들은 생성과 파괴가 이뤄지는데 이는 곧 컴포넌트만의 생명주기가 있다는 것을 의미합니다.
아래 표는 Hilt 컴포넌트별 생명주기를 나타내고 있습니다.
※ 2.28.1이상 버전에서는 ApplicationComponent가 SingletonComponent 로 대체 되었습니다.
(아직 개발자 문서에는 해당 내용에 대해 업데이트 되지 않은듯 싶습니다.)
Hilt Module
위 의존성 설명 예시의 Car 클래스와 같이 생성자를 이용하여 의존성을 주입하는 경우가 많습니다.
하지만 인터페이스와 같이 생성자를 사용할 수 없거나, 안드로이드 표준 컴포넌트가 아닌 Retrofit2 와 같은 흔히 사용되는 외부 라이브러리에 대한 의존성 지원은 따로 하지 않고 있습니다. 그래서 이번에 알아볼 모듈(Module) 를 이용하면 이를 해결하실 수 있습니다!
Hilt Module 이란 간단히 말하면 @Module 어노테이션으로 정의된 클래스입니다. 기존에 이용되던 Dagger 와 다른 점으로는 @InstallIn 어노테이션을 통해 모듈을 사용하거나 설치할 Android 클래스를 Hilt 에게 알려줍니다. Hilt 모듈에서 제공하는 종속 항목은 Hilt 모듈을 설치하는 Android 클래스와 연결되어 있는 모든 생성된 구성요소에서 사용할 수 있습니다.
1. @Binds 를 사용하여 인터페이스 인스턴스 삽입하기
앞서 설명드렸던 바와 같이 인터페이스의 경우 생성자를 삽입할 수 없습니다. 그렇기 때문에 일반적인 생성자를 이용한 의존성 주입방법이 아닌 Hilt 모듈 내에 @Binds 어노테이션 이용해 추상 함수를 생성하여 Hilt 에 결합 정보를 제공하는 방법을 이용합니다.
@Binds 어노테이션은 인터페이스의 인스턴스를 제공해야 할 때 사용할 구현을 Hilt 에 알려줍니다.
@Binds 를 사용한 함수는 Hilt 에 다음과 같은 정보를 제공합니다.
- 함수 반환 유형은 함수가 어떤 인터페이스의 인스턴스를 제공하는지 Hilt 에 알려줍니다.
- 함수 매개변수는 제공할 구현을 Hilt에 알려줍니다.
아래는 @Binds 를 사용한 안드로이드 공식 문서 내 예시입니다.
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 가 AnalyticsModule의 종속 항목을 Activity 에 주입하기 위해 @InstallIn(ActivityComponent::class) 어노테이션을 사용하였습니다. 이로써 AnalyticsModule 의 모든 종속 항목을 액티비티 내에서 사용할 수 있습니다!
2. @Provides를 사용하여 인스턴스 삽입
Retrofit, OkHttpClient, Room 과 같이 클래스가 외부 라이브러리에서 제공되는 경우 또는 Builder 패턴으로 인스턴스를 생성해야 하는 경우에도 생성자 삽입이 불가능합니다. 이 경우 @Providers 어노테이션을 이용하여 이 유형의 인스턴스를 제공하는 방법을 Hilt에 알릴 수 있습니다. @Providers 어노테이션을 사용하는 함수는 Hilt 에 다음 정보를 제공합니다.
- 함수 반환 유형은 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt 에 알려줍니다.
- 함수 매개변수는 해당 유형의 종속 항목을 Hilt 에 알려줍니다.
- 함수 본문은 해당 유형의 인스턴스를 제공하는 방법을 Hilt 에 알려줍니다. Hilt는 해당 유형의 인스턴스를 제공해야 할 때마다 함수 본문을 실행합니다.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Module
@InstallIn(ActivityComponent::class)
object NetworkModule2 {
@ActivityScoped
@Provides
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@InstallIn 어노테이션은 선언한 Hilt 의 표준 컴포넌트들 중 어떤 컴포넌트에 모듈을 설치할지를 결정하기 때문에
위의 코드에서는 SingletonComponent 과 ActivityComponet 에 모듈을 각각 설치하였습니다.
SingletonComponent 의 경우 @Singleton 어노테이션을 이용해 싱글톤 패턴으로 생성하여 사용할 때마다 인스턴스가 생성되는 것을 막아주며, ActivityComponent의 경우 Activity 내 활동 범위에서 지정되어야 하므로 @AcitivityScoped 어노테이션을 이용합니다.
이는 곧 자신과 관련된 활동범위로 지정되어야함을 의미합니다!
AndroidEntryPoint
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {
@Inject lateinit var retrofit: Retrofit
}
AndroidEntryPoint 는 쉽게말해 객체가 주입되는 대상이라고 이해하시면 됩니다.
위의 코드를 보면 @AndroidEntryPoint 를 사용하여 객체 주입 대상으로 선언해주고,
Retrofit 변수에 @Inject 어노테이션을 이용해 주입하게되면 선언한 모듈중 Retrofit 선언과 관련된 함수를 찾아 사용합니다.
Hilt의 사전 정의된 한정자
Hilt 에서는 몇 가지 사전 정의된 한정자를 제공합니다. 예를 들면 애플리케이션 또는 액티비티의 Context 클래스가 필요한 경우 @ApplicationContext 및 @ActivityContext 한정자를 제공하고 있습니다.
아래 코드는 AnalyticsAdapter 에 액티비티 Context 를 제공하는 방법을 보여주고 있습니다.
class AnalyticsAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: AnalyticsService
) { ... }
동일한 객체지만 각각 따로 선언하고싶다면?🤔
예를 들어 Retrofit 을 이용한 네트워크 모듈을 만들었다고 가정하겠습니다. Retrofit 객체를 생성할때 baseUrl() 을 이용해 URL을 정의하는 부분이 있습니다. 만약 URL이 다른 또다른 Retrofit 객체를 만들기 위해서는 의존성을 어떻게 주입시켜줘야 할까요?
이는 @Qualifier 어노테이션을 이용하면 구현하실 수 있습니다. 아래 코드는 서로 다른 2개의 Retrofit 객체를 모듈에 선언하는 예제입니다.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Qualifier
annotation class Retrofit
@Qualifier
annotation class Retrofit2
@Singleton
@Provides
fun getOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.MINUTES)
.build()
}
@Singleton
@Provides
@Retrofit
fun getRetrofitApis(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Singleton
@Provides
@Retrofit2
fun getRetrofitNaverMap(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(URL2)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Singleton
@Provides
fun getApi(@Retrofit retrofit: Retrofit): Api {
return retrofit.create(Api::class.java)
}
@Singleton
@Provides
fun getApi2(@Retrofit2 retrofit: Retrofit): Api2 {
return retrofit.create(Api2::class.java)
}
}
코드를 보시면 @Qualifier 어노테이션 선언 이후 annotation class 를 선언하여 각각 Retrofit, Retrofit2 라는 이름을 가진 2개의 Retrofit 객체를 선언하고 있습니다. 이를 필드나 매개변수에 주입시킬 때는 선언한 이름의 어노테이션 @Retrofit, @Retrofit2 을 이용합니다.
또 다른 방법으로는 @Named 어노테이션을 이용하는 방법이 있습니다. 아래 예제를 통해 살펴보도록 하겠습니다.
@Module
@InstallIn(SingetonComponent::class)
object AppModule {
@Singleton
@Provides
@Named("String1")
fun getTestString() = "Test1"
@Singleton
@Provides
@Named("String2")
fun getTestString2() = "Test2"
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
@Named("String1")
lateinit var test1 : String
...
}
AppModule 에서는 String 을 반환하는 두개의 함수를 선언하고 있습니다. 이들 모두 String 을 반환하기 때문에 주입 시에 구분이 필요합니다. @Named("이름") 어노테이션을 이용하면 각각 이름을 선언하여 구분할 수 있습니다. 예제 내 액티비티에 주입할 때 @Named("String1") 어노테이션을 사용함으로서 AppModule 의 getTestString1() 을 자동으로 반환하게 됩니다!
(참고) Hilt Annotations
마무리하며..✍
오늘은 최근 여러 안드로이드 프로젝트에서 자주 이용되는 종속성 라이브러리 Hilt 에 대해 알아보았습니다.
위에서 설명한 내용 외에도 더 많은 기능들이 있기때문에 Hilt 에 대한 더 많은공부가 필요하다고 느꼈습니다..😔
Hilt 를 사용하므로써 코드의 길이가 줄어들고, 가독성이 늘릴 수 있기때문에 ACC의 MVVM 패턴과도 찰떡궁합이라는
생각이 들었습니다.
비록 수박 겉핥기로 Hilt 에 대해 알아보았지만, 앞으로 만드는 프로젝트에 Hilt 을 계속해서 적용
하여 숙달될 수 있도록 노력할 것입니다!
지금까지 부족한 글 읽어주셔서 감사합니다!🙇
참고
https://developer.android.com/training/dependency-injection/hilt-android?hl=ko
Hilt를 사용한 종속 항목 삽입 | Android 개발자 | Android Developers
Hilt를 사용한 종속 항목 삽입 Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. 수동 종속 항목 삽입을 실행하려면 모든 클래스
developer.android.com
https://hanyeop.tistory.com/220
[Android] Dagger Hilt 사용하여 의존성 주입(DI) 하기
0️⃣ 의존성 주입(DI) 이란? 먼저, 의존성이란 A 클래스가 자체적인 B 클래스를 구성하는 것을 말한다. 구글의 예시를 통해 알아보자. 그림과 같이 Car라는 클래스가 Engine 라는 클래스를 가져다 쓰
hanyeop.tistory.com
'Android' 카테고리의 다른 글
[안드로이드 / Kotlin] 소스코드 수정 없이 debug/release 앱 분리하기 (0) | 2022.05.29 |
---|---|
[안드로이드 / Kotlin] Status bar 투명하게 (with DrawerLayout) (0) | 2022.05.18 |
[안드로이드 / Kotlin] Room (0) | 2022.04.27 |
[안드로이드 / Kotlin] ViewModel(뷰모델) (0) | 2022.04.12 |
[안드로이드 / Kotlin] 키해시(Key Hash) 얻기 (0) | 2022.04.06 |