오늘은 Hilt를 이용한 Dependency Injection에 대해서 정리해 보도록 하겠습니다.

Dependency Injection에 대해 경험이 없으신 분들이라면, 아래 글을 통해서 기본적인 개념에 대해서 이해해 보시면 도움이 될 것 같습니다. 특히 Dependency를 주입해준다는 말의 의미에 대해서 이해해 보아야 합니다.

>> Dependency Injection(DI)에 대해서 알아보자

이제 DI에 대해서 이해가 가신다면, HILT에 대해서 알아보도록 하겠습니다.

 

1. HILT

HILT는 Google의 Dagger를 기반으로 만든 Dependency Injection 라이브러리인데요.

공식문서에는 Hilt의 목표에 대해서 아래와 같이 정리하였는데요.

Kolin이 Kotlin에 특화된 심플한 DI라이브러리라면,

안드로이드 앱이 명시되어 있을 정도로 Android에 특화된 앱이라고 할 수 있겠습니다.

 

 

Android 공식문서에도 Hilt에 대해서 잘 설명해주고 있기때문에 많은 도움을 받을수있습니다.

다만, 주의할 점은 Android 공식문서는 최신화가 느리기 때문에,

아직 Alpha단계로 변화가 많은 Hilt의 라이브러리에 대해서는 공식문서에서 확인이 필요합니다.

간혹, 기존에 사용하던 Annotation이 사용할 수 없는 경우도 있는데요.

공식페이지의 문서를 통해 변경사항을 확인해 보시는 것이 좋습니다.

 

2. Library설정

현재는 2.28버전을 사용하도록 되어 있는데요.

중요한 것은 app레벨이 아닌, 프로젝트 레벨에 dependency를 추가하는 것 입니다.

대부분 라이브러리가 app레벨에 추가하기 때문에 아무생각없이 app레벨에 하게 되는데, 주의가 필요합니다.

아래와 같이 project레벨의 build.gradle에 추가를 해 줍니다.

 

dependencies {
	classpath 'com.google.dagger:hilt-android-gradle-plugin:2.34.1-beta'
}

 

 

이제 app레벨의 build.gradle에 아래와 같이 적용을 해 줍니다.

plugin도 아래와 같이 추가해 주구요.

dependency도 아래와 같이 추가해 줍니다.

kotlin-kapt플러그인은 annotation을 위한 것으로, 이미 추가되어 있는 경우가 많을 것 같습니다.

 

apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.34-beta"
    kapt "com.google.dagger:hilt-android-compiler:2.34-beta"
}

 

참고로 Hilt는 JAVA8을 사용하므로, project에서 java8을 사용한다고 아래와 같이 app레벨의 build.gradle에 추가해 줍니다.

많은 라이브러리들이 Java8의 기능을 사용하므로 이것도 이미 되어있는 분들이 많을 거에요.

 

 

 

3. Application Class 와 SingletonComponent

Hilt를 사용하기 위해서는 ApplicationClass가 필요합니다.

왜냐하면 Hilt라이브러리는 ApplicationClass와 ApplicationContext에 접근해서 많은 일들을 뒤에서 해 주어야 하기 때문입니다.

 

보통 MyApplicatoin 같은 이름을 많이 사용하는데요.

중요한 것은 클래스의 윗부분에 "@HiltAndroidApp" 이라는 Annotation을 추가해 주어야 한다는 것 입니다.

 

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

 

HiltAndroidApp Annotation에 의해서 Application Component가(Singleton Component로 이름바뀜) 생성되게 되는데요.

앱이 살아있는 동안 Dependency를 제공하는 역할을 하는, 애플리케이션 레벨의 Component인 것 입니다.

 

참고로, Application클래스를 생성한 뒤에는 AndroidManifest파일의 application태그에 name을 설정해 주는 것을 잊으면 않되겠지요.

 

<application
  android:name=".MyApplication"
  ...
</application>

 

이렇게 Dependency에 접근할 수 있는 준비를 하는 쉬운 작업이 첫번째 스텝이었습니다.

 

4. 각 클래스 Component생성

위에서 application레벨의 컴포넌트를 annotaiton을 붙여서 생성하였는데요.

