봄날은 갔다. 이제 그 정신으로 공부하자

의존성 주입 라이브러리 - 2편 (Dagger Hilt) 본문

Android jetpack

의존성 주입 라이브러리 - 2편 (Dagger Hilt)

길재의 그 정신으로 공부하자 2020. 11. 23. 18:27

해당 글은 android DI 라이브러리 2번째 글로 여기에서는 google에서 최근 새롭게 선보인(2020-06) Dagger Hilt에 대해 설명합니다.

Dagger Hilt와 Koin을 비교하는 것은 별도의 글에서 설명할 예정(안한다는 얘기를 돌려서하는 중...)이며,

여기에서는 Dagger Hilt에 대해서만 설명합니다.

 

Dagger Hilt란?

의존성 주입(Dependency Injection)은 최근 Android 개발 환경에 있어서 가장 주목받고 있는 디자인 패턴 중 하나입니다.

많은 개발자들이 의존성 주입을 위해 Koin과 함께 Google에서 밀어주고 있는 오픈소스 라이브러리 Dagger 사용하고 있습니다.

하지만 Dagger는 annotation processing, 각 annotation에 대한 역할, module & component 간의 관계, scope 개념 등 라이브러리에 대한 많은 이해를 필요로 하므로 처음 접하시는 분들에게는 러닝 커브가 높은 편이고, 프로젝트 상황에 따라

초기 DI 환경을 구축하는데 요구되는 비용이 오히려 manual 한 DI 환경을 구축하는 데 드는 비용보다 훨씬 커질 수도 있습니다.

이러한 여러 가지 모종의 이유로 Kotlin의 언어적 특성을 활용하여 상대적으로 학습하기 쉽고, 사용이 용이한 오픈소스 라이브러리 Koin 또한 많은 인기를 얻고 있습니다.

제가 개발하고 있는 App도 현재 Dagger의 환경 구축 비용 등의 문제로 사용성이 좋은 Koin을 사용하고 있습니다.

 

그런데 최근(2020-06) 기존 Dagger 사용자들의 의견을 수렴한 Google이 기존의 Dagger-Android 보다 초기 구축 비용을 훨씬 절감시킬 수 있고 Android Framework에서 더 강력함을 발휘 할 수 있는 Dagger Hilt를 발표하였습니다.

 

Dagger Hilt는 android 전용 DI 라이브러리로 Dagger2를 기반으로 Android Framework에서 표준적으로 사용되는 DI component와 scope를 기본적으로 제공하여, 초기 DI 환경 구축 비용을 크게 절감시키는 것이 가장 큰 목적입니다.

 

Dagger Hilt는 아래 3가지를 목표로 개발되었습니다.

  • Android 앱용 Dagger 관련 인프라 단순화
  • 설정, 가독성 / 이해 및 앱 간 코드 공유를 쉽게하기 위해 표준 구성 요소 및 범위 집합 생성
  • 다양한 빌드 유형 (예 : 테스트, 디버그 또는 릴리스)에 다른 바인딩을 프로비저닝하는 쉬운 방법을 제공

 

따라서 기존에 불가피하게 작성해야 했던 보일러 플레이트를 대량 줄이고 프로젝트의 전반적인 readability를 향상함으로써, 유지보수 면에서도 큰 이득을 취할 수 있습니다.

그뿐만 아니라, Google에서 전격적으로 지원하는 Jetpack의 ViewModel에 대한 의존성 주입도 별도의 큰 비용 없이 구현할 수 있습니다.

아직은 alpha 초기 버전이라 real project에서 사용됨에 따라 다양한 이슈들이 발견되고 있지만, 앞으로의 발전이 기대되는 DI 라이브러리입니다.

 

Component hierachy

기존의 Dagger2는 개발자가 직접 필요한 component들을 작성하고 상속 관계를 정의했다면, Hilt에서는 Android 환경에서 표준적으로

사용되는 component들을 기본적으로 제공하고 있습니다.

