의존성 주입(DI)은 Dependency Injection의 줄임말로, 객체 간의 의존 관계를 외부에서 주입하여 결합도를 줄이고 유지보수성과 테스트 용이성을 향상시키는 데 사용되는 기술을 의미한다.
의존성 주입을 위한 라이브러리는 여러개 있지만 우선 의존성 주입의 정확한 개념 이해를 위해 직접 의존성 주입을 적용하는 예제를 확인해보자.
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array
위의 코드는 의존성 주입을 적용하지 않은 일반 코드이다.
이 코드에서 문제점은 하단의 그림처럼 Car 클래스 안에 Engine이 속하게 되어 두 클래스 사이의 결합도가 높아지는 부분에 있다.

이 그림처럼 구현이 될 경우 Car 인스턴스는 한 가지 유형의 Engine을 사용하므로 서브클래스 또는 대체 구현을 쉽게 사용할 수 없게된다. 또한 Car가 자체 Engine을 구성했다면 Gas 및 Electric 유형의 엔진에 동일한 Car를 재사용하는 대신 두 가지 유형의 Car를 생성해야하는 번거로움과 메모리 낭비 문제가 생기게 된다.
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array
의존성을 주입한 코드는 Car 안에서 Engine을 구현하는 것이 아닌 Engine 객체를 생성자의 매개변수로 받게된다.

