DI는 프로그래밍에 널리 사용되는 방식으로 객체의 생성과 사용의 관심을 분리하는 것이다. 다시 말하면 클라이언트에서 어떤 서비스를 이용할 때 이를 외부에서 생성하여 해당 서비스를 사용할 수 있게 해주는 것이다. 클라이언트는 서비스의 생성 및 구성 방식에 대해 알 필요가 없이 서비스의 기능을 사용하면 된다.
DI를 사용했을 때의 장점은 뭐가 있을까?
Boilerplate code
를 줄일 수 있다.Hilt는 Android 클래스에 의존성 주입을 지원하고 수명 주기를 자동으로 관리해 주기 때문에 Android에서 DI를 사용하기에 적합한 라이브러리이다.
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:$version'
}
}
...
plugins {
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation "com.google.dagger:hilt-android:2.38.1"
kapt "com.google.dagger:hilt-compiler:2.38.1"
}
@HiltAndroidApp
class ExampleApplication : Application() { ... }
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }
Activity
외에도 Android 클래스를 지원한다.
이와 같이 Android 클래스에 주석을 추가하는 경우 해당 클래스에 종속된 Android 클래스에도 주석을 달아야 한다. 예를 들어 Activity에 종속된 Fragment의 경우이다.
의존성을 주입하기 위해선 사용되는 항목의 인스턴스 제공 방법을 알아야 한다.
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
@Inject
주석을 사용하여 인스턴스를 제공하는 방법을 Hilt에 알려준다.
위와 같은 방법으로 진행할 수 없는 경우가 있다. 예를 들어 인터페이스를 생성자에 삽입, 외부 라이브러리의 클래스와 같이 소유하지 않은 경우 불가하다. 이럴 때 Hilt 모듈을 사용하여 해결할 수 있다.
@Module
주석과 @InstallIn
주석을 사용한다.
@Binds를 사용하여 인터페이스 인스턴스 삽입
@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
}
@InstallIn(ActivityComponent::class)
주석은 해당 모듈이 모든 Activity
에서 사용할 수 있음을 의미한다.
@Provides를 사용하여 인스턴스 삽입
소유하지 않은 클래스의 경우( 외부 라이브러리 Room, Retrofit 등), 빌더 패턴으로 인스턴스를 생성해야 하는 경우 사용된다. @Provides
주석을 사용하여 인스턴스 제공 방법을 작성한다.
@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)
}
}
Application이나 Activity의 Contect
가 필요할 수 있으므로 @ApplicationContext
및 @ActivityContext
한정자를 제공한다. 아래와 같이 사용할 수 있다.
class AnalyticsAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: AnalyticsService
) { ... }
Android 클래스마다 @InstallIn
주석에 참조할 수 있는 Hilt Component가 있다. 이전에 봤던 ActivityComponent
이 그 예이다.
Component lifetimes
구성요소의 생명주기에 따라 인스턴스를 자동으로 만들고 제거한다.
Component scopes
이전 예제에서의 AnalyticsAdapter
의 경우 별다른 scope를 설정해 주지 않았기 때문에 AnalyticsAdapter
의 인스턴스를 제공할 때마다 새 인스턴스를 제공한다. 이때 scope를 지정하여 특정 Component의 생명주기 동안에 동일한 인스턴스를 제공할 수 있다.
아래와 같이 @ActivityScoped
주석을 사용하면 해당 Activity의 생명주기 동안 동일한 인스턴스를 제공하게 된다.
@ActivityScoped
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
이번에는 DI란 무엇이고 왜 필요한지, 안드로이드에서 DI를 사용할 때 효과적인 Hilt의 사용법에 대해 간단하게 알아보았다. 다음 포스팅에서는 이번에 배운 Hilt를 사용하여 간단한 앱을 만들어 보려고 한다.