Hilt는 컴포넌트들을 이용해서 필요한 Dependency를 클래스에 제공할 수 있게 해 줍니다.

 

Hilt가 Dependency를 주입해 줄 수 있는 클래스의 종류는 다음과 같은 것들이 있는데요.

(참고로 위에서 @HiltAndroidApp으로 추가한 ApplicationClass는 제외하였습니다.)

아래 클래스들에 "@AndroidEntryPoint" annotation을 줕여주면,

Hilt가 해당 클래스에 Dependency를 제공해 줄 수 있는 Component를 생성해 줍니다.

 

  • Activity
    • ComonentAcitivty를 상속받는 Activity만 지원한다.
      • AppCompatActivity가 대표적인 예
  • Fragment
    • androidx.Fragment를 상속받는 Fragment클래스에만 dependency주입가능
  • View
  • Service
  • BroadcastReceiver

 

예를 들면 아래와 같이 Annotation을 붙여서 컴포넌트를 만들고 Dependency를 주입받을 수 있도록 하는 것 이지요.

 

 

 

참고로 생성된 Component들은 아래와 같은 hierarchy를 갖습니다.

당연하게도 ApplicationComponet가 가장 상위에 있겠지요.

아래 표에서도 볼 수 있듯이, 각 Component들은 그 부모로부터 Dependency를 받을 수 있습니다.

ApplicationComponent는 SingletoneComponent로 이름이 바뀌었는데요.

안드로이드 공식문서에는 아직 반영이 되지 않았네요.

 

 

 

또한 해당 Component들은 아래와 같은 함수 호출시점에 생성되고, Destroy되는데요.

이런 것들도 보아두면 좀 더 도움이 되겠습니다.

참고로 ApplicationComponent의 이름은 아래와 다르게,

SingletonComponent로 대체되었으므로 주의가 필요합니다.

 

 

5. Injection

Hilt는 Dependency Graph를 만들어서 필요한 곳에 Dependency를 제공해주는 라이브러리이기 때문에,

어떤 곳에서 해당 Depedency가 필요하다고 Notation이 붙어있다면,

해당하는 객체를 어떻게 생성하는지 Hilt가 알고있어야 합니다.

 

Hilt의 "@Inject" Annotation은 Dependency Graph를 이어주는데요.

Hilt가 Dependency를 제공해서 생성할 객체의 클래스에도 붙이고,

Dependency를 주입받을 객체에도 붙여줍니다.

 

 

Injection은 Install된 Component로부터 Dependecy를 주입하거나 받을 수 있다는 것을 의미하는 것 이지요.

개인적으로는 종이컵 전화기같은 것이라고 생각이 됩니다.

 

이러한 Injection은 크게 두가지로 나누어 볼 수 있습니다.

Field Injection과 Constructor Injection인데요.

Field Injection에 비해서, Constructor Injection은 생성시에 어떤 클래스의 컴포넌트가 필요한지 정확하게 알 수 있는 장점이 있는데요.

의존성을 제공하면서, 필요한 객체가 어떤것들을 요구하는지 알면서 생성하는 것이, 좀 더 명확하고 좋은 코드를 작성하는데 도움이 되기 때문입니다.

 

위에서 정리한 Component들로부터 Dependency를 받기 위해서는,

"@Inject" Annotation을 사용해 주는데요.

이 Annotation을 이용해서 Constructor와 Field Injection이 어떻게 다른지 보도록 하겠습니다.

 

5-1. 간단한 형태의 Field Injection

AnalyticsAdapter객체를 생성하는 아래 클래스를 보도록 하겠습니다.

이 객체가 Hilt에 의해 주입받을 수 있는 객체의 클래스라는 것을 알려주기 위해서 생성자에 "@Inject" annotation을 붙여주었습니다.

 

class AnalyticsAdapter
@Inject
constructor() { 

}

 

이 클래스는 Component 들에서 사용이 가능할텐데요.

대표적으로 @AndroidEntryPoint Annotation을 붙인 Activity에서 사용이 가능하겠지요.

 

아래와 같이, "@Inject" annotation을 붙여서 원하는 field에 객체를 주입받을 수 있게 됩니다.

