Hilt (Dependency Injection)란?

쓰리원·2022년 6월 8일
0

Depedency Injection

목록 보기
4/4
post-thumbnail

선수 지식 : 개념-의존성-주입-DI-Dependency-Injection-2

DI 라이브러리 없이 수동으로 의존성을 주입하면 어떠한 어려운점이 있는지 입니다.

1. Hilt를 사용한 종속 항목 삽입

Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. Hilt는 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써 애플리케이션에서 DI를 사용하는 표준 방법을 제공합니다.

Hilt는 Dagger가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성 및 Android 스튜디오 지원의 이점을 누리기 위해 인기 있는 DI 라이브러리 Dagger를 기반으로 빌드되었습니다.

공식 문서 내용의 이해가 추상적이라서 어려울 수 있습니다. 위의 내용을 풀어서 설명한다면, Hilt가 없으면 종속 항목을 삽입을 할 때 삽입할 클래스를 인스턴스화 하고 그 다음에 그 인스턴스를 순서에 맞게 다른 클래스에 생성자 삽입을 하고 원하는 조건이 충족하면 난 뒤에 최종적으로 실행을 하게 됩니다.

그리고 이 와중에 안드로이드는 생명 주기가 있기 때문에 인스턴스가 사라지지 않게 생명주기 또한 관리하면서 갱신해주거나 메모리 관리를 위해 파괴를 하거나 해야합니다. 그러한 귀찮은 것을 Hilt만 사용하면 아주 쉽게 처리할 수 있는 것 입니다.

Dagger라는 것이 이러한 DI 역할을 했었는데, Dagger는 중간 과정에 복잡한 것들이 있습니다. 그러나 Hilt는 어노테이션을 다는 것으로 Dagger에서 들어가던 보일러 플레이트 코드 노력을 우리가 안해도 됩니다. 그래서 위의 글은 정말 혁신적인것을 설명하고 있다 볼 수 있습니다.

2. 종속 항목 추가

공식문서에 있는 gradle 설정법 입니다. 그대로 따라해 줍니다.

3. Hilt 애플리케이션 클래스

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

@HiltAndroidApp
class ExampleApplication : Application() { ... }

생성된 이 Hilt 구성요소는 Application 객체의 수명 주기에 연결되며 이와 관련한 종속 항목을 제공합니다. 또한 이는 앱의 상위 구성요소이므로 다른 구성요소는 이 상위 구성요소에서 제공하는 종속 항목에 액세스할 수 있습니다.

@HiltAndroidApp을 어노테이션 작성을 해줘야 의존성 주입의 시작점을 지정이 가능해서 컴파일 시점에 DI에 필요한 구성요소들을 초기화 위한 Hilt 클래스들이 생성 됩니다. 그래서 Hilt는 Application 생명 주기를 따르게 됩니다.

4. Android 클래스에 종속 항목 삽입

Application 클래스에 Hilt를 설정하고 애플리케이션 수준 구성요소를 사용할 수 있게 되면 Hilt는 @AndroidEntryPoint 주석이 있는 다른 Android 클래스에 종속 항목을 제공할 수 있습니다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

위의 내용에 예를 들어보면 ViewModel을 만들었다고 가정하겠습니다. 그러면 ViewModel을 사용할 activity가 필요합니다. 이때 ViewModel을 사용하는 activity에 @AndroidEntryPoint를 어노테이션을 달아줘야 사용할 수 있게 됩니다.

Fragment에서 사용한다면 Fragment에도 @AndroidEntryPoint를 달아주고 Fragment가 붙어있는 activity에 @AndroidEntryPoint를 달아 사용할 수 있습니다.

5. Hilt 모듈

