안드로이드

[Android] 힐트(Hilt)에 대해 알아보자

코딩하는후운 2022. 6. 20. 18:37
반응형

Android 힐트(Hilt)에 대해 알아보자

# Hilt

Hilt는 Google의 Dagger를 기반으로 만든 Dependency Injection 라이브러리.

  • Android에서 DI를 위한 Jetpack의 권장 라이브러리
  • 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써 애플리케이션에서 DI를 실행하는 표준 방법을 정의

 

build.gradle파일에 추가

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

Gradle 플러그인을 적용하고 app/build.gradle파일에 다음 종속 항목을 추가

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

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

Hilt는 자바 8 기능을 사용합니다.

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

 

# Hilt 애플리케이션 클래스

Hilt를 사용하는 모든 앱은 @HiltAndroidApp으로 주석이 지정된 Application클래스를 포함해야 합니다.

@HiltAndroidApp
class ExampleApplication : Application() { ... }
  • 애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 비롯하여 Hilt코드 생성을 트리거합니다.
  • 생성된 이 Hilt 구성요소는 Application객체의 수명 주기에 연결되며 이와 관련한 종속 항목을 제공합니다.

# Android 클래스에 종속 항목 삽입

Hilt는 @AndroidEntryPoint 주석이 있는 다른 Android 클래스에 종속 항목을 제공할 수 있습니다.

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

# Hilt가 지원하는 Android 클래스

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

  • Application (@HiltAndroidApp)


아래 클래스들에 @AndroidEntryPoint Annotation을 붙여주면

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

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

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

 

# 구성요소 계층 구조

각 Component들은 그 부모로부터 Dependency를 받을 수 있습니다.

ApplicationComponent는 SingletoneComponent로 이름이 바뀜

 

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

 

# Injection

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

해당 Dependency가 필요하다고 Notion이 붙어있다면,

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

 

@Inject Annotation은 Dependency Graph를 이어주는데,

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

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

 

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

 

# Constructor Injection

생성시에 어떤 클래스의 컴포넌트가 필요한지 정확하게 알 수 있다. (장점)

- 필요한 객체가 어떤것들을 요구하는지 알면서 생성하는 것이, 명확하고 좋은코드를 작성하는데 도움

# Field Injection

원하는 field에 객체를 주입받을 수 있게 하는 방법

주의 : Hilt에 의해서 주입받은 변수 객체는 private할 수 없다고 함.

 

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

그럼, Dagger는 생성된 Dependency그래프를 따라가고 클래스들과 그들이 필요로 하는 Dependency를 주입해 줍니다.

 

# Constructor Injection 예외

  • Interface를 Constructor Injection에 사용하는 것은 금지
    • Hilt가 interface의 implement된 타입의 객체를 어떻게 생성해야 할지 알 수 없기 때문
  • 외부 라이브러리 클래스의 객체를 Inject하는 것은 금지
    • 자신이 만든 클래스가 아닌 곳에 @Inject를 annotation추라할 수도 없기 때문
    • Hilt가 이 객체를 어떻게 만들어야 할지 모르기 때문

 

# Hilt Modules

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

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

 

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

 

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

# Provides

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

그래야 Hilt가 Module이 있는 곳임을 알 수 있다.

 

다음으로, "@InstallIn" Annotation을 붙여준다.

해당 모듈이 activity에서 사용가능하다고 선언하는 의미 이다.

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

 

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

 

# 같은 타입의 객체에 대한 Dependency

같은 타입일 경우 Dagger에서는 @Named("Identifier")를 사용했었는데,

Hilt에서는 Qualifier로 구분 해준다고함.

위의 스크린샷의 @Named는 잘못 쓰인듯

@Qualifier @Retention(AnnotationRetention.BINARY)를 붙여서 구분할 Identifier라고 Hilt에 알려준다.

실제 사용

 

# Binds

외부 라이브러리에는 사용할 수 없습니다.

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

 

Provides와 마찬가지로 @Module을 붙여주고 abstract class를 만든 다음,

abstract 함수를 정의해 주면 된다.

@Bind를 붙여준다.

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

 

# Provides VS Binds

Binds는 @Provides와 달리 구현체에 @Inject constructor()를 추가해주어야 합니다.

@Provides는 각각 파라미터를 확인하면서 명시해줘야 한다.

@Binds를 이용할 때에는 abstract로 명시만 하면됩니다.

: 오히려 더 간단

개인적인 생각으론 Provides는 객체 생성하는 법까지 명시해주는 것이고, Binds는 알아서 생성해서 넣어(?)주는 느낌

하지만, Binds는 제약조건이 있습니다. (구현체에 @Inject construct()를 명시해줘야 함.)

 

그래서 외부 라이브러리에 사용을 못한다(소스 수정 불가하므로)

 

# Scope

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

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

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

