개발/안드로이드

[Hilt] AndroidEntryPoint의 이해와 Hilt 모듈, 바인딩

스몰스테핑 2024. 8. 2. 16:13

AndroidEntryPoint의 이해

Dagger에서 말하는 Component는 의존성을 관리하는 컨테이너를 말한다. SubComponent는 어떤 컴포넌트의 하위에 속하는 컴포넌트를 일컫는다. SubComponent의 하위에 있는 또다른 컴포넌트 또한 SubComponent이다. 그렇기에 엄밀히 따지면 Hilt에서 말하는 Component는 Singleton Component 하나 뿐이다. 나머지는 전부 SubComponent이다.

Dagger에서는 여러 컴포넌트를 정의하고 인스턴스화 하는게 가능하지만, Hilt에서는 표준 컴포넌트를 사용하고 그것들이 계층을 이루고 있으므로 엄밀히 따지면 Singleton Component만 컴포넌트라고 할 수 있다는 것이다.

Hilt에서 SubComponent를 인스턴스화 하는 방법은 간단하다. Android Class에다가 @AndroidEntryPoint를 선언하는 것이다.

 

@AndroidEntryPoint가 지원하는 타입

  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver (SingletonComponent 사용)

우선 @HiltAndriodApp으로 의존성 주입을 가능하게 만들었다면, 안드로이드 클래스에서도 의존성 주입을 @AndroidEntryPoint를 사용하여 활성화 시킬 수 있다. 다른 안드로이드 클래스와 달리 BroadcastReceiver는 Activity나 Fragment, View와 달리 전용 컴포넌트를 가지고 있지 않기 때문에 단순하게 SingletonComponent를 사용하여 의존성을 주입 받게하면 된다.

 

@AndroidEntryPoint 주의 사항

  • Hilt는 ComponentActivity를 상속한 Activity만 지원
  • Hilt는 androidx 라이브러리에 포함된 Fragment만 지원
  • Android SDK에 포함된 Fragment는 Deprecated 되었으며 지원하지 않는다

안드로이드 클래스에서 의존성 주입시, 상위 (서브)컴포넌트에도 반드시 @AndroidEntryPoint를 선언해야한다는 점이다.
Fragment에서 @AndroidEntryPoint를 사용했는데 만약 상위 액티비티에서 @AndroidEntryPoint를 마킹하지 않았다면 에러가 발생할 것이다.


요즘은 Compose로 화면 구성이 가능해지며 Fragment를 잘 쓰지 않는 추세이기도 하다. 신경 안쓸수도 있으나 과거 코드를 유지보수해야하는 경우도 있기에 그럴 경우에는 신경쓰는 것이 좋다.

 

Retained Fragments

Fragment 인스턴스를 유지할지 말지 결정하는 메서드가 retainInstance Method이다. 이 메소드를 호출하면서 True를 전달하면 화면 회전과 같은 환경변화 속에서도 Fragment Instants가 유지된다. 그런데 Fragment가 Hilt와 함께 사용되는 경우 컴포넌트에 바인딩된 인스턴스가 유지되지 않는다.

 

의존성과 같은 레퍼런스를 유지하려한다면 메모리 누수 발생이 생길 수 있기에 그렇게 설계가 되었으며, 만약 상위 액티비티가 Hilt를 사용하고 Fragment가 Hilt를 사용하지 않을 경우, retainInstance를 호출하여 Fragment Instants를 유지할 수 있다. 그렇다하더라도 그런 시도를 권장하진 않는다.

 

그렇다면 그 대안은 무엇일까?

Fragment가 재생성될때 어떠한 의존성을 컴포넌트에서 유지하고 싶다면 상위 컴포넌트에서 바인딩하면 된다.
또는 ViewModel을 활용하여 Fragment의 상태를 저장하고 복원하려는 노력을 하는 것도 메모리 관리 측면과 코드 유지보수 측면에서 더 좋은 방법이 될 것이다.

 

프래그먼트 바인딩을 View와 함께 운용하기

기본적으로 SingletonComponent, ActivityComponent의 바인딩은 View에 주입이 가능하다. 그런데 Fragment의 바인딩을 View에 주입하기 위해선 별도의 애노테이션을 추가해야한다.

 

이것이 바로 @WithFragmentBindings이다.

하지만 이것은 Compose를 주로 사용하는 개발자라면 별로 사용할 일이 없을 것이다. Fragment를 사용하지 않기 때문에.

 

요약

  1. Application은 컴포넌트, 나머지 타입은 서브컴포넌트
  2. Fragment Instants를 유지하려는 노력은 하지말자
  3. Fragment 범위에 바인딩 된 의존성을 View에 주입하는 경우 @WithFragmentBindings를 활용하자

 

Hilt와 모듈, 바인딩