상단의 그림처럼 구현한 것을 풀어 설명해보자면, main 함수에서 Car 객체를 사용하고 Car은 Engine에 종속되므로 Engine 인스턴스를 main에서 생성한 후 이를 사용하여 Car 인스턴스를 구성하게 된다. 이 구현 방법이 DI 기반 접근 방법이라고 할 수 있다.
이렇게 의존성을 주입하여 개발하였을 때 이점으로는
1. Car의 재사용 가능 : Engine의 다양한 구현을 Car에 전달할 수 있다. 예를 들어 Car에서 사용할 ElectricEngine이라는 새로운 Engine 서브클래스를 정의할 수 있게됨. DI를 사용한다면 업데이트된 ElectricEngine 서브클래스의 인스턴스를 전달하기만 하면 되며 Car는 추가 변경 없이도 계속 작동.
2. Car의 테스트 편의성 : 테스트 더블을 전달하여 다양한 시나리오를 테스트할 수 있다. 예를 들어 FakeEngine이라는 Engine의 테스트 더블을 생성하여 다양한 테스트에 맞게 구성할 수 있다.
생성자 삽입 : 상단에서 구현해본 방법으로, 클래스의 종속 항목을 생성자에 전달하는 방법.
필드 삽입(또는 setter 삽입) : 활동 및 프래그먼트와 같은 특정 Android 프레임워크 클래스는 시스템에서 인스턴스화하므로 생성자 삽입이 불가능. 필드 삽입을 사용하면 종속 항목은 클래스가 생성된 후 인스턴스화됨.
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array
직접 의존성 주입을 하는 방법을 알아봤으니 이제는 라이브러리를 사용하여 보다 간편하게 의존성 주입을 적용하는 방법에 대해 알아보자.
크게 의존성 주입에 대한 라이브러리는 Dagger, Koin, Hilt 이렇게 세개가 대표적이다.
세개를 다 다뤄보진 않을거고, 일단은 이 게시글에서는 안드로이드 공식 문서에서 권장하고 있는 Hilt을 사용하여 의존성 주입하는 예제에 대해서만 확인해볼 것이다.
Hilt는 Android에서 의존성 주입을 위한 Jetpack의 권장 라이브러리다.
Hilt는 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써 애플리케이션에서 DI를 사용하는 표준 방법을 제공한다.
Hilt는 Dagger가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성 및 Android 스튜디오 지원의 이점을 누리기 위해 인기 있는 DI 라이브러리인 Dagger를 기반으로 빌드되었다.
먼저 hilt-android-gradle-plugin 플러그인을 프로젝트의 루트 build.gradle(Project) 파일에 추가
plugins {
...
id("com.google.dagger.hilt.android") version "2.44" apply false
}
그런 다음, Gradle 플러그인을 적용하고 build.gradle(Module) 파일에 다음 종속 항목을 추가
plugins {
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
...
}
dependencies {
implementation("com.google.dagger:hilt-android:2.44")
kapt("com.google.dagger:hilt-android-compiler:2.44")
}
// Allow references to generated code
kapt {
correctErrorTypes = true
}
Hilt를 사용하는 모든 앱은 @HiltAndroidApp으로 주석이 지정된 Application 클래스를 포함해야 한다.
Hilt 라이브러리는 Application와 ApplicationContext에 접근하기 위해 ApplicationClass가 필요하다.
@HiltAndroidApp
class MainApplication : Application() {
...
}
@HiltAndroidApp 어노테이션에 의해 Singleton Component를 생성하게 된다.
앱이 살아있는 동안 의존성(Dependency)을 제공하는 역할을 하는 애플리케이션(Application) 레벨의 Component가 되는 것이다.
<application
...
android:name=".base.MainApplication" >
...
</application>
MainApplication을 생성 후 매니페스트 파일에 name에 생성한 application을 설정해준다.
Application 클래스에 Hilt를 설정하고 애플리케이션 수준 구성요소를 사용할 수 있게 되면 Hilt는 @AndroidEntryPoint 주석이 있는 다른 Android 클래스에 종속 항목을 제공할 수 있게된다.
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }
Android 클래스에 @AndroidEntryPoint로 주석을 지정하면 이 클래스에 종속된 Android 클래스에도 주석을 지정해야 한다.
구성요소에서 종속 항목을 가져오려면 다음과 같이 @Inject 주석을 사용하여 필드 삽입을 실행
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapter
...
}
참고: Hilt가 삽입한 필드는 비공개일 수 없다. Hilt를 사용하여 비공개 필드를 삽입하려고 하면 컴파일 오류가 발생.
Hilt에 결합 정보를 제공하는 방법은 생성자 삽입으로, 다음과 같이 클래스의 생성자에서 @Inject 주석을 사용하여 클래스의 인스턴스를 제공하는 방법을 Hilt에 알려준다.
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
위의 코드를 해석해보자면 AnalyticsAdapter에는 AnalyticsService가 종속 항목으로 포함되어 있다는 것을 알 수 있다.
인터페이스나 외부 라이브러리를 사용하여 생성자를 사용할 수 없는 경우에는 Hilt 모듈을 사용하여 Hilt에 결합 정보를 제공할 수 있다.
Hilt 모듈은 @Module로 주석이 지정된 클래스로 @InstallIn 주석을 지정하여 각 모듈을 사용하거나 설치할 Android 클래스를 Hilt에 알려야한다. (모든 생성된 구성요소에서 사용할 수 있음)
인터페이스일 경우 생성자를 사용할 수 없기 때문에 Hilt 모듈 내에 @Binds 주석을 사용하여 Hilt에 결합 정보를 제공할 수 있다.
interface AnalyticsService {
fun analyticsMethods()
}
class AnalyticsServiceImpl @Inject constructor(
...
) : AnalyticsService { ... }
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
Hilt가 AnalyticsModule의 종속 항목을 ExampleActivity에 삽입하기를 원하기 때문에 Hilt 모듈 AnalyticsModule에 @InstallIn(ActivityComponent.class) 주석을 지정한다.
이 주석은 AnalyticsModule의 모든 종속 항목을 앱의 모든 활동에서 사용할 수 있음을 의미
외부 라이브러리를 사용하여 클래스를 소유하지 않는 경우(Retrofit, Room과 같은 클래스들)에도 생성자 사용이 불가능하기 때문에 이 경우에는 함수에 @Provides 주석을 지정하여 이 유형의 인스턴스를 제공하는 방법을 Hilt에 알릴 수 있다.
@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)
}
}
종속 항목과 동일한 유형의 다양한 구현을 제공하는 Hilt가 필요한 경우에는 @Qualifier를 사용하여 동일한 유형에 대해 여러 결합을 정의할 수 있다.
AnalyticsService 호출을 가로채야 한다면 인터셉터와 함께 OkHttpClient 객체를 사용할 수 있다. 이런 경우에는 서로 다른 두 가지 OkHttpClient 구현을 제공하는 방법을 Hilt에 알려야 한다.
다음과 같이 @Binds 또는 @Provides 메서드에 주석을 지정하는 데 사용할 @Qualifier를 정의한다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
@Provides와 함께 Hilt 모듈을 사용하여 각 @Qualifier와 일치하는 유형의 인스턴스를 제공하는 방법을 알아낸다.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@AuthInterceptorOkHttpClient
@Provides
fun provideAuthInterceptorOkHttpClient(
authInterceptor: AuthInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
}
@OtherInterceptorOkHttpClient
@Provides
fun provideOtherInterceptorOkHttpClient(
otherInterceptor: OtherInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(otherInterceptor)
.build()
}
}
다음과 같이 필드 또는 매개변수에 해당 @Qualifier로 주석을 지정하여 필요한 특정 유형을 삽입할 수 있다
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
@Provides
fun provideAnalyticsService(
@AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.client(okHttpClient)
.build()
.create(AnalyticsService::class.java)
}
}
class ExampleServiceImpl @Inject constructor(
@AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {
@AuthInterceptorOkHttpClient
@Inject lateinit var okHttpClient: OkHttpClient
}

출저 및 참조 : https://developer.android.com/training/dependency-injection/hilt-android?hl=ko
공식문서 Hilt 사용방법 : https://developer.android.com/codelabs/android-hilt?hl=ko#0
Hilt 사용법 간편 정리 : https://developer88.tistory.com/349
Hilt 사용법 간편 정리2 : https://jminie.tistory.com/182