또한 Hilt 내부적으로 제공하는 component들의 전반적인 라이프 사이클 또한 자동으로 관리해주기 때문에 사용자가 초기 DI 환경을 구축하는데 드는 비용을 최소화하고 있습니다.

아래는 Hilt에서 제공하는 표준 component hierarchy 입니다.

 

Hilt에서 표준적으로 제공하는 Component 관련 Scope, 생성 및 파괴 시점은 아래와 같습니다.

각 component 들은 생성 시점부터 파괴되기 이전까지 member injection이 가능하며, 각 컴포넌트는 자신만의 lifetime을 갖습니다.

Hilt에서는 아래 표와 같이 component scope를 제공하고 있습니다. 

 

새로운 component를 정의하고 싶다면 @DefineComponent어노테이션을 사용하여 사용자 정의가 가능합니다.

아래는 LoggedUserScope라는 사용자 scope를 정의하고, 해당 scope를 사용하여 UserComponent라는 새로운 component를 만든 예시 입니다.

@Scope
@MustBeDocumented
@Retation(value = AnnotationRetention.RUNTIME)
annotation class LoggedUserScope

 

@LoggedUserScope

@DefineComponent(parent = ApplicationComponent::class)

interface UserComponent{

    // Builder to create instances of UserComponent

    @DefineComponent.Builder

    interface Builder{

        fun setUser(@BindsInstance user: User): UserComponent.Builder

        fun build(): UserComponent

    }

}

@DefineComponent 어노테이션에서 예상할 수 있듯이, 사용자 정의되는 component들은 반드시 표준 컴포넌트 중 하나를 부모 컴포넌트로 상속받아야 합니다.

 

사용자 component는 반드시 leaf component로써 표준 component에 추가될 수 있으며, 2개의 component를 상속 받는 것은 불가능합니다.

 

Hilt Modules

위 설명한 것과 같이 Hilt는 기본 제공하는 component들이 있으므로 @InstallIn 어노테이션을 사용하여 표준 component에 module들을

install할 수 있습니다. Hilt에서 제공하는 기본적인 규칙은 모든 module에 @InstallIn 어노테이션을 사용하여 어떤 component에 install할지 반드시 정해주어야 합니다.

아래 예시는 FooModule이라는 module을 ApplicationComponent에서 install하고, ApplicationComponent에서 제공해주는 Application class를 내부적으로 활용하고 있습니다.

@Module
@InstallIn(ApplicationComponent::class)
object class FooModule{
    // @InstallIn(ApplicationComponent::class) module providers have access to this Application binding.
    @Provides
    fun provideBar(app: Application): Bar {

        ...

    }

}

만약 하나의 module을 다중의 component에 install하고 싶다면 아래와 같이 여러 개의 component를 install할 수 있습니다.

@InstallIn({ViewComponent::class, viewWithFragmentComponent::class})

이처럼 다중 component에 하나의 module을 install하는데는 아래와 같은 세 가지 규칙이 있습니다.

  • Provider는 다중 component가 모두 동일한 scope에 속해있을 경우에만 scope 지정이 가능합니다.
  • Provider는 다중 component가 서로 간 요소에 접근이 가능한 경우에만 주입이 가능합니다.
  • 부모 component와 자식 component에 동시에 install될 수 없으며, 자식 component는 부모의  module에 접근할 수 있습니다.

 

AndroidEntryPoint

기존 Dagger2에서는 직접 의존성을 주입할 대상을 전부 dependency graph에 지정해주었다면, Hilt에서는 객체를 주입할 대상에게 

@AndroidEntryPoint 어노테이션을 추가하는 것만으로도 member injection을 수행할 수 있습니다.

@AndroidEntryPoint을 추가 할 수 있는 Android Component는 아래와 같습니다.

  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

아래는 간단한 예시 입니다.

