Hilt

BongKu·2023년 6월 21일
0

Android

목록 보기
7/30
post-thumbnail

Hilt ?

Hilt는 안드로이드 애플리케이션에서 의존성 주입(Dependency Injection)을 쉽게 구현할 수 있도록 도와주는 Jetpack 라이브러리입니다. Hilt는 Dagger 2를 기반으로 구축되었으며, Dagger를 더 쉽게 사용할 수 있도록 제공되는 기능을 포함하고 있습니다.

의존성 주입

의존 이란 객체 지향 프로그래밍에서 클래스나 객체들 간의 상호작용과 의존성을 나타내는 관계입니다. 클래스 A가 클래스 B를 사용하거나 참조하는 경우, A는 B에 의존성을 가지고 있다고 말할 수 있습니다.

Hilt 라이브러리를 사용하지 않는 간단한 예를 들어보겠습니다.

class MainViewModel : ViewModel() {
    private val coinRemoteRepository = CoinRemoteRepository()
    fun getCoinList(){
    	coinRemoteRepository.getCoinList()
            //...
    }
class CoinRemoteRepository {
    fun getCoinList(): List<CoinList>{
    	//api 호출을 통해 coinList를 가져옵니다.
    }
}

위 예제에서 MainViewModel은 CoinRemoteRepository에 대해 의존성을 가지고 있다고 할 수 있습니다.

Why Hilt ?

객체 간의 의존 관계를 외부에서 주입하여 코드의 유연성, 재사용성, 테스트 용이성을 개선하기 위해서 사용을 합니다.

위 예제에서 CoinRemoteRepository를 사용하다가 CoinDBRepository를 바꾸는 예시로 알아보겠습니다.

class CoinDBRepository{
	fun getDBCoinList(): List<CoinList>{
    	//DB에서 CoinList 가져오는 역할 수행
    }
}
class MainViewModel : ViewModel() {
    private val coinDBRepository = CoinDBRepository()

    fun getCoinList() {
    	val result = coinDBRepository.getDBCoinList()
            //...
    }
}

위 코드는 간단한 예제이므로 간단히 메서드 이름만 변경된 것이 보이지만, 실제로 큰 프로젝트에서 위와 같이 ViewModel에서 기존에 CoinRemoteRepository를 사용하다가 CoinDBRepository로 변경을 해야 한다면 코드 수정이 많이 일어나게 되고 이에 따라 예기치 못한 오류가 발생할 가능성이 높습니다.

이런 경우 흔히 Interface를 정의하여 사용을 하는데 Interface를 정의한다면 다음과 같이 사용할 수 있습니다.

    interface CoinRepository{
        fun getCoinList():List<CoinList>
    }
	class CoinRemoteRepository : CoinRepository {
    	override fun getCoinList(): List<CoinList>{
    	//api 호출을 통해 coinList를 가져옵니다.
    }
}
	class CoinDBRepository : CoinRepository {
    	override fun getCoinList(): List<CoinList>{
    	//DB에서 coinList를 가져옵니다.
    }
}

다음과 같이 interface 정의를 통해, viewModel에서의 코드 수정 없이 사용할 수 있습니다.

하지만, Main 함수에서 해당 viewModel에 구현체를 전달할 때는 CoinDBRepository인지, CoinRemoteRepositry 인지 상황에 맞게 전달해 주어야 합니다. 이런 과정 또한 메인 함수를 수정하는 과정에서 오류를 발생시킬 수 있습니다.

따라서, 의존성 주입을 편하게 할 수 있도록 해주는 Hilt 라이브러리를 사용합니다.

Hilt는 주석 기반의 구성을 사용하여 의존성을 식별하고, 애플리케이션 컴포넌트에 필요한 모듈을 설치합니다. 이를 통해 Hilt는 컴파일 타임에 의존성 그래프를 생성하고, 런타임 시에 필요한 의존성을 제공합니다.

Hilt는 Android Jetpack의 다른 구성 요소들과도 통합이 잘 되어 있습니다. 예를 들어, Hilt는 ViewModel 주입을 지원하여 ViewModel과 관련된 의존성을 간편하게 주입할 수 있도록 합니다.

Hilt 사용

Hilt 관련 안드로이드 개발자 문서와 , Hilt 사용을 위해 간단하게 만들어본 날씨앱 예제를 함께 보며 알아보겠습니다.

루트 build.gradle에 hilt-android-gradle-plugin 추가

plugins {
  ...
  id("com.google.dagger.hilt.android") version "2.44" apply false
}

app/build.gradle에 종속 항목 추가

plugins {
  kotlin("kapt")
  id("com.google.dagger.hilt.android")
}

android {
  ...
}

dependencies {
  implementation("com.google.dagger:hilt-android:2.44")
  kapt("com.google.dagger:hilt-android-compiler:2.44")
}

// Allow references to generated code
kapt {
  correctErrorTypes = true
}

Hilt 애플리케이션 클래스

<개발자 문서>

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

@HiltAndroidApp
class ExampleApplication : Application() { ... }

<예제>

@HiltAndroidApp
class  TodaysWeather: Application()

이 작업은 Hilt를 초기화 하는 역할을 수행합니다. 이 작업 애플리케이션 컴포넌트를 생성하고 관리하는 Hilt의 핵심 컴포넌트인 HiltComponent를 생성하기 위해 필요합니다.

Android 클래스에 종속 항목 삽입

<개발자 문서>

Application 클래스에서 Hilt를 초기화 했다면, Hilt는 @AndroidEntryPoint 주석이 있는 다른 Android 클래스에 종속 항목을 제공할 수 있습니다.
Hilt는 다음 클래스들에 대해서 지원합니다.