생성자 삽입할 수 없는 상황도 있습니다. 이러한 상황은 여러 가지 이유로 인해 발생할 수 있습니다. 예를 들어 인터페이스를 생성자 삽입할 수 없습니다. 또한 외부 라이브러리의 클래스와 같이 소유하지 않은 유형도 생성자 삽입할 수 없습니다. 이럴 때는 Hilt 모듈을 사용하여 Hilt에 결합 정보를 제공할 수 있습니다.

Hilt 모듈은 @Module로 주석이 지정된 클래스입니다. Dagger 모듈과 마찬가지로 이 모듈은 특정 유형의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다. 그러나 Dagger 모듈과는 달리, Hilt 모듈에 @InstallIn 주석을 지정하여 각 모듈을 사용하고 설치할 Android 클래스를 Hilt에 알려야 합니다.

1. hilt @Module 어노테이션

Hilt를 사용할 때 @Module 어노테이션을 사용하는 이유는 Hilt가 DI 컨테이너를 생성하고 의존성을 주입하기 위해 사용하는 객체 그래프를 만들기 위해서입니다.

@Module 어노테이션은 Dagger에서 사용되는 것과 같이 Hilt에서도 사용됩니다. 이 어노테이션을 사용하여 Hilt에 의해 생성되는 컨테이너에 포함될 클래스를 정의합니다. @Module 어노테이션은 클래스가 Hilt에서 주입 가능한 의존성을 제공하는 클래스임을 나타냅니다. @Module 어노테이션은 DI를 위해 필요한 객체를 제공하는 메서드를 포함하는 클래스에 적용됩니다. 이 클래스는 Hilt가 객체를 만들 때 참조하는 구성 모듈로 사용됩니다.

@Module 어노테이션으로 주입 가능한 클래스를 정의하면 Hilt가 이를 바탕으로 객체 그래프를 만들고 필요한 객체들을 주입합니다. 따라서, @Module 어노테이션은 Hilt가 DI 컨테이너를 만들고 객체 그래프를 생성하기 위한 필수적인 어노테이션입니다. @Module 클래스에서는 @Provides 어노테이션을 사용하여 객체를 생성하고 제공할 수 있습니다. 이렇게 생성된 객체는 다른 클래스에서 @Inject 어노테이션을 사용하여 DI할 수 있습니다.

예를 들어, 앱에서 사용하는 데이터베이스 객체를 제공하는 모듈 클래스를 만들고 @Module 어노테이션을 적용할 수 있습니다. 이 클래스에서는 @Provides 어노테이션을 사용하여 데이터베이스 객체를 생성하고 제공합니다. 이렇게 제공된 데이터베이스 객체는 다른 클래스에서 DI할 수 있습니다.

따라서, @Module 어노테이션은 Hilt DI 프레임워크에서 객체를 생성하고 제공하는 역할을 합니다.

2. @Binds를 사용하여 인터페이스 인스턴스 삽입

만약에 예를들어 AnalyticsService가 인터페이스라면 이 인터페이스를 생성자 삽입할 수 없습니다. 대신 Hilt 모듈 내에 @Binds로 주석이 지정된 추상 함수를 생성하여 Hilt에 결합 정보를 제공합니다.

@Binds 주석은 인터페이스의 인스턴스를 제공해야 할 때 사용할 구현을 Hilt에 알려줍니다.

주석이 지정된 함수는 Hilt에 다음 정보를 제공합니다.

  • 함수 반환 유형은 함수가 어떤 인터페이스의 인스턴스를 제공하는지 Hilt에 알려줍니다.
  • 함수 매개변수는 제공할 구현을 Hilt에 알려줍니다.
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의 종속 항목을 ExampleActivity에 삽입하기를 원하기 때문에 Hilt 모듈 AnalyticsModule에 @InstallIn(ActivityComponent::class) 주석을 지정합니다. 이 주석은 AnalyticsModule의 모든 종속 항목을 앱의 모든 Activity에서 사용할 수 있음을 의미합니다.

3. @Provides를 사용하여 인스턴스 삽입