@AndroidEntryPoint
class MyActivity: BaseActivity(){
    // Bindings in ApplicationComponent or ActivityComponent
    @Inject lateinit var bar: Bar

 

    override fun onCreate(savedInstanceState: Bundle?){

        // Injection happens in super.onCreate()

        super.onCreate()

        // Do something with bar ...

    }

}

 

EntryPoint

module과 유사하게 InstallIn 어노테이션을 사용하여 install하려는 component를 지정하고 @EntryPoint 어노테이션을 추가합니다.

아래는 @EntryPoint 예시 입니다.

@EntryPoint
@InstallIn(ApplicationComponent::class)
interface RetrofitInterface{

    fun getRetrofit(): Retrofit

}

@EntryPoint 어노테이션을 추가한 RetrofitInterface는 아래와 같이 사용 가능합니다.

@AndroidEntryPoint
class MainActivity: AppCompatActivity(){

    override fun onCreate(savedInstanceState: Bundle?){

        super.onCreate(savedInstanceState)

         

        val retrofit = EntryPoints.get(applicationContext, RetrofitInterface::class.java).getRetrofit()

    }

}

 

jetpack ViewModel

Hilt는 기본적으로 jetpack에서 제공하는 ViewModel에 대한 의존성 주입을 제공합니다.

ViewModel Injection을 사용하기 위해서는 app level의 build.gradle 파일 하단에 아래 의존성을 추가합니다.

implementation "com.google.dagger:hilt-android:2.29-alpha"
implementation 'com.google.dagger:hilt-lifecycle-viewmodel:2.29-alpha'
implementation "androidx.hilt:hilt-lifecycle-viewmodel:2.29-alpha"
kapt "androidx.hilt:hilt-compiler:2.29-alpha"

 

ViewModel Injection

jetpack에서 소개된 ViewModel은 Android SDK 내부적으로 ViewModel에 대한 lifecycle을 관리하고 있습니다.

따라서 ViewModel의 생성 또한 jetpack에서 제공하는 ViewModelFactory를 통해서 이루어져야 합니다.

기존에는 각자 ViewModel 환경에 맞는 ViewModelFactory를 따로 작성하거나 Dagger-Android 개발자들은 ViewModel의 constructor injection을 위한 글로벌한 ViewModelFactory를 작서하여 사용하였습니다.

Hilt에서는 이러한 보일러 플레이트를 줄이기 위한 ViewModelFactory가 이미 내부에 정의되어 있고, ActivityComponent와  FragmentComponent에 자동으로 install됩니다. 

아래는 @ViewModelInject 어노테이션을 사용한 constructor injection 예시 입니다.

class MyViewModel @ViewModelInject constructor(

    private val bar : Bar

    ) : ViewModel(){

 

    ...

}

이렇게 생성된 MyViewModel은 아래와 같이 사용 가능합니다.

@AndroidEntryPoint

class MainActivity: AppCompatActivity(){

 

 

    private val viewModel by viewModels<MyViewModel>()

 

 

    override fun onCreate(savedInstanceState: Bundle?){

        super.onCreate(savedInstanceState)

         

        ...

    }

}

ViewModel에서 SavedStateHandle을 주입 받으려면 아래와 같이 @Assisted 어노테이션이 사용됩니다.

class MyViewModel @ViewModelInject constructor(

    private val bar : Bar

    @Assisted private val savedStateHandle: SavedStateHandle

    ) : ViewModel(){

 

    ...

}

 

정리

Hilt는 현재 alpha 버전으로 개발에 사용하기에는 조금 부족할지 몰라도 기존 Dagger에 비해 사용 편의성 크게 개선되었고

올해 6월 발표된 이후 현재(11/5)기준으로 2.29.1 버전까지 배포되는 등 빠른 버전업 또한 눈여겨 볼만한 부분입니다.

마지막으로 위 언급한 것과 같이 Jetpack과의 호환성을 지원하는 등 android와의 연동이 기대되는 의존성 주입 라이브러리 입니다.

Comments