Class를 생성하면서 Scope설정해 주면, 해당 Scope의 주기를 따라가게 된다.

참고로, Activity에서 주입될 객체를 생성하는 클래스에 @FragmentScoped를 붙인다면 에러가 난다.

 

만약, Scope에 대해서 아무것도 Annotation을 붙이지 않는다면?

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

 

# ViewModel

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

Hilt도 마찬가지로 Annotation만 붙여주면 됩니다.

다만 그전에 라이브러리를 implementation을 해주어야 함.

fragment-ktx는 관련이 없어보이지만 추가해줘야 한다고 한다.

 

라이브러리가 업데이트 되면서 @ViewModelInject가 @HiltViewModel로 바뀌었다고 한다.

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

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

 

# Hilt 동작 원리

Hilt는 Dagger에 비해 따로 Component를 만들어줄 필요가 없이, super.onCreate에서 자동으로 주입이 이루어 진다.

이게 어떻게 가능한지 살펴보자!

 

간단한 액티비티 생성

빌드를 해보면 아래와 같은 여러개의 파일들이 만들어 진다.

 

Hilt_{앱이름} 파일엔 @HiltAndroidApp 클래스가 재생성 된다.

: applicationContext를 힐트 모듈에 넣어주는 코드가 추가

: 이러한 과정이 있어서 @ApplicationContext quilifier로 context를 주입 받을 수 있게 해줍니다.

 

{모듈클래스 이름}_Provide{provide 타입}Factory 파일을 보면, 우리가 주입해줄 값들의 인스턴스화가 진행된다.

public static IntWrapper provideInt() {
    return Preconditions.checkNotNullFromProvides(InjectModule.INSTANCE.provideInt());
}

 

Dagger{앱이름}_HiltComponents_SingletonC 파일 : Hilt의 주입 기능들이 이루어 진다.

: 먼저 각각 Provider를 생성해 준다.

private void initialize(final Activity activityParam) {
    this.provideStringProvider = DoubleCheck.provider(
        new SwitchingProvider<String>(singletonC, activityRetainedCImpl, activityCImpl, 0)
    );
    this.provideIntProvider = DoubleCheck.provider(
        new SwitchingProvider<IntWrapper>(singletonC, activityRetainedCImpl, activityCImpl, 1)
    );
}

SwitchingProvider의 인자로 provider의 스코프와 id가 들어간다.

이 provider의 id는

@Override
public T get() {
    switch(id) {
        case 0: // java.lang.String
        return (T) InjectModule_ProvideStringFactory.provideString();
        case 1: // io.github.jisungbin.hiltplayground.IntWrapper
        return (T) InjectModule_ProvideIntFactory.provideInt();

        default: throw new AssertionError(id);
    }
}

provider에서 특정 값을 가져오는데 사용이 된다.

get()은 어디서 사용이 될까?

동일 파일에서 inject{주입될 액티비티명}2라는 함수에서 사용된다.

@Override
public void injectMainActivity(MainActivity mainActivity) {
    injectMainActivity2(mainActivity);
}

private MainActivity injectMainActivity2(MainActivity instance) {                    
    MainActivity_MembersInjector.injectMessage(instance, provideStringProvider.get());
    MainActivity_MembersInjector.injectNumber(instance, provideIntProvider.get());
    return instance;
}

이 함수를 보면 주입될 액티비티를 인자로 받고 있고, 이 액티비티를 inject{주입받은 변수명} 메서드를 통해 사용되고 있다. 

이 메서드는 {주입될 액티비티명}_MemberInjector라는 파일에서 정의가 되어있다.

 

이로써 inject{주입될 액티비티명}2 함수로 의존성 주입이 이루어 지는걸 확인할 수 있다.

 

이 함수는 언제 호출이 될까?

Hilt_MainActivity() {
    super();
    _initHiltInternal();
}

Hilt_MainActivity(int contentLayoutId) {
    super(contentLayoutId);
    _initHiltInternal();
}

private void _initHiltInternal() {
    addOnContextAvailableListener(new OnContextAvailableListener ()
    {
        @Override
        public void onContextAvailable(Context context){
            inject();
        }
    });
}

protected void inject() {
    if(!injected) {
        injected = true;
        ((MainActivity_GeneratedInjector)this.generatedComponent()).injectMainActivity(UnsafeCasts.<MainActivity>unsafeCast(this));
    }
}

 

Hilt_{주입받을 액티비티명} 파일이 컴파일 되면서 @HiltEntryPoint가 붙은 클래스의 부모클래스가 이 클래스로 바이트코드에서 변환이 이루어짐.

이 파일을 보면 클래스가 초기화 되면서 위에서 봤던 inject{주입될 액티비티명}를 호출하는 inject()함수를 호출하게 된다.

이로써 super.onCreate에서 주입이 어떻게 가능한지 알 수 있다.

 

 

 


