Hilt와 의존성 주입

Hilt

  • Hilt는 의존성 주입을 위한 라이브러리입니다.

의존성 주입

  • 객체 간의 의존성을 외부에서 주입하여 코드의 결합도를 낮추고 테스트 가능성을 높이는 방법입니다.
  • 객체 생성과 의존성 관리를 프레임워크나 DI 컨테이너에서 처리하도록 위임합니다.

Dagger

  • Hilt는 Dagger를 기반으로 빌드되었습니다.
  • Dagger가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성을 사용할 수 있으며, 안드로이드 의존성이 있다는 특징이 있습니다.

Hilt 애플리케이션

  • Hilt를 사용하는 모든 앱은 @HiltAndroidApp으로 주석이 지정된 Application 클래스를 포함해야 합니다.
  • 애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 포함하여 Hilt의 코드 생성을 트리거합니다.
@HiltAndroidApp
class ExampleApplication: Application()

Android 클래스에 종속 항목 삽입

  • Application 클래스에 Hilt를 설정하면, Hilt는 @AndroidEntryPoint 주석이 있는 다른 Android 클래스에 종속 항목을 제공할 수 있습니다.
  • @Inject 주석을 사용하여 필드 삽입을 진행할 수 있습니다.
// AppCompatActivity와 같은 ComponentActivity를 확장하는 활동 지원
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity(){
    @Inject lateinit var repository: SampleRepository

}

Android 클래스

  • Hilt는 다음과 같은 Android 클래스를 지원합니다.
    • Application (with HiltAndroidApp)
    • ViewModel (with HiltViewModel)
    • Activity
    • Fragment
    • View
    • Service
    • BroadcastReceiver

안드로이드 클래스의 예외 사항

  • Hilt는 AppComatActivity와 같은 ComponentActivity를 확장하는 컴포넌트만 지원합니다.
  • androidx.Fragment를 확장하는 프래그먼트만 지원하며, 보존된 프래그먼트(Activity 재생성 시에도 인스턴스가 유지되는 Fragment)를 지원하지 않습니다.

Hilt bindings 정의

  • 필드를 삽입하기 위해 Hilt가 해당 컴포넌트에 필요한 종속 항목의 인스턴스를 제공하는 방법을 알아야 합니다.
  • constructor injection를 통해 Binding을 정의할 수 있습니다.
  • 클래스의 생성자에서 @Inject 주석을 사용하여 클래스의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다.
class SampleRepository @Inject constructor(
    private val service: SampleService,
)
  • 주석이 지정된 클래스 생성자의 매개변수는 그 클래스의 종속 항목이 됩니다.
  • 이를 제공하기 위해서는 @Inject 주석을 통해 Hilt에 SampleService의 인스턴스를 제공하는 방법을 알려야 합니다.

빌드 시간에 Hilt는 Android 클래스용 Dagger 구성요소를 생성하여 Dagger 코드를 검증합니다. 그 후 종속 항목 그래프를 빌드하여 유효성을 검사합니다. 런타임 시 실제 객체 및 종속 항목을 만드는 데 사용되는 클래스 인스턴스를 생성합니다.

Hilt 모듈

  • 다음과 같은 경우에 생성자 삽입이 어려울 수 있습니다.
    • 인터페이스를 삽입하는 경우
    • 외부 라이브러리의 클래스를 삽입하는 경우
  • 이럴 때 Hilt 모듈을 활용하여 Hilt에 binding 정보를 제공할 수 있습니다.
  • Hilt 모듈은 @Module 주석으로 지정이 가능하며, 특정 유형의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다.

InstallIn

  • @InstallIn 주석을 지정하여 각 모듈을 사용하거나 설치할 Android 클래스를 Hilt에 알려야 합니다.

Binds

  • @Binds 주석은 인터페이스 인스턴스 삽입에 사용할 수 있습니다.
interface SampleService {
    fun service()
}

class SampleServiceImpl(

) : SampleService{
    override fun service() {
        TODO("Not yet implemented")
    }
}
  • 추상 함수를 생성하여 Hilt에 결합 정보를 제공합니다.
    • 함수 반환 유형은 함수가 어떤 인터페이스의 인스턴스를 제공하는지 알려줍니다.
    • 함수 매개변수는 제공할 구현을 Hilt에 알려줍니다.