  • Application(@HiltAndroidApp을 사용하여)
  • ViewModel(@HiltViewModel을 사용하여)
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

구성요소에서 종속 항목을 가져오려면 @Inject 주석을 사용하여 필드 삽입을 합니다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

<예제>

@AndroidEntryPoint
class HomeFragment(private val resolutionMetrics: ResolutionMetrics) :
    BindingFragment<FragmentHomeBinding>(R.layout.fragment_home),LifecycleObserver{
    	private val viewModel by viewModels<HomeViewModel>()
       	...
    }

Hilt 결합 정의

<개발자 문서>

Hilt 결합 정보를 제공하기 위해서 클래스의 생성자에서 @Inject 주석을 사용해서 클래스의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다.

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

@Inject 어노테이션을 생성자에 적용함으로써, Hilt는 AnalyticsAdapter 클래스가 인스턴스화될 때 AnalyticsService를 주입하여 완전한 객체를 생성합니다. 이를 통해 AnalyticsAdapter 클래스는 AnalyticsService에 접근하여 해당 서비스의 기능을 활용할 수 있습니다.

<예제>

class HomeRepositoryImpl @Inject constructor(
    private val remoteGetWeatherInfoDataSource: RemoteGetWeatherInfoDataSource
) : HomeRepository {...}

해당 예제에서는 HomeRepositoryImpl 클래스에 RemoteGetWeatherInfoDataSource를 주입합니다.

Hilt 모듈

<개발자 문서>

인터페이스외부 라이브러리 클래스 의 경우 생성자로 삽입할 수 없습니다. 따라서 Hilt 모듈을 사용하여 Hilt 결합 정보를 제공할 수 있습니다.

Hilt 모듈은 @Module 어노테이션으로 지정된 클래스입니다. 그리고 @InstallIn 어노테이션으로 모듈에서 사용하거나 설치할 Android 클래스를 Hilt에 알려야 합니다.

  • 인터페이스 인스턴스 삽입
    Hilt 모듈 내에 @Bind로 주석이 지정된 추상 함수를 생성하여 결합 정보를 제공합니다.
interface AnalyticsService {
  fun analyticsMethods()
}

// Constructor-injected, because Hilt needs to know how to
// provide instances of AnalyticsServiceImpl, too.
class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

@InstallIn(ActivityComponent.class) 주석을 지정합니다. 이 주석은 AnalyticsModule의 모든 종속 항목을 앱의 모든 활동에서 사용할 수 있음을 의미합니다. 메서드의 이름은 아무렇게나 해도 상관이 없습니다.

<예제>

interface RemoteGetWeatherInfoDataSource {
    suspend fun getWeatherInfo(
        dataType: String,
        numOfRows: Int,
        pageNo: Int,
        baseDate: Int,
        baseTime: Int,
        nx: String,
        ny: String
    ):Response<WeatherInfoResponse>
}

class RemoteGetWeatherInfoDataSourceImpl @Inject constructor(
    private val weatherInfoService: GetWeatherInfoService
):RemoteGetWeatherInfoDataSource{
    override suspend fun getWeatherInfo(
        dataType: String,
        numOfRows: Int,
        pageNo: Int,
        baseDate: Int,
        baseTime: Int,
        nx: String,
        ny: String
    ): Response<WeatherInfoResponse>{
        return weatherInfoService.getWeatherInfo(dataType,numOfRows,pageNo, baseDate, baseTime, nx, ny)
    }
}

@Module
@InstallIn(SingletonComponent::class)
interface RemoteDataSourceModule {
    @Binds
    @Singleton
    fun bindsRemoteGetWeatherDataSource(source: RemoteGetWeatherInfoDataSourceImpl): RemoteGetWeatherInfoDataSource
}

위 예제는 @InstallIn(SingletonComponent::class)을 통해 싱글톤 컴포넌트에 설치되도록 했습니다. RemoteDataSourceModule을 싱글톤 컴포넌트에 설치함으로써, 해당 모듈에서 바인딩된 RemoteGetWeatherInfoDataSource 의존성은 애플리케이션 전체에서 하나의 인스턴스만 생성되고 공유됩니다. 이를 통해 여러 곳에서 동일한 인스턴스를 사용하여 일관성 있는 데이터를 유지할 수 있습니다.

