이동식 저장소

[Hilt] Entry Points 본문

Primary/Android

[Hilt] Entry Points

해스끼 2022. 9. 10. 13:42

Dagger가 직접 지원하지 않는 클래스에서 객체를 주입받고 싶다면 entry point를 사용해 보자. Entry point는 Dagger가 관리하는 객체 간의 그래프를 참조하기 위한 진입점 역할을 한다. 

AndroidEntryPoint

사실 우리는 이미 ``@AndroidEntryPoint``라는 어노테이션을 알고 있다. ``@AndroidEntryPoint``는 Hilt가 미리 정의해 둔 entry point로, Activity나 Fragment 등 주요 Android 클래스에서 Hilt 컴포넌트와 해당 컴포넌트에 설치된 Hilt 모듈에 접근할 수 있게 한다. 

 

그러나 ``AndroidEntryPoint``를 사용해도 Hilt가 지원하지 않는 클래스에서 객체를 주입받을 수는 없다. 이런 경우에는 어쩔 수 없이 entry point를 직접 정의해야 한다.

Entry point 정의하기

Entry point를 직접 정의하는 과정은 다음과 같다.

  1. 인터페이스를 하나 정의한다.
  2. 인터페이스에 ``@EntryPoint`` 어노테이션을 붙인다.
  3. 인터페이스 내부에 필요한 타입을 제공하는 함수를 정의한다.
  4. 인터페이스를 적절한 컴포넌트에 설치한다.
// From MyVoca

@EntryPoint
@InstallIn(SingletonComponent::class)
interface VocaPersistenceRoomEntryPoint {
    fun vocaDao(): VocaDao
}

이때 인터페이스가 설치된 컴포넌트에서 해당 타입을 제공할 수 있어야 한다. 나는 ``SingletonComponent``에 설치된 다른 모듈에서 ``VocaDao`` 타입을 제공하는 함수를 작성해 두었다. 따라서 ``VocaPersistenceRoomEntryPoint``는 해당 바인딩을 참조하여 객체를 제공할 것이다.

 

``fun`` 앞에 어노테이션을 붙여 객체를 제공할 바인딩을 직접 지정할 수도 있다.

 

Entry point의 모든 함수는 public이어야 한다. 외부의 Dagger 컴포넌트가 entry point의 함수를 구현하기 때문이다.

Entry point 사용하기

Entry point에 접근하려면 ``EntryPoints.get()`` 함수를 사용하자. 

val entryPoint = EntryPoints.get(applicationContext, VocaPersistenceRoomEntryPoint::class.java)
val dao = entryPoints.vocaDao()

다만 ``Application`` 등 Android 객체에서 컴포넌트를 가져오는 경우 ``EntryPointAccessors``의 함수를 사용하는 편이 적절하고, 타입 안전성도 얻을 수 있다.

ViewModel은 없다

val entryPoint = EntryPointAccessors.fromApplication(applicationContext, VocaPersistenceRoomEntryPoint::class.java)
val dao = entryPoint.vocaDao()

실전 사용 예시

MyVoca에 직접 작성한 코드이다.

// 이 객체 자체도 Hilt로 주입할 수 있게 @Singleton과 생성자 주입을 적용했다.
@Singleton
class VocaPersistenceRoom @Inject constructor(@ApplicationContext context: Context) :
    VocaPersistence, CoroutineScope {
	
    // EntryPoint 정의
    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface VocaPersistenceRoomEntryPoint {
        fun vocaDao(): VocaDao
    }

    private lateinit var vocaDao: VocaDao

    // 생성자에서 entry point에 접근한다.
    init {
        synchronized(this) {
            val entryPoint = getVocaPersistenceRoomEntryPoint(context)
            assignDao(entryPoint)
        }
    }

    private fun getVocaPersistenceRoomEntryPoint(context: Context) =
        EntryPointAccessors.fromApplication(context, VocaPersistenceRoomEntryPoint::class.java)

    private fun assignDao(entryPoint: VocaPersistenceRoomEntryPoint) {
        vocaDao = entryPoint.vocaDao()
    }
}

왠지 ``synchronized``를 사용해야 할 것만 같았다. 클래스 자체가 singleton이기도 하고, entry point에서 생성하는 객체가 매우 비싸기 때문에 객체 할당 부분을 단 한 번만 실행하고 싶었다. 유효한 논리인지는 잘 모르겠다만..

Entry point를 어디에 정의해야 하는가?

일반적으로 entry point는 객체를 주입받고 싶어하는 클래스에 정의해야 한다.

 

예를 들어 위의 코드에서 entry point가 ``VocaDao.kt`` 파일에 정의됐다고 가정하자. ``VocaDao`` 파일을 읽는 사람은 곧 혼란에 빠지게 될 것이다.

 

``VocaDao``를 어떻게 얻어야 하는가? 이 인터페이스를 구현한 객체를 얻을 수 있나? 아니면 여기 정의된 entry point에 접근하여 얻어야 하나?

 

둘 다 틀렸지만, 어쨌든 이런 쓸데없는 혼란을 부추길 수 있으니 entry point는 가급적 entry point를 사용할 클래스에 정의하자. 다른 의존성을 쉽게 추가할 수 있기도 하고.

'Primary > Android' 카테고리의 다른 글

Gson에서 null이 반환될 때 with ProGuard  (0) 2022.09.27
[Android] 디버깅할 때 앱이 느리다면  (0) 2022.09.25
[Hilt] Modules  (0) 2022.08.29
[Hilt] ViewModels  (0) 2022.08.29
[Hilt] Android Entry Points  (0) 2022.08.11
Comments