인터페이스가 유형을 생성자 삽입할 수 없는 유일한 경우는 아닙니다. 클래스가 외부 라이브러리에서 제공되므로 클래스를 소유하지 않은 경우(Retrofit, OkHttpClient 또는 Room 데이터베이스와 같은 클래스) 또는 빌더 패턴으로 인스턴스를 생성해야 하는 경우에도 생성자 삽입이 불가능합니다.

@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)
  }
}

4. 동일한 유형에 대해 여러 결합 제공

종속 항목과 동일한 유형의 다양한 구현을 제공하는 Hilt가 필요한 경우에는 Hilt에 여러 결합을 제공해야 합니다. 한정자를 사용하여 동일한 유형에 대해 여러 결합을 정의할 수 있습니다.

한정자는 특정 유형에 대해 여러 결합이 정의되어 있을 때 그 유형의 특정 결합을 식별하는 데 사용하는 주석입니다.

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

그런 다음, Hilt는 각 한정자와 일치하는 유형의 인스턴스를 제공하는 방법을 알아야 합니다. 이 경우에는 @Provides와 함께 Hilt 모듈을 사용할 수 있습니다. 두 메서드 모두 동일한 반환 유형을 갖지만 한정자는 다음과 같이 두 가지의 서로 다른 결합으로 메서드에 라벨을 지정합니다.

@Module
@InstallIn(ApplicationComponent::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()
  }
}

다음과 같이 필드 또는 매개변수에 해당 한정자로 주석을 지정하여 필요한 특정 유형을 삽입할 수 있습니다.

// As a dependency of another class.
@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)
  }
}

// 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가 잘못된 종속 항목을 삽입할 수 있습니다.

5. Hilt의 사전 정의된 한정자

Hilt는 몇 가지 사전 정의된 한정자를 제공합니다. 예를 들어 애플리케이션 또는 활동의 Context 클래스가 필요할 수 있으므로 Hilt는 @ApplicationContext 및 @ActivityContext 한정자를 제공합니다.

예의 AnalyticsAdapter 클래스에 활동 컨텍스트가 필요하다고 가정해 보겠습니다. 다음 코드는 AnalyticsAdapter에 활동 컨텍스트를 제공하는 방법을 보여줍니다.

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

6. Android 클래스용으로 생성된 구성요소

필드 삽입을 실행할 수 있는 각 Android 클래스마다 @InstallIn 주석에 참조할 수 있는 관련 Hilt 구성요소가 있습니다. 각 Hilt 구성요소는 해당 Android 클래스에 결합을 삽입해야 합니다.

Hilt는 다음 구성요소를 제공합니다.

구성요소 전체 기간

Hilt는 해당 Android 클래스의 수명 주기에 따라 생성된 구성요소 클래스의 인스턴스를 자동으로 만들고 제거합니다.

구성요소 범위

기본적으로 Hilt의 모든 결합은 범위가 지정되지 않습니다. 즉, 앱이 결합을 요청할 때마다 Hilt는 필요한 유형의 새 인스턴스를 생성합니다.

이 예에서 Hilt는 다른 유형의 종속 항목으로 또는 필드 삽입을 통해(ExampleActivity에서와 같이) AnalyticsAdapter를 제공할 때마다 AnalyticsAdapter의 새 인스턴스를 제공합니다.

그러나 Hilt는 결합을 특정 구성요소로 범위 지정할 수도 있습니다. Hilt는 결합의 범위가 지정된 구성요소의 인스턴스마다 한 번만 범위가 지정된 결합을 생성하며, 이 결합에 관한 모든 요청은 동일한 인스턴스를 공유합니다.

아래 표에는 생성된 각 구성요소의 범위 주석이 나와 있습니다.

이 예에서 @ActivityScoped를 사용하여 AnalyticsAdapter의 범위를 ActivityComponent로 지정하면 Hilt는 해당 활동의 수명 주기 동안 동일한 AnalyticsAdapter 인스턴스를 제공합니다.