@Module
@InstallIn(ActivityComponent::class)
abstract class SampleModule {
    @Binds
    abstract fun bindSampleService(
        sampleServiceImpl: SampleServiceImpl
    ): SampleService
}
  • @InstallIn(ActivityComponent::class) 주석을 통해 모듈의 모든 종속 항목을 앱에서 사용할 수 있습니다.

Provides

  • 클래스가 외부 라이브러리에서 제공되어 클래스를 소유하지 않은 경우 활용할 수 있습니다.
  • 빌더 패턴으로 인스턴스를 생성해야 하는 경우가 대표적입니다.
    • Room 데이터 베이스
    • Retrofit, OkHttpClient
  • Provides 쿼리를 활용하는 경우 Hilt에 다음을 제공해야 합니다.
    • 함수 반환 유형 : 함수가 어떤 유형의 인스턴스를 제공하는지
    • 함수 매개변수 : 해당 유형의 종속 항목을 Hilt에 알려주어야 함
    • 함수 본문 : 해당 유형의 인스턴스를 제공하는 방법을 Hilt에 알려주어야 함
      • Hilt는 함수 본문을 실행하여 해당 유형의 인스턴스를 제공
@Module
@InstallIn(ActivityComponent::class)
object SampleProvideModule {
    @Provides
    fun provideService(): SampleService{
        return Retrofit.Builder()
            .baseUrl("https//sample.com")
            .build()
            .create(SampleService::class.java)
    }
}
  • 추상 클래스가 아닌 object 제공
  • 함수 반환 유형, 매개변수, 본문 구성

동일한 유형에 대한 bindings 제공

  • 종속 항목과 동일한 유형의 다양한 구현이 필요한 경우가 있습니다.
  • 이런 경우 여러 경합을 제공해야 하는데, 한정자(@Qualifier)를 활용해야 합니다.

Qualifier

  • OkHttpClient 객체를 2가지 유형으로 제공할 수 있습니다.
    • 호출을 가로채서 인터셉터를 구현하는 경우
    • 클라이언트만 제공하는 경우
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
  • Binds 혹은 Provides 메서드에 주석을 지정하는 데 사용할 한정자를 제공할 수 있습니다.

Module

  • 각 메서드는 동일한 반환 유형을 가지지만, Qualifier에 따라 다른 결합을 지원할 수 있습니다.
class OtherInterceptor() : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        TODO("Not yet implemented")
    }
}

class AuthInterceptor() : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        TODO("Not yet implemented")
    }
}

@Module
@InstallIn(ActivityComponent::class)
object SampleProvideModule {
    @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()
    }
}
  • 필드 또는 매개변수에 해당 한정자로 주석을 지정하여 필요한 유형을 삽입할 수 있습니다.
    @Provides
    fun provideSampleService(
        @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
    ): SampleService{
        return Retrofit.Builder()
            .baseUrl("<https://example.com>")
            .client(okHttpClient)
            .build()
            .create(SampleService::class.java)
    }

특정 유형 삽입

  • 한정자를 사용해 생성자나 필드로도 선언이 가능합니다.