안드로이드를 위한 의존성 주입 라이브러리
생명주기를 고려하는 의존성 주입

1. 프로젝트 셋팅
2. @HiltAndroidApp
-애플리케이션에 Hilt 코드를 자동으로 생성할 수 있게 한다.

3. Container(as hilt-component)
-필요한 곳에 생명주기를 고려하여 의존성 주입을 하는 기능을 가짐

4. @AndroidEntryPoint(component-scope)
-이 어노테이션이 붙은 안드로이드 클래스는 해당 클래스의 생명주기를 따르는 의존성 컨테이너를 만든다.
ex) fragment에서 사용할 경우
이 fragment를 포함하는 activity에도 @AndroidEntryPoint를 사용해 주어야 한다.

Hilt 예외사항
-액티비티중 ComponentActivity(AppCompatActivity같이)를 상속하는 액티비티만 지원.
-프래그먼트중 androidx의 프래그먼트를 상속한 애들만 지원.

5. @Inject
-field injection
-constructor injection

Field injection
@AndroidEntryPoint
class LogsFragment : Fragment() {
  @Inject lateinit var logger: LoggerLocalDataSource
}
:hilt가 어떻게 주입해야 하는지 알 경우
:private접근자를 사용하면 hilt는 주입을 하지 못한다.

*Hilt는 언제 inject를 시켜줄까?
https://developer.android.com/training/dependency-injection/hilt-android#component-lifetimes

6. 의존성 주입 방식은 2가지 경우가 있다.
1) constructor inject할 수 있는 클래스
: 내가 구현한 클래스
2) constructor inject할 수 없는 클래스
: 인터페이스 or abstract class 구현체(@Module 사용)
: 내가 구현할 수 없는 클래스(3rd party library)(@Module 사용)

1) constructor inject 할 수 있는 경우
:간단하게 구현 클래스에 @Inject constructor(...)를 넣어 주기만 하면 된다.
class DateFormatter @Inject constructor() {

}

//@Singleton : 알고 있는 그대로다 application-level에서 사용할 클래스라는 거다(object,static)

@Inject 하나로 어떻게 의존성 주입을 생성할까
-그 실질적인 구현체(generated package에)를 dagger가 만들어 주입시켜 준다고 한다.

2) constructor inject 할 수 없는 경우
: hilt-module : @Module 과 @InstallIn 어노테이션 사용한 클래스
@Module : hilt-module 임을 가리킴(hilt가 알 수 있게)
@InstallIn : 어느 안드로이드 클래스(activity, fragment etc)를 사용할건지 가리킴(hilt가 알 수 있게)
:dagger에 없던건데 안드로이드 클래스의 생명주기(scope)에 맞게 해당 컴포넌트(안드로이드 클래스와 대응하는 컴포넌트)를 지정하는 어노테이션이다.

//case1 object, @Provides주목
@InstallIn(ApplicationComponent::class)
@Module object DatabaseModule{

  @Provides
  @Singleton
  fun provideDatabase(@ApplicationComponent appContext: Context): AppDatabase{
    return Room.databaseBuilder(
      appContext,
      AppDatabase::class.java, "logging.db"
    ).build()
  }
}

//case2 abstract, @Binds주목
@InstallIn(ActivityComponent:class)
@Module
abstract class LoggingInMemoryModule{

  @Binds
  abstract fun bindInMemoryLogger(impl:LoggerInMemoryDataSource):LoggerDataSource
}

모듈 생성에서 abstract, object(static) 두 경우가 있다.
1) abstarct class - @Binds
:구현체(리턴값)가 되는 파라미터를 하나만 가질 수 있다.
:static이 아니기에 메모리 효율적이라 할 수 있겠다.
:따로 구현이 필요 없을 경우 @Provides대신 사용한다.
:간결하다

2) object - @Provides
:구현에 필요한 파라미터를 여러개 가질 수 있다.(없어도 됨)
:구현 로직을 짤 수 있다.(짜야 함)
:해당 의존성을 사용할 때 매번 호출을 한다.

*abstarct class에서 @Provides를 사용할 수 없고,
object에서 @Binds를 사용할 수 없다.

 

 

참조 : 

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

https://developer88.tistory.com/349?category=367239 

https://yuar.tistory.com/entry/Hilt-%EC%82%AC%EC%9A%A9%EB%B2%95-%EB%B0%8F-Module-Binds-vs-Provides-%EC%A0%95%EB%A6%AC-%EB%B0%8F-%ED%9B%84%EA%B8%B0

https://sungbin.land/di%EB%8A%94-%EC%99%9C-%EC%93%B8%EA%B9%8C-%EB%98%90%ED%95%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9E%91%EB%8F%99%EB%90%A0%EA%B9%8C-482627090a1e

 

 

반응형