프로그래밍/Android

[안드로이드] Koin에서 Hilt로, Hilt 배워보기

Lou Park 2021. 10. 9. 23:15

단지 쉽다는 이유만으로 Koin 라이브러리를 사용하고 있었는데 최근 앱의 리팩토링 고민을 하면서, 다른 프로젝트 코드들을 읽어보다가 내가 Hilt에 대해서 너무 모르는거 아닌가라는 생각이 들었다.  참고자료에 있는 Droid Knights 2020 영상에 따르면 상위 10,000개의 안드로이드 앱 중 74%가  Dagger를 사용하고 있다고 한다... github에 올라온 많은 프로젝트들도 Dagger/Hilt를 사용하고있으니 이번기회에 확실히 짚고 넘어가면 좋을 것 같다.

 


 

Koin의 특징

Koin은 확실히 쉽다. Hilt가 쉬워졌다고 하지만 Koin급은 아니다.

그리고 어노테이션을 사용하지 않고 Stub 파일을 만들지 않기 때문에 빌드 시간이 빠르다.

하지만 런타임에 바이트 코드를 생성하기 때문에 Hilt에 비교하면 런타임엔 퍼포먼스가 떨어질 수 있다.

 

Hilt의 특징

Hilt는 컴파일시에 Stub 파일을 생성하고 그래프를 구성하는데 그래서 빌드 시간은 Koin에 비해 상대적으로 느리지만 런타임에는 쾌적하다. AndroidX 라이브러리와도 호환되며, Koin에 비해서 Android Component별 Scope가 명확하다.

 

Hilt 컴포넌트 계층

주입된 객체들은 Component별 생명주기에 따라 생성되고 제거된다.

방향이 상위에서 하위 컴포넌트로 화살표가 흐르고 있는데, 하위 컴포넌트에서는 상위 컴포넌트의 객체에 접근할 수 있다.

사진에서 각 Component위에있는 @이 바로 Scope인데 Scope를 설정하게되면 Scope의 범위에서 동일 인스턴스를 공유한다.

 

ApplicationComponent = SingletonComponent

 

Hilt Annotations

Dagger 부터 이어져온 @의 향연...아직 그치지않았다. 하지만  Hilt에서는 Annotation이 사용하기 쉽게 정리되었다고 한다.

 

@HiltAndroidApp

Hilt를 사용하는 모든 앱은 Application 클래스에 @HiltAndroidApp이라는 어노테이션을 적용해야한다. 

@HiltAndroidApp은 적용된 Application 클래스를 포함한 Hilt의 코드 생성을 트리거 시킨다.

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

 

@AndroidEntryPoint

Activity, Fragment, View, Service, BroadcastReceiver 같은 Android Component에 사용할 수 있는 어노테이션으로, 이를 적용한 컴포넌트 내에서 @Inject가 달린 필드에 의존성 주입을 한다. Application 클래스에 대해서는 @HiltAndroidApp이 대신하고 있다.

 

만약 Fragment에 @AndroidEntryPoint를 지정했을 경우 그 Fragment를 사용하는 Activity에도 @AndroidEntryPoint를 지정해야한다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

 

@EntryPoint

위에서 @AndroidEntryPoint에서 커버할 수 있는 컴포넌트외에서 의존성 주입이 필요한 경우가 있는데 (ContentProvider 등) 이때 @EntryPoint를 이용할 수 있다.

@EntryPoint
@InstallIn(SingletonComponent::class)
interface FooBarInterface {
  @Foo fun getBar(): Bar
}

이렇게 정의한 EntryPoint에 접근하기 위해서 EntryPoints 클래스를 이용해야한다.

val bar = EntryPoints.get(applicationContext, FooBarInterface::class.java).getBar()

 

하지만 Dagger 공식 문서에 따르면 EntryPointAccessors 사용이 더 권장되는 방법이다.

var bar = EntryPointAccessors.fromApplication(context, FooBarInterface::class.java)

 

@Module

인터페이스나, 빌더 패턴을 사용한 경우, 외부 라이브러리 클래스 등등 생성자를 사용할 수 없는 Class를 주입해야 할 경우 @Module에다 객체 생성방법을 정의해서 @Inject가 가능하도록 만들 수 있다.

 

@InstallIn

@Module이나 @EntryPoint 어노테이션과 함께 사용해야하며, @InstallIn을 이용해서 모듈을 어느 컴포넌트에 설치할지 정할 수 있다.

아래 예제는 ActivityComponent에 설치하는 Analytics Module의 예시코드다.

 

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

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

 

@Binds

위 예시코드에  @Binds가 들어간김에 이것도 같이 얘기할텐데...인터페이스의 구현체를 제공해야할 때 사용할 수 있다. Repository 인터페이스를 구현하고 이를 따르는 구현체 RepositoryImpl를 많이 사용하는데 그 패턴맞다.

모듈에 정의한 후에는 아래 코드처럼 구현체의 생성자에서 @Inject를 시켜주면 된다.

class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

 

@Provides

Retrofit이나 Room DB와 같이 빌더패턴을 사용해서 인스턴스를 생성해야할때 @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)
  }
}

 


이제 내가 Koin를 사용하던 프로젝트에서 Hilt로 옮겨보려고 하는데, 기존에 무분별하게 주입시키던걸 생명주기를 생각해서 쪼개고 해야하다보니 쉽지 않은 작업이 될 것 같다. 다 바꾸고나서 여유가 된다면, 뭐 눈에 띄는 차이가 있다면 포스팅 해보도록 하겠다.

 

참고 자료

https://developer.android.com/training/dependency-injection/hilt-android?hl=ko

https://dagger.dev/hilt/entry-points.html

https://www.youtube.com/watch?v=gkUCs6YWzEY 

https://www.youtube.com/watch?v=G2gaUnFGGV0