개발/안드로이드

[Hilt] Hilt의 표준 컴포넌트를 살펴보자

스몰스테핑 2024. 8. 2. 15:26

Hilt의 컴포넌트 계층

 

기존 Dagger와는 다르게 표준 컴포넌트를 제공하기 때문에 개발자가 따로 커스텀 컴포넌트를 만들 필요는 없다.

물론 커스텀 컴포넌트를 정의하는 방법은 있으나 일반적이진 않으며 권장하지 않는 방법이다.

Dagger와 달리 표준 컴포넌트 조차 소스 코드내에서 인스턴스화 하지 않는다.
컴포넌트 계층 이미지에서 볼 수 있는 각 컴포넌트 명칭 위의 스코프 애노테이션은 해당 컴포넌트의 생명주기에 대한 의존성 범위를 지정할때 사용되는 애노테이션이다.

컴포넌트간 화살표 방향은 하위 컴포넌트를 가르키는 것이다.
일반적으로 하위 컴포넌트는 상위 컴포넌트의 바인딩에 접근할 수 있다. 하지만 역방향으로는 불가능하다.
예를들어, FragmentComponent에서 ActivityComponent의 의존성으로 접근하는 것은 가능하다. ActivityComponent에서 FragmentComponent가 가지고 있는 의존성에 접근하는 것은 불가능하다.
이것은 단순히 생각하더라도 Activity 보다도 Fragment의 생명주기가 짧기 때문에 의존 관계를 잘못 설정할 경우 메모리 누수가 발생할 수 있기 때문이다.

이러한 의존 관계는 컴파일 타임에 다 체크하고, 문제 발생시 빌드를 중단하고 런타임의 안전성을 확보한다.

 

안드로이드 컴포넌트에 대응되는 Hilt 컴포넌트

 

애플리케이션 클래스를 위한 싱글톤 컴포넌트를 제외하고는 안드로이드 컴포넌트에서는 일반적으로 @AndroidEntryPoint를 통해서 위와 같은 컴포넌트를 다 생성할 수 있고, 그로 인해서 의존성 주입이 가능해진다.

 

Hilt 컴포넌트의 생명주기

 

Hilt 컴포넌트의 생명주기는 해당 컴포넌트에 바인딩되는 수명과도 깊게 연관된다. 안드로이드 컴포넌트에서 의존성 주입을 시도할때 Hilt 컴포넌트가 인스턴스화 되기 이전 또는 이후에 의존성 주입을 시도하면 Crush가 발생할 수 있다. 이전 포스팅에서 onCreate() 이전에 참조하려다가 실패한 예시를 보인 적이 있다.

그렇기에 컴포넌트의 생성, 소멸 시점을 반드시 알고 있어야한다.

 

Scope vs Unscope

기본적으로 Dagger나 Hilt에서 스코프 애노테이션을 사용하지 않으면 매번 새로 인스턴스를 생성하는 것을 암시한다. 그래서 스코프 지정과 지정하지 않는 것 구분을 명확히 지어놨다.


어떤 스코프 애노테이션을 선언하는 순간, 의존성은 스코프에 해당하는 컴포넌트 생명주기 동안 동일하게 참조된다. 클라이언트 요청에 대해서 동일한 인스턴스를 반환하는데 오해하면 안되는 것이 모든 Fragment가 동일한 인스턴스를 공유하지는 않는다. 왜냐하면 FragmentScope를 활용한 경우 개별 Fragment별로 바인딩 인스턴스를 생성하기 때문이다.


해당 컴포넌트가 동일한 인스턴스를 반환하는 것을 보장하기 때문에 FragmentComponent 하위에 있는 것과는 공유가 되겠으나, Activity내에서 Fragment 2개가 공존하는 경우를 상상해보자. 만약 서로 다른 Fragment끼리 동일한 인스턴스를 공유하고 싶다면 그보다 상위 컴포넌트인 ActivityComponent나 그 상위 컴포넌트들에 바인딩하고 스코프하여 관리해야한다.

 

모듈에서 Scope 지정하기

@Module
@InstallIn(FragmentComponent::class)
object FooModule {
    @Provides
    fun provideUnscopedBinding() = UnscopedBinding()

    @Provides
    @FragmentScoped
    fun provideScopedBinding() = ScopedBinding()
}

 

Hilt의 컴포넌트에 의존성을 바인딩하는 방법은 여러가지가 있으나 앞서 포스팅했던 것처럼 Moudle을 선언하고 Provides 애노테이션을 선언하고 스코프 없이 쓰거나 Scoped 애노테이션을 쓰며 지정할 수도 있다.

 

Scope Annotation 사용은 언제?

  1. 어떤 의존성을 인스턴스화하는 비용이 클 때
  2. 동일한 인스턴스를 반환을 원할 때
  3. 특정 인스턴스를 공유하고 싶을 때

Scope 애노테이션 사용에 대한 결정은 생성되는 코드 사이즈, 메모리 사용량, 런타임 성능 등을 결정하게 된다. 그렇기에 유의해서 사용해야한다.


위와 같을 때 사용하면 좋다. 하나라도 해당이 된다면 Scope 애노테이션 사용을 고려해볼만 하다. 주로 메모리 사용량이나 퍼포먼스가 트레이드 오프 관계이기 때문에 신중히 사용해야한다.

 

기본으로 제공되는 바인딩

 

개별적인 Hilt 컴포넌트들은 몇가지 기본 바인딩을 제공한다. 이미지를 보면 알 수 있듯이 컴포넌트에다 저런 바인딩을 굳이 하지 않아도 된다.

 

 

실습 예시

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideMyName(): MyName {
        Log.e("AppModule", "provideMyName 호출")
        return MyName()
    }
}
import java.util.UUID

class MyName {
    private val uuid = UUID.randomUUID()

    override fun toString(): String {
        return uuid.toString()
    }
}
import android.app.Application
import android.util.Log
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

@HiltAndroidApp
class App: Application() {
    val TAG: String = App::class.java.simpleName

    @Inject lateinit var myName: MyName

    override fun onCreate() {
        super.onCreate()
        Log.e(TAG, "$myName")
    }
}

 

@Singleton 애노테이션을 붙인다. 물론 이게 동일한 인스턴스를 제공하는지 확인은 어렵기 때문에 provideMyName이 얼마나 자주호출되는지 찍어본다.


또한, MyName의 고유함을 확인하기 위해 uuid를 하나 지정한다. 해당 uuid를 출력하는 코드로 바꾼다. 그리고 App에도 myName을 출력시킨다.


App에서 출력되는 uuid와 MainActivity에서 출력되는 uuid가 같다면 동일한 인스턴스라고 파악할 수 있다.

 

 

logcat의 로그 uuid와 MainActivity UI에서 보여지는 uuid가 같으므로 동일한 인스턴스 반환을 보장하는 것을 알 수 있겠다.

 

Default Binding

ActivityComponent의 경우, Default Binding으로 Activity와 Application이 제공 될 것이다.
Application을 주입 받아서 실제로 Application을 요청했을때 들어오는지를 확인해보자.

 

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    @Inject lateinit var myName: MyName
    @Inject lateinit var app: Application

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        Log.e("MainActivity", "app = $app")

        setContent {
            SnsAplicationTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting(myName.toString())
                }
            }
        }
    }
}

 

 

MainActivity에 있는 Log코드가 logcat에 제대로 출력되는 것을 확인할 수 있다. 즉, App.kt와 MainActivity에서 요청한 app 인스턴스가 같다는 것을 알 수 있다.