  • Provices를 사용하여 인스턴스 삽입

<개발자 문서>

외부 라이브러리의 클래스 (Retrofit, RoomDB) 또는 빌더 패턴으로 인스턴스를 생성해야 하는 경우 생성사 삽입이 불가능합니다.
이 경우 @Provides 어노테이션을 지정하여 이 유형의 인스턴스를 제공하는 방법을 Hilt에 알릴 수 있습니다.

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    // Potential dependencies of this type
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

동일한 유형에 대해 여러 결합을 제공할 경우

AnalyticsService 호출을 가로채야 한다면 인터셉터와 함께 OkHttpClient 객체를 사용할 수 있습니다. 다른 서비스에서는 호출을 다른 방식으로 가로채야 할 수도 있습니다. 이 경우에는 서로 다른 두 가지 OkHttpClient 구현을 제공하는 방법을 Hilt에 알려야 합니다.

이때는 @Binds 또는 @Provides 메서드에 어노테이션을 지정할 한정자를 다음과 같이 정의해야합니다.

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

이후 다음과 같이 두 메서드 모두 동일한 반환 유형을 갖지만 한정자는 두 가지의 서로 다른 결합으로 메서드에 라벨을 지정해줍니다.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

  @AuthInterceptorOkHttpClient
  @Provides
  fun provideAuthInterceptorOkHttpClient(
    authInterceptor: AuthInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(authInterceptor)
               .build()
  }

  @OtherInterceptorOkHttpClient
  @Provides
  fun provideOtherInterceptorOkHttpClient(
    otherInterceptor: OtherInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(otherInterceptor)
               .build()
  }
}

필드 또는 매개변수에 해당 한정자로 어노테이션을 지정하여 필요한 특정 유형을 삽입할 수 도 있습니다.

// As a dependency of another class.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .client(okHttpClient)
               .build()
               .create(AnalyticsService::class.java)
  }
}

// As a dependency of a constructor-injected class.
class ExampleServiceImpl @Inject constructor(
  @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...

// At field injection.
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {

  @AuthInterceptorOkHttpClient
  @Inject lateinit var okHttpClient: OkHttpClient
}

<예제>

날씨앱 예제에서는 한정자를 사용하지 않아, 이전에 했던 다른 프로젝트에서 예시를 가져오겠습니다.

아래의 예제는 프로젝트 자체 서버와, 네이버클로바 두 개의 다른 서비스에서 호출을 다른 방식으로 받아와야 하기 때문에 한정자를 사용하여 정의했습니다.

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DameDameServer

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class NaverClovaSentimentServer

위와 같이 한정자를 정의합니다.

@Module
@InstallIn(SingletonComponent::class)
object RetrofitModule {
	//...
    
        @Provides
    @Singleton
    @DameDameServer
    fun providesDameDameOkHttpClient(
        @DameDameServer interceptor: Interceptor,
        httpLoggingInterceptor: HttpLoggingInterceptor
    ): OkHttpClient =
        OkHttpClient.Builder()
            .connectTimeout(15, TimeUnit.SECONDS)
            .writeTimeout(20, TimeUnit.SECONDS)
            .readTimeout(15, TimeUnit.SECONDS)
            .addInterceptor(interceptor)
            .addInterceptor(httpLoggingInterceptor)
            .build()
    //...
    @Provides
    @Singleton
    @NaverClovaSentimentServer
    fun providesNaverOkHttpClient(
        @NaverClovaSentimentServer interceptor: Interceptor,
        httpLoggingInterceptor: HttpLoggingInterceptor
    ): OkHttpClient =
        OkHttpClient.Builder()
            .connectTimeout(15, TimeUnit.SECONDS)
            .writeTimeout(20, TimeUnit.SECONDS)
            .readTimeout(15, TimeUnit.SECONDS)
            .addInterceptor(interceptor)
            .addInterceptor(httpLoggingInterceptor)
            .build()
}

다음은 필드 또는 매개변수에 한정자 어노테이션 지정한 것입니다.

@Module
@InstallIn(SingletonComponent::class)
object RetrofitServiceModule {
	//...
        @Provides
    @Singleton
    fun providesCheckDuplicateProfileIdService(@DameDameServer retrofit: Retrofit): CheckDuplicateProfileNicknameService =
        retrofit.create(CheckDuplicateProfileNicknameService::class.java)

    @Provides
    @Singleton
    fun providesSetProfileService(@DameDameServer retrofit: Retrofit): SignUpService =
        retrofit.create(SignUpService::class.java)
   	//...
}

구조로 보면 다음과 같습니다.

더 자세한 내용은 안드로이드 개발자 문서를 참고해주세요
https://developer.android.com/training/dependency-injection/hilt-android?hl=ko#component-hierarchy

profile
화이팅

0개의 댓글