본문 바로가기

Android/Tech

[Dagger-Hilt] Dagger-Hilt 의 대표적인 Annotations

Unsplash, Lucas kapla.

Dependency Injection

의존성 주입(Dependency Injection, DI)은 안드로이드 앱 아키텍처 및 클린 아키텍처 적용시 필수적으로 사용되어야 하는 기술입니다. 대부분의 아키텍처가 테스트 용이성에 포커스를 두기 때문입니다. 다만 구현하기가 쉽지 않습니다. 수많은 패턴에 대해 학습하고 이를 적재적소에 사용하여야 하며, 이러한 과정은 수많은 보일러 플레이트 코드를 양산할 수 밖에 없습니다.

 

메이저 기술의 구현 과정이 쉽지 않다면, 보통 라이브러리가 존재합니다. 안드로이드 진영에는 꽤 많은 DI 라이브러리가 존재하는데, 그 중 Dagger-Hilt 는 Jetpack Library 로 채택되어 있으며, 수많은 안드로이드 개발자들에게 꾸준한 사랑을 받고 있습니다. 


Dagger-Hilt 작동 원리

Dagger-Hilt 는 그의 전신인 Dagger 2를 기반으로 한 안드로이드용 DI 라이브러리입니다. 

Hilt 는 어노테이션을 기반으로 의존성 주입 코드를 작성할 수 있는데, 이 어노테이션들은 DI 라이브러리에 의존성 주입에 관한 정보를 전달하는 데에 사용됩니다. Dagger-Hilt 는 컴파일 타임에 어노테이션을 읽어내어, 요구되는 의존성에 적절한 객체를 전달합니다.

 


Dagger-Hilt Annotations

Dagger-Hilt 라이브러리 사용에 필요한 주요 어노테이션들에 대한 원리와 정보를 알아봅니다.

@AndroidEntryPoint

안드로이드 컴포넌트 (4대 컴포넌트 + Fragment) 에 사용할 수 있는 어노테이션입니다. 해당 어노테이션을 사용하면 Dagger-Hilt 가 컴포넌트를 인식하고 의존성 주입을 활성화하는 역할을 합니다. 제너레이트된 코드들을 보면, 'Hilt_' 로 시작하는 클래스들이 있는데, 이들이 해당 어노테이션으로 인해 생성된 코드입니다. 

 

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

 

액티비티의 Context 에 대한 null check 를 진행 후, inject() 해줍니다. 

@Module, @InstallIn

@Module 어노테이션은 클래스나 오브젝트에 선언 가능하며, 해당 클래스 또는 오브젝트는 실질적으로 주입할 객체를 반환하는 메서드를 가집니다. 해당 메서드는 이 다음에 설명할 @Provides 또는 @Binds 어노테이션이 붙어야만 실질적으로 객체를 제공할 수 있습니다.

 

@InstallIn 어노테이션도 붙게 되는데, 해당 어노테이션은 의존성 객체가 생성되고 영향을 주는 범위를 설정할 수 있습니다.

 

Dagger-Hilt 에서의 모듈은 의존성 객체를 생성하고 제공하는 방법을 정의하는 역할을 수행합니다.

 

@Module
@InstallIn(ViewModelComponent::class)
object UseCaseModule {

    @Provides
    @ViewModelScoped
    fun provideGetUserIdUseCase(splashRepository: SplashRepository): GetUserIdUseCase {
        return GetUserIdUseCase(splashRepository)
    }
}

@Provides

@Module 어노테이션이 붙은 클래스나 오브젝트 내의 일반 메서드에 선언되며, 주입할 객체를 반환하는 역할을 수행합니다. 위 코드 스니펫에서는 UseCase 를 반환하는 함수에 함께 작성되었습니다.

@Binds

추상 클래스 내의 추상 메서드에 작성되며, 특정 인터페이스 또는 추상 클래스의 구현체를 주입할 때 사용합니다. 구현체가 필요한 클래스의 생성자에 해당 구현체에 대한 주입을 요청하면 됩니다. @Module 어노테이션이 붙은 추상 클래스 내에 선언하는 bind~ 메서드의 반환 타입은 인터페이스 타입이어야 합니다.

🤔 두 어노테이션을 붙이는 기준은 무엇인가요?

먼저 @Provides 어노테이션을 붙이는 기준은 다음과 같습니다.

  • 주입하고자 하는 인스턴스가 클래스의 인스턴스인 경우
  • 주입하고자 하는 인스턴스의 클래스가 인터페이스나 추상 클래스의 구현체일지라도 생성과 동시에 초기화 작업이 필요한 경우