해당 클래스에서 생성하지 않고도 객체를 주입받아 사용하는 것 이지요.

위에서 선언한 방식이 생성자 Injectoin이라면, 이러한 방식은 Field Injection입니다.

 

@AndroidEntryPoint
class TestActivity : AppCompatActivity() {

  @Inject 
   lateinit var analytics: AnalyticsAdapter
  ...
}

 

 

다만, 주의할 점은, Hilt에 의해서 주입받은 변수 객체는 private할 수 없다는 것입니다.

만약 private filed로 선언하면, compilation error를 보게 됩니다.

 

Hilt는 빌드타임시에 Androdi클래스의 Dagger컴포넌트들을 생성해 줍니다.

그럼, Dagger는 생성된 Dependency그래프를 따라가게 되구요.

클래스들과 그들이 필요로 하는 Dependency들을 주입해 줍니다.

 

5-2. Constructor Injection

의존성이 있는 두가지 클래스를 보도록 할 텐데요.

ATypeClass와 BTypeClass가 있다고 가정해 보겠습니다.

BTypeClass는 "@Inject" Annotation을 붙어서 주입될 객체라고 표시가 되었구요.

ATypeClass도 마찬가지지만, 생성자에서 BTypeClass의 객체를 주입받고 있는 데요.

Constrouctor Injection이 사용되는 것 입니다.

 

Constructor Injection을 사용함으로서, 생성시에 어떤 클래스의 객체가 필요한지 Hilt가 알수 있고, 개발자도 알 수 있겠지요.

 

class ATypeClass
@Inject
constructor(private val bTypeClass: BTypeClass) {
    fun doBtypeTest(): String {
        return bTypeClass.test()
    }
}

class BTypeClass
@Inject
constructor() {
  fun test(): String {
    return "test is done"
  }

}

 

위의 두 클래스를 이용해서, 아래와 같이 field injection으로 객체를 주입받아, 사용할 수 있습니다.

 

@AndroidEntryPoint
class TestActivity : AppCompatActivity() {

  @Inject 
   lateinit var aType: ATypeClass

   overide fun onCreate(savedInstanceState: Bundle?){
        aType.doBtypeTest()
  }

}

 

 

그런데, Construction Injection을 사용할 수 없는 경우들이 존재하는데요.

아래에서 보도록 하겠습니다.

6. Constructor Injection 예외

6-1. Interface를 Construcotr Injection에 사용하는 것은 금지되어 있음

아래와 같이 interface가 있다고 가정해 보겠습니다.

 

interface AInterface {
  fun showString(): String
}

 

위의 Ainterface를 implement하는 ClassA와 ClassB가 존재한다고 가정해 보겠습니다.

이들은 각각 AInterface를 implement할 것인데요.

문제는 interface를 inject하는 Constructor Injection이 사용되었기 때문에 정상적으로 Inject되지 않고 에러가 나게 됩니다.

interface나 interface를 implement하는 객체를 inject할 수 없는 것 이지요.

Hilt가 interface가 implement된 타입의 객체를 어떻게 생성해야 할지 알 수 없기 때문입니다.

 

class ClassA
@Inject
constructor(private  val bTypeVal: AInterface){
 fun doTestA(): String {
   return bTypeVal.doTestB()
 }
}

class ClassB
@Inject
constructor(): AInterface {
  override fun showString(): String {
    return "get Go!"
  }
}

interface AInterface {
    fun showString(): String
}

 

 

6-2. 외부 라이브러리 클래스의 객체를 Inject하는 것은 금지됨

retrofit같은 라이브러리의 객체를 Construcotr Injection하는 것은 금지되어 있습니다.

자신이 만든 클래스가 아닌 곳에 @Inject를 annotation에 추가할 수도 없기 때문에,

마음대로 사용하는 것은 불가능하지요. HILT가 이 객체를 어떻게 만들어야 할지 모르기 때문입니다.

어찌보면 당연하다고 할 수 있는 부분입니다.

 

그럼 위의 두가지 케이스를 해결하기 위해서는 어떤 방법을 써야할까요?

아래에서 알아보도록 하겠습니다.

 

7. Hilt Modules