AnalyticsService에 ExampleActivity뿐만 아니라 앱의 모든 위치에서 매번 동일한 인스턴스를 사용해야 하는 내부 상태가 있다고 가정해 보겠습니다. 이럴 때에는 AnalyticsService의 범위를 ApplicationComponent로 지정하는 것이 적절합니다. 결과적으로 구성요소는 AnalyticsService의 인스턴스를 제공해야 할 때마다 매번 동일한 인스턴스를 제공합니다.

다음 예에서는 Hilt 모듈에서 결합의 범위를 구성요소로 지정하는 방법을 보여줍니다. 결합의 범위는 결합이 설치된 구성요소의 범위와 일치해야 하므로 이 예에서는 ActivityComponent 대신 ApplicationComponent에 AnalyticsService를 설치해야 합니다.

// If AnalyticsService is an interface.
@Module
@InstallIn(ApplicationComponent::class)
abstract class AnalyticsModule {

  @Singleton
  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

// If you don't own AnalyticsService.
@Module
@InstallIn(ApplicationComponent::class)
object AnalyticsModule {

  @Singleton
  @Provides
  fun provideAnalyticsService(): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

구성요소 계층 구조

구성요소에 모듈을 설치하면 이 구성요소의 다른 결합 또는 구성요소 계층 구조에서 그 아래에 있는 하위 구성요소의 다른 결합의 종속 항목으로 설치된 모듈의 결합에 액세스할 수 있습니다.

ApplicationComponent
Application 전체의 생명주기를 lifetime으로 갖습니다. Application의 onCreate() 시점에 함께 생성되고 onDestroy() 되는 시점에 함께 파괴되게 됩니다.

ActivityRetainedComponent
ApplicationComponent의 하위 컴포넌트이며 Activity의 생명주기를 lifetime으로 갖습니다. 단, Acitivity의 configuration change(화면 가로/세로 전환 등) 시에는 파괴되지 않고 유지됩니다.

ServiceComponent
ApplicationComponent의 하위 컴포넌트이며 Service의 생명주기를 lifetime으로 갖습니다. Service의 onCreate() 시점에 함께 생성되고 onDestroy 시점에 함께 파괴됩니다.

ActivityComponent
ActivityReatinedComponent의 하위 컴포넌트이며 Activity의 생명주기를 lifetime으로 갖습니다. Activity의 onCreate() 시점에 함께 생성되고 onDestroy() 되는 시점에 함께 파괴됩니다.

ViewModelComponent
Jetpack ViewModel의 생명주기를 갖습니다.

FragmentComponent
ActivityComponent의 하위 컴포넌트이며 Fragment의 생명주기를 lifetime으로 갖습니다. Fragment가 Activity의 붙는 onAttach() 시점에 함께 생성되고 onDestroy() 되는 시점에 함께 파괴됩니다.

ViewComponent
ActivityComponent의 하위 컴포넌트이며 View의 생명주기를 lifetime으로 갖습니다. View가 생성되는 시점에 함께 생성되고 파괴되는 시점에 함께 파괴됩니다.

ViewWithFragmentComponent
FragmentComponent의 하위 컴포넌트이며 Fragment의 view 생명주기를 lifetime으로 갖습니다. View가 생성되는 시점에 함께 생성되고 파괴되는 시점에 함께 파괴됩니다.

7. Hilt가 지원하지 않는 클래스에 종속 항목 삽입

7번은 나중에 사용해야할 일이 생기면 다시한번 내용을 숙지해 보겠습니다.

8. reference

https://developer.android.com/training/dependency-injection/hilt-jetpack?hl=ko
https://developer.android.com/training/dependency-injection/hilt-android#setup
https://dagger.dev/hilt/components
https://thinking-face.tistory.com/entry/Hilt-2-Dependency-Injection-with-Hilt
https://f2janyway.github.io/android/hilt2/

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글