// As a dependency of a constructor-injected class.
class ExampleServiceImpl @Inject constructor(
  @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...

// At field injection.
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {

  @AuthInterceptorOkHttpClient
  @Inject lateinit var okHttpClient: OkHttpClient
}

사전 정의 한정자

  • Hilt에서는 사전 정의된 한정자를 제공합니다.
  • 애플리케이션이나 액티비티의 Context가 필요할 수 있으므로 @ApplicationContext 혹은 @ActivityCountext를 제공할 수 있습니다.
class SampleAdapter @Inject constructor(
    @ActivityContext private val context: Context
) {
}
  • 이 외에 다양한 사전 정의된 결합을 사용할 수 있습니다.

Hilt를 사용한 종속 항목 삽입  |  Android Developers

Android 클래스 컴포넌트

  • 의존성 주입을 실행할 수 있는 각 Android 클래스마다 @InstallIn 주석에 참조할 수 있는 관련 컴포넌트를 확인할 수 있습니다.
  • Hilt 아래와 같은 컴포넌트를 제공합니다.

Hilt 구성요소 주입 대상

SingletonComponent Application
ActivityRetainedComponent N/A
ViewModelComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent @WithFragmentBindings 주석이 지정된 View
ServiceComponent Service
  • Hilt는 SingletonComponent에서 직접 broadcast receiver를 삽입하므로, broadcast receiver에 대한 컴포넌트를 생성하지 않습니다.

컴포넌트 라이크 사이클

  • Android 클래스의 생명 주기에 따라 생성된 컴포넌트 클래스의 인스턴스를 자동으로 생성 후 제거합니다.

생성된 컴포넌트 생성 위치 소멸 위치

SingletonComponent Application#onCreate() Application 소멸됨
ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
ViewModelComponent ViewModel 생성됨 ViewModel 소멸됨
ActivityComponent Activity#onCreate() Activity#onDestroy()
FragmentComponent Fragment#onAttach() Fragment#onDestroy()
ViewComponent View#super() View 소멸됨
ViewWithFragmentComponent View#super() View 소멸됨
ServiceComponent Service#onCreate() Service#onDestroy()

범위 스코프

  • 일반적으로 Hilt의 모든 bindings는 범위가 지정되지 않습니다.
  • 앱이 결합을 요청할 때마다 필요한 유형의 새 인스턴스를 생성합니다.
  • 특정 스코프를 지정하여, 지정된 구성요소의 인스턴스마다 결합을 생성할 수 있습니다.
  • 이 결합에 관한 모든 요청은 동일한 인스턴스를 공유하게 됩니다.

Android 클래스 생성된 구성요소 범위

Application SingletonComponent @Singleton
Activity ActivityRetainedComponent @ActivityRetainedScoped
ViewModel ViewModelComponent @ViewModelScoped
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
@WithFragmentBindings 주석이 지정된 View ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped
  • @ActivityScoped를 사용하면 해당 생명 주기 동안 인스턴스를 제공할 수 있습니다.
@ActivityScoped
class SampleAdapter @Inject constructor(
    @ActivityContext private val context: Context
) {
}

스코프를 지정할 경우, 해당 컴포넌트의 생명주기가 유지되는 동안 메모리에 남기 때문에 잘못된 스코프 범위를 지정하면 메모리 낭비가 될 수 있습니다. 애플리케이션에서 범위가 지정된 결합의 사용을 최소화해야 하며, 가장 적은 비용의 결합을 지원해야 합니다.

컴포넌트 계층 구조

  • 컴포넌트에 모듈을 설치하면, 다른 결합 또는 계층 구조에서 하위 구성 요소의 다른 결합의 종속 항목으로 설치된 모듈의 결합에 접근할 수 있습니다.

Hilt Dependency Injection.svg

지원하지 않는 클래스에 주입

  • Hilt는 일반적으로 Android 클래스에 대한 지원을 제공합니다.
  • 하지만 Hilt가 지원하지 않는 서드 파티 라이브러리나, 컨텐트 프로바이더에 주입이 필요한 경우가 있습니다.
  • 이 경우 @EntryPoint를 활용할 수 있습니다.
  • 진입점을 통해 Hilt는 Hilt가 관리하지 않는 코드를 사용하여 종속 항목 그래프 내에서 종속 항목을 제공할 수 있습니다.
class ExampleContentProvider: ContentProvider() {
    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface ExampleContentProviderEntryPoint {
        fun service() : SampleService
    }
}

Hilt와 Dagger

  • Hilt는 Dagger 종속 항목 삽입 라이브러리를 기반으로 빌드되어 Dagger를 안드로이드 애플리케이션에 통합하는 표준 방법을 제공합니다.
    • Android 앱을 위한 Dagger 관련 인프라 간소화
    • 앱 간의 설정, 가독성 및 코드 공유를 용이하게 하기 위한 표준 구성요소 및 범위 세트 생성
    • 테스트, 디버그 또는 출시와 같은 다양한 빌드 유형에 서로 다른 결합을 프로비저닝하는 쉬운 방법 제공
      • 프로비저닝 : 앱을 빌드하고 배포하기 위해 필요한 설정, 리소스, 인증 정보 준비

Dagger 및 Hilt 코드는 동일한 코드베이스에 공존할 수 있으며, Hilt를 활용하면 Android에서 Dagger의 모든 사용을 관리할 수 있습니다.