위에서 했던 것과 같이 @Inject Annotation을 붙이는 방법말고,

Module을 이용해서 Hilt에게 원하는 Dependency를 생성하는 방법을 알려줄 수 있습니다.

 

특히, interface나 외부 라이브러리의 객체처럼, Hilt가 어떻게 객체를 생성해야할지 모르는 경우에는 꼭 필요한 방법입니다.

 

Module을 사용하는 방법은 두가지가 있는데요.

Provides Annnotation과 Binds Annotation을 이용하는 것 입니다

개인적으로는 Provides Annotation을 사용하는 것이 사용하기가 편한 것 같습니다.

보통은 임의의 Module클래스를 생성한 후, 이곳에 Module클래스들을 생성해서 사용하게 됩니다.

 

7-1. Provides

코드를 먼저 보도록 하겠습니다.

먼저, 위에서 보았던 AInterface를 implement한 ClassA와 ClassB가 있다고 하겠습니다.

ClassB의 생성자에 String타입의 cDependency가 인자로 들어간다고 가정해 보겠습니다.

 

 

class ClassA
@Inject
constructor(private  val bTypeVal: AInterface){
 fun doTestA(): String {
   return bTypeVal.doTestB()
 }
}

class ClassB
@Inject
constructor(private val cDependency: String): AInterface {
  override fun doTestB(): String {
    return "get Go ${cDependency}!"
  }
}


interface AInterface {
    fun showString(): String
}

 

 

Module클래스를 생성할 때 가장 먼저 할 것은 "@Module" Annotation을 붙여주는 것 입니다.

그래야 Hilt가 여기가 Module이 있는 곳임을 알 수 있겠지요.

 

다음으로 "@InstallIn" Annotation을 붙여줍니다.

@InstallIn(ActivityComponent::class)는 해당 모듈이 acitivity에서 사용가능하다고 선언하다는 의미입니다.

대부분 Activity와 생명주기를 같이하는 ActivityComponent에서 사용되어 지겠지요.

다른 Component에서 사용가능하다로고 하려면 해당 Component의 이름을 넣어주면 됩니다.

(Component들은 위의 4번의 Component에 대한 설명을 참조해주세요.)

예를 들어, ViewModel클래스라면 ActivityRetainedComponent가 되겠지요.

중요한 점은, Hilt에게 이 Class의 객체가 어디에서 사용되는지를 알려주는 것 입니다.

 

이제 Module Class내부에 Provides함수들을 넣어주어야 하는데요.

Annotation으로 커뮤니케이션하는 Hilt에게, 아래와 같이 "@Provides" Annotation을 붙여줍니다.

 

위에서는 두가지의존성을 제공해야 하는 것으로 보이는데요.

하나는 cDependency라는 String객체이구요.

또 하나는 AInterface타입의 객체입니다.

둘다 어떻게 만드는지 Hilt는 모르므로, Provides Annotation을 붙여서 각각 알려주어야 겠지요.

 

@Module
@InstallIn(ActivityComponent::class)
class AModule {

   @Provides
   fun provideCString(): String {
      return "c String"
   }

  
    @Provides
    fun testProvides(cString: String): AInterface {
       return ClassB(cString)
    }
}

 

Provide방식을 이용하면, Retrofit이나 Gson과 같은 외부라이브러리객체의 경우도 아래와 같이 Provide를 이용해서 Dependency를 제공할 수 있습니다.

 

@Module
@InstallIn(SingletonComponent::class)
object TestModule {
    @Singleton
    @Provides
    fun provideAService(): AService {
       return Retrofit.Builder()
                 .baseUrl("https://a.com")
                 .build().create(AService::class.java) 
    }

  @Singleton
  @Provides
   fun provideGson(): Gson {
      return Gson()
   }
}

 

 

7-2. 같은 타입의 객체에 대한 Dependency

위에서 사용한 String타입의 경우, 2,3개가 사용될 경우, Hilt에게 어떻게 말해주어야 할까요?

같은 String타입일 경우, 어떤 Type의 객체를 Inject할지 Hilt가 알수 없을텐데요.

 

예전에 Dagger에서는 "@Named("Identifier")" Annotation을 사용해 주었었는데요.