모듈이란, 프로그램을 구성하는 구성요소, 관련된 데이터와 함수를 하나로 묶은 단위이다.
보통 하나의 소스파일에 모든 코드를 작성하지 않고 기능별로 작성하는 편이다. 이런한 모듈들을 하나로 합쳐 실행가능한 프로그램을 만들게 된다. 그렇게 기능별로 나눠 독립된 파일에 저장하여 관리하는 방식을 모듈화 프로그래밍이라고도 한다.

 

Dagger는 모듈화 프로그래밍을 개발자에게 강제하였고, Hilt 또한 Dagger 기반이기에 컴파일 타임에 HiltComponent에 모듈을 설치하는 것으로 의존성 주입을 위한 세팅이 이뤄진다.

 

모듈이 설치되는 과정

  1. 컴파일 타임, 애노테이션 처리
  2. @Module 탐색
  3. @InstallIn에서 설치될 컴포넌트 탐색
  4. 해당 컴포넌트에 설치

 

@InstallIn 사용하기

@Module
@InstallIn(SingletonComponent::class)
object FooModule {
    @Provides
    fun provideBar(): Bar {...}
}

 

다음과 같이 모듈이 마킹된 클래스에 @InstallIn을 추가할 수 있다. 이 애노테이션의 속성으로 컴포넌트 클래스를 명시하면 해당 컴포넌트의 모듈이 갖고 있는 의존성들이 바인딩된다.


주의할 점은 모듈 클래스에 모듈만 마킹되고 @InstallIn이 없다면, 컴파일 타임에 에러가 발생한다. 즉, 모듈과 @InstallIn은 같이 와야한다.

 

@Module
@InstallIn(SingletonComponent::class)
internal object FooModule {
    @Singleton
    @Provides
    fun provideBar(): Bar {...}
}


다음 코드를 보면 의존성에 @Singleton이 붙었다.
이렇게 되면 인스턴스를 컴포넌트 내부에 저장해두기 때문에 함수 호출 시 한번만 이뤄지게 된다. 해당 인스턴스는 모듈(SingletonComponent)과 생명주기를 공유하기 때문에 애플리케이션 종료될때까지 해당 인스턴스는 메모리에 남아있게 될 것이다.

 

그렇기에 @Singleton는 일종의 메모리 누수 효과를 불러일으킬 수 있기에 잘 써야한다.

 

@Module
@InstallIn(SingletonComponent::class)
object FooModule {
    @Provides
    fun provideBar(app: Application): Bar {...}
}


추가적으로 모듈 클래스에서 provide하는 함수는 모듈 클래스 자기 자신이 설치된 컴포넌트 바인딩에 접근하는 것이 가능하다. 즉, SingletonComponent에서는 기본 바인딩으로 Application을 제공하고 있다. 그렇기에 provide하는 함수에 파라미터로 Application을 선언하면 이 Application의 인스턴스를 주입받고 참조하는 것이 가능해진다.

 

여러 컴포넌트에 모듈 설치

@InstallIn(ViewComponent::class, ViewWithFragmentComponent::class)

 

만약에 중복된 코드 작성을 피하고자 모듈을 여러 컴포넌트에 설치하고 싶을 수도 있다.

@InstallIn의 속성으로 컴포넌트 클래스들을 모두 포함시키면 된다.

 

하지만, 이 경우 반드시 지켜야할 규칙 세 가지가 존재한다.

  1. 컴포넌트에 접근하여 제공되는 바인딩들은 모두 동일한 스코프에 있어야 한다.
  2. 선언된 컴포넌트들의 바인딩에 접근가능하며, 프로바이드 함수에 주입할 수 있어야 한다.
  3. 하위 및 상위 컴포넌트 계층 간에는 동일한 모듈을 설치할 수 없다.

 

Nullable 바인딩은 지양

  • Hilt는 자바코드에서 null 바인딩을 금하고 있다.
  • 자바코드에서 의존성 바인딩 및 요청 시 @Nullable 애노테이션을 사용해서 우회할 수 있다.
  • 코틀린에서는 문법자체적으로 null assign을 금하고 있으나, Nullable 타입이 존재한다.
  • 실제로 Hilt는 자바기반이므로 코틀린으로 작성한 nullable 바인딩이 가능하다.
  • 그래도 null-safety한 코드를 위해 nullable한 의존성은 지양하자.

 

요약

  1. Hilt 모듈은 연관된 의존성들을 한데 묶어서 제공하는 클래스
  2. 컴파일 타임에 @InstallIn을 체크하여 적절한 컴포넌트에 설치된다.
  3. 제약적이지만 여러 컴포넌트에 동일한 모듈을 설치하는 것이 가능하다.
  4. Null은 바인딩하지도, 요청하지도 말자.