다음으로, @Binds 어노테이션을 붙이는 기준입니다.

  • 주입하고자 하는 인스턴스가 인터페이스나 추상 클래스의 구현체일 때

🤔 인터페이스나 추상 클래스의 구현체도 객체이므로 Hilt 를 통해 주입하는 것은 모두 객체일텐데, 그냥 싹 다 @Provides 로 주입하면 안 되나요?

그렇게 해도 문제는 없습니다. 그러나, @Provides 의 경우, 별도로 [클래스명]Module_Provide[클래스명]Factory 라는 보일러 플레이트 코드가 생성되지만, @Binds 의 경우에는 그렇지 않고 해당 인터페이스 / 추상 클래스 타입의 구현체를 반환하는 것이 전부이므로, @Binds 를 사용하는 방식에 성능 상 이점이 있습니다. 즉, @Binds 를 사용할 수 있는 곳엔 @Binds 를 사용하는 것이 좋다입니다. 구현체를 DI 하는 부분에서는 @Binds 를, 그렇지 않은 곳에서는 @Provides 를 사용하면 됩니다.

@HiltViewModel

androidx.lifecycle.ViewModel 을 상속하는 클래스에 선언할 수 있으며, 해당 어노테이션이 붙은 ViewModel 은 HiltViewModelFactory 에서 관리됩니다. ViewModel 이 @AndroidEntryPoint 어노테이션이 붙은 컴포넌트에 주입될 수 있게 하고, 해당 ViewModel 에 @Inject 어노테이션을 통해 다른 의존성을 주입할 수 있도록 설정합니다.

 

해당 어노테이션이 붙은 ViewModel 은 HiltViewModelFactory 에서 관리되므로, Activity 에 주입하려면 다음과 같이 코드를 작성합니다.

 

override val viewModel: SplashViewModel by viewModels()

 

Jetpack Compose 를 사용하는 경우, 별도로 Activity 필드에 선언하지 아니하고, Screel-Level Composable 의 파라미터에 곧바로 선언할 수도 있습니다.

 

@Composable
private fun CommunityPostingActivityContent(
    viewModel: CommunityPostingViewModel = hiltViewModel()
) {

@Reusable

의존성 객체를 재사용할 수 있도록 지정하는 역할을 합니다. Dagger-Hilt 는 해당 어노테이션이 붙은 객체를 캐싱하고, 필요한 경우 동일한 인스턴스를 그대로 반환하여 성능을 향상시킵니다.

 

보통 Dagger-Hilt 는 싱글톤으로 대부분의 객체를 생성합니다. 그러나 해당 어노테이션이 선언된 객체는 싱글톤으로 생성하지 않고 재사용합니다. 

🤔 그럼 싱글톤이랑 뭐가 다른가요?

해당 어노테이션이 붙은 객체는, 해당 객체를 주입해야 하는 컴포넌트의 수명주기가 모두 끝을 맞이하면 그 때 함께 메모리에서 정리됩니다. 프로세스가 끝날 때까지 존재하는 싱글톤과는 다릅니다.

@Retention

* 해당 어노테이션은 Hilt 에서 제공되는 어노테이션이 아니지만, Hilt 사용 시 자주 사용됩니다.

본래 어노테이션은 컴파일 타임에 코드를 생성하거나 검증하기 위해 사용되므로, 컴파일 타임이 지나면 제거되는 것이 보통입니다. 즉, 런타임에 사용할 수 없는 것이 일반적인데, 해당 어노테이션을 통해 객체에 붙은 어노테이션에 대한 런타임 접근 가능성을 확보할 수 있습니다. 

 

SOURCE, BINARY, RUNTIME 이 있습니다.

SOURCE - 컴파일이 끝나면 해당 클래스의 어노테이션을 삭제합니다.

BINARY - 클래스에는 어노테이션 정보를 남기지만 런타임에는 접근할 수 없습니다.

RUNTIME - 클래스에 어노테이션 정보를 남기고, 리플렉션을 통해 런타임에 접근할 수 있습니다.

@Qualifier

* 해당 어노테이션은 Hilt 에서 제공되는 어노테이션이 아니지만, Hilt 사용 시 자주 사용됩니다.

Dagger-Hilt 는 @Binds 나 @Provides 어노테이션을 붙여 객체를 주입할 때, 그 객체를 타입으로 구분 짓습니다. 동일한 타입이지만 경우에 따라 다른 객체를 반환해주고 싶을 때, 이를 정의하기 위해 사용됩니다. 

 

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DefaultDispatcher

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class IoDispatcher

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MainDispatcher

 


적고 보니 생각보다 많네요. 이 기회에 간략하게나마 훑어 볼 수 있어 좋았습니다.