id를 붙여서 구분시켜 준 것이지요.

Hilt도 비슷한 방식을 사용하여서 같은 타입객체에 대하여서 구분을 해 줄 수 있습니다.

 

아래와 같이 "Qualifier" 구분을 해주면 되는데요.

"@Qualifier @Retention(AnnotationRetention.BINARY)" Annotation을 붙여서 구분할 Identifier라고 Hilt에 알려주구요.

키워드로 annotation class를 붙인다음 이름을 정해줍니다.

아래에서는 TestType1과 TestType2를 사용하였습니다.

이제 Hilt는 이 Qualifier를 따라서 Dependency를 주입해 주겠지요.

 

@Module
@InstallIn(SingletonComponent::class)
object TestModule {

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

   @Qualifier
   @Retention(AnnotationRetention.BINARY)
    annotation class TestType2
  
    @TestType1
    @Provides
    fun testProvides1(): AInterface {
       return ClassB()
    }

   @TestType2
   @Provides
    fun testProvides2(): AInterface {
       return ClassB()
    }

}

 

 

위에서 만든 Qualifier를 사용해 보겠습니다.

ClassA는 AInterface타입으로 객체를 두개 받습니다.

Hilt입장에서는 어떤 객체를 연결시켜주어야 하는지 모르게되는데요.

생성자의 인자들 앞에 보시면 "@TestType1"과 "@TestType2"를 붙여주었습니다.

각각 다른 identifier를 붙여주어서 구분해 준 것입니다.

 

class ClassA
@Inject
constructor(
    @TestType1 private  val bTypeVal1: AInterface,
    @TestType2 private  val bTypeVal2: AInterface,
){
    fun doTest1(): String {
        return bTypeVal.doTestB()
     }
    fun doTest2(): String {
         return bTypeVal2.doTestC()
     }
}

class ClassB
@Inject
constructor(): AInterface {
    override fun doTestB(): String {
         return "First Implementation"
    }
}

class ClassC
@Inject
constructor(p): AInterface {
  override fun doTestC(): String {
    return "Second implementation"
  I
}


interface AInterface {
    fun showString(): String
}

 

위와 같이해주면, Hilt가 어떤 Dependency를 어디에 연결해서 주입해야되는지를 알게 되는 것 이지요.

Provide에 대해서는 정리해 보았는데요. Binds방식에 대해서도 알아보도록 하겠습니다.

 

7-3. Binds

이번에는 Binds방식으로는 어떻게 하는지 보도록 하겠습니다.

참고로 Binds방식은 외부라이브러리에는 사용할 수 없습니다.

interface타입의 객체를 어떻게 만드는지 Hilt에게 알려주기 위한 용도로 사용하는 것 이지요.

 

먼저 ClassA가 Constructor Injection을 사용하고 있고,

아래의 AInterface를 implement하고 있기 때문에 compile에러가 난다고 했었는데요.

(Hilt는 Interface가 implement된 타입이 Constructor Injection된 경우 어떻게 생성해야 하는지 모른다고 했었지요.)

 

class ClassA
@Inject
constructor(private  val bTypeVal: AInterface){
 fun doTestA(): String {
   return bTypeVal.doTestB()
 }
}

class ClassB
@Inject
constructor(): AInterface {
  override fun showString(): String {
    return "get Go!"
  }
}

interface AInterface {
    fun showString(): String
}

 

Provides와 마찬가지로 "@Module" Annotation을 붙여주구요.

abstract class를 만든다음, abstract 함수를 정의해 주면 됩니다.

Provide때와 마찬가지로 "@Binds" Annotation을 붙여주는데요.

 

그리고 필수는 아니지만, AcitivityScope를 의미하는 @ActivityScoped를 붙여서 Scope까지 정의해 주었습니다.

Injecting한 Dependency를 언제까지 사용할 것인지 정의하는 것 이지요.

다만 주의할 점은, Module에서 정의한 InstallIn의 Component의 Scope의 범위를 여기서 정의한 Scope가 벗어나면 않됩니다.

(Scope에 관한 부분은 7번 Scope를 참조해주세요)

 

아래와 같이 해주면, 이제 Hilt가 AInterface타입의 객체는 어떻게 생성하여야 하는지 알게되는 것 이지요.

AInterface타입이지만, classB에서 객체를 생성해야 한다고 알려주는 것이지요.

아래에서는 ClassB타입의 객체가 되겠지요.

 

@Module
@InstallIn(ActivityComponent::class)
abstract class AModule {
      @ActivityScoped
      @Binds
      abstract fun bindInterfaceDependency(testClassB: ClassB): AInterface
}

 

위와 같이 abstract class타입의 Module을 생성해서 함수를 만들어 넣어주면 되는 것 인데요.

핵심은 interface처럼 어디서 객체를 생성해야 할지 모르는 Hilt에게, 어떤 클래스로부터 가져온 객체라고 알려주는 것입니다.

다만 위에서 언급한데로, 이 방법은 Provide와는 달리 외부 라이브러리 객체에 대해서는 동작하지는 않으므로 사용에 주의가 필요합니다.

 

8. Scope

Scope에 대해서도 살펴볼 필요가 있는데요.

Hilt에서는 각 Class들에 대응하는 Component와 Scope를 같이 유지함으로써,

매번 객체를 주입할 때마다 새로운 객체를 생성하는 것이 아닌, 해당 Scope내에서 사용할 수 있도록 하고 있습니다.

Activity보다 오래 살아남는 ViewModel의 경우는 ActivityRetainedComponent와 ActivityRetainedScope를 가지게 되는 것 이지요.

 

 

 

Class를 생성하면서 Scope를 위의 표에 있는 것으로 설정해 주면,

해당 Scope의 주기를 따라가게되겠지요.

해당 객체가 Fragment가 살아있는 동안에만 쓰인다면,

"@FragmentScoped"로 Scope를 Annotation에 명시해주면 더욱 좋겠지요.

 

참고로, Activity에서 주입될 객체를 생성하는 클래스에 @FragmentScoped를 붙인다면 당연하게도 에러가 나게 되겠지요.

 

그리고, Scope에 대해서 아무것도 Annotation을 붙이지 않는다면 어떻게 될까요?

해당 Class는 생성시마다 계속 새로 생성되게 되겠지요.

 

9. Hilt와 ViewModel

Koin도 ViewModel에서 Inject하기 쉬운 방법을 제공했었는데요.

Hilt도 마찬가지입니다. Annotation만 붙여주면 되는데요.

다만 그전에 라이브러리를 implement 해 주어야 합니다.

 

아래의 라이브러리를 implementation해 주어야 합니다.

fragment-ktx는 관련이 없어보이지만 필요하니 잊지말고 추가해주어야 합니다.

 

implementation "androidx.fragment:fragment-ktx:1.3.2"

 

이제 "@ViewModelInject"라는 annotation과,

"@Assisted"라는 Annotation과 같이 constructor의 인자에 savedStateHandle을 아래와 같이 넣어주기만 하면 됩니다.

라이브러리가 업데이트 되면서 "@HiltViewModel" 로 Annotation Name이 바뀌었네요.

이제 "@Inject"는 필요없어졌구요. savedStateHandle도 필요한 경우에만 사용해주면 됩니다.

아래와 같이 해주면 되는군요.

 

 

 

위와 같이 ViewModel클래스를 넣어 주었다면,

@AndroidEntryPoint Annotation이 붙은 Activity나 Fragment에서 아래와 같이 바로 사용이 가능합니다.

만약 viewModels()가 활성화되지 않았다면, 위에서 필요로하는 라이브러리 중 하나가 빠지지 않았는지 확인해 보아야 합니다.

 

 

10. 정리

지금까지 DI라이브러리인 HILT에 대해서 정리해 보았습니다.

아직 정식버전의 라이브러리가 나오지는 않았는데요.

계속 개선될 것으로 보이구요.

이글에서 변경사항은 계속 업데이트하도록 하겠습니다.

 

728x90
  1. bumjae 2021.08.26 18:27 신고

    궁금한게 있습니다.
    코드 사진들의 클래스B나 클래스C는 AInterface를 사용하고 있는데 오버라이드 되는 doTestB()나 doTestC()는 뭔지 알수 있을까요

+ Recent posts