Android/다양한 기능들(Library)

[Android] Dagger Hilt, 제대로 알고 쓰자

chattymin 2024. 6. 17. 18:53
728x90
반응형

직접 만들었는데 꽤나 귀엽게 잘만든거 같다 ㅋㅋ 만족~

Hilt가 뭘까??

Hilt는 안드로이드 프로젝트에서 수동 의존성 주입 시 발생하는 보일러 플레이트를 줄이는 의존성 주입 라이브러리이다.

Hilt는 Dagger가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성 및 Android Studio의 이점을 누리기 위해 인기 있는 DI 라이브러리인 Dagger를 기반으로 빌드되었다.

 

 

왜 쓰는 걸까??

의존성 주입을 위해서는 많은 보일러플레이트가 존재한다. 이러한 보일러플레이트들을 줄이기 위해서 나온 것이 Hilt이다.

 

여러분들이 가장 많이 겪었을 상황을 아래 예시와 함께 보자.

예전에 썼던 부끄러운 코드... ㅠ.ㅠ

이렇게 ViewModel에 Repository를 Inject해주고 있다.

이러한 코드를 사용하기 위해서는 ViewModel은 내부 파라미터를 지원하지 않기 때문에 반드시 DI를 해줘야한다. 

 

그래서 일반적으로 사용하는 방법은 아래와 같이 ViewModelFactory를 만들어 준다.

 

즉, ViewModel을 만들때 마다 ViewModelFactory를 연결시켜 줘야 하는 것이다.

이 뿐만 아니라 Service를 Repository에 연결하거나, DataStore등을 사용할 때 마다 연결해주는 코드를 작성해줘야 한다.

 

매번 코드들을 작성해줘야 한다는 불편함을 해결하고자 Hilt를 사용한다.

 

 

그럼 어떻게 써?

이 글은 ksp를 기준으로 작성되었습니다.
ksp는 Kotlin 버전과 밀접하게 연결되어있으니 상황에 맞는 버전은 여기를 참고하시길 바랍니다.

 

build.gradle(Project)

buildscript {
    ext {
        android_version = "8.3.1"
        kotlin_version = "1.9.0"
        hilt_version = "2.51"
        ksp_version = "1.9.0-1.0.13"
    }

    dependencies {
        classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$ksp_version"
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

plugins {
    id 'com.android.application' version "$android_version" apply false
    id 'com.android.library' version "$android_version" apply false
    id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
    id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" apply false
    id 'com.google.devtools.ksp' version "$ksp_version" apply false
}

주의 할 점은 android version, kotlin version, ksp version 등을 고려해서 버전을 서로 맞춰줘야 한다.

 

 

build.gradle(Module)

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'com.google.devtools.ksp' // ksp 설정
    id 'dagger.hilt.android.plugin' // hilt 설정
}

android {
	...
}

dependencies {
    ...
    // Hilt
    implementation "com.google.dagger:hilt-android:$hilt_version"
    ksp "com.google.dagger:hilt-android-compiler:$hilt_version"
}

내가 hilt를 사용하고자 하는 Module에 plugins와 dependencies를 설정해준다.

 

이렇게 하면 gradle에서 해야하는 간단한 세팅 끝이다.

 

 

Application Class 생성

@HiltAndroidApp
class App : Application()

 

Hilt를 사용하는 모든 앱은 @HiltAndroidApp으로 주석이 지정된 Applicaton 클래스를 포함해야 한다.

@HiltAndroidApp은 애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 비롯하여 Hilt의 코드 생성을 트리거한다.

 

AndroidManifest에서 아래와 같이 android:name을 설정해준다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
    	...
        android:name=".App">
        <activity
            ...
        </activity>
    </application>
</manifest>

 

 

Class에 종속성 주입

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navigator: MainNavigator = rememberMainNavigator()
            val deviceWidth = applicationContext?.resources?.displayMetrics?.widthPixels ?: 0

            NOWSOPTAndroidTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    CompositionLocalProvider(
                        LocalDeviceSizeComposition provides DeviceSize.of(deviceWidth)
                    ) {
                        MainScreen(navigator)
                    }
                }
            }
        }
    }
}

Activity에는 반드시 @AndroidEntryPoint를 해준다.

 

@HiltViewModel
class HomeViewModel @Inject constructor(
    reqresRepository: ReqresRepository
) : ViewModel() {
    val userListStream: Flow<PagingData<ReqresUserModel>> =
        reqresRepository.getUserList().distinctUntilChanged().cachedIn(viewModelScope)
}

ViewModel의 경우 @HiltViewModel을 적어줘야 한다.

또한 생성자에 @Inject constructore()를 이용하여 DI해주고자 하는 객체를 써주면 된다.

Repository를 주입해주기 위해서는 Module을 설정해줘야한다. 이는 밑에서 추가적으로 설명하겠다.

 

 

 

의존성 주입

@Inject 어노테이션을 활용한다.

 

Field Injection

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

의존성 주입하고 싶은 객체의 선언시 앞에 @Inject를 선언하여 사용한다.

private로 설정 불가능하고, Hilt로 주입받는 모든 변수들은 super.onCreate()가 호출된 후에 사용 가능하다.

 

 

Constructor Injection

위 ViewModel에서 사용한 것 처럼 @Inject contructor()와 같은 방법으로 사용한다.

class TestB @Inject constructor(
    testA: TestA
){
    ...
}

 

 

 

 

Module

아래는 Repository를 주입해주기 위한 RepositroyModule이다.

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    @Singleton
    abstract fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository

    @Binds
    @Singleton
    abstract fun bindHomeRepository(homeRepositoryImpl: HomeRepositoryImpl): HomeRepository

    @Binds
    @Singleton
    abstract fun bindReqresRepository(reqresRepositoryImpl: ReqresRepositoryImpl): ReqresRepository
}

 

아래는 Retrofit을 사용하여 Sevice를 주입해주기 위한 ServiceModuledle이다.

@Module
@InstallIn(SingletonComponent::class)
object ServiceModule {
    @Provides
    @Singleton
    fun provideAuthService(@AUTH retrofit: Retrofit): AuthService =
        retrofit.create(AuthService::class.java)

    @Provides
    @Singleton
    fun provideMainService(@HEADER retrofit: Retrofit): HomeService =
        retrofit.create(HomeService::class.java)

    @Provides
    @Singleton
    fun provideReqresService(@REQRES retrofit: Retrofit): ReqresService =
        retrofit.create(ReqresService::class.java)
}

 

위 두 모듈을 본다면 RepositroyModule은 @Binds를 사용하고 있고, ServiceModuledle은 @Provides를 쓰고 있다.

무슨 차이가 있는 걸까?

 

 

@Provides? @Binds?

@Provides : Builder패턴으로 인스턴스가 생성 된다.

@Binds : 구현체 파라미터 하나만 가질 수 있고, static이 아니라서 더 효율적이다. 또한 interface와 관련되어있을때 주로 사용해서 abstract class에서만 사용이 가능합니다.

 

그래서 언제 뭘 써야 하는가?

쉽게 말해 외부 라이브러리를 쓸 때는 Provides, 내부에서 선언한 값을 쓸 때는 Binds를 쓰면 된다.

물론 두 경우 다 Provides를 사용해도 구현은 잘 된다. 다만 Binds가 더 성능이 좋으니 가능하면 Binds를 사용 하는 것을 추천한다.

 

 

이 그림을 보면 보이듯 @Provides가 파일을 하나 더 생성하고 있다.

 

 

 

 

@Qualifier

이 어노테이션은 같은 객체를 리턴하는 여러 코드가 존재할 때 사용한다.

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

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

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

 

아래 예시를 보자

@Provides
@Singleton
@AUTH
fun provideAuthRetrofit(
    @AUTH client: OkHttpClient,
    factory: Converter.Factory,
): Retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .client(client)
    .addConverterFactory(factory)
    .build()

@Provides
@Singleton
@HEADER
fun provideHeaderRetrofit(
    @HEADER client: OkHttpClient,
    factory: Converter.Factory,
): Retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .client(client)
    .addConverterFactory(factory)
    .build()

@Provides
@Singleton
@REQRES
fun provideReqresRetrofit(
    @AUTH client: OkHttpClient,
    factory: Converter.Factory,
): Retrofit = Retrofit.Builder()
    .baseUrl(REQRES_URL)
    .client(client)
    .addConverterFactory(factory)
    .build()

 

셋 다 Retrofit을 리턴해주고 있다.

단, 셋다 다른 역할을 하고 있다.

AUTH와 HEADER는 BASE_URL은 같지만, interceptor의 역할이 다르고, REQRES는 BASE_URL이 다르다.

 

이렇듯 Retrofit을 주입 받으려고 하는데 Retrofit을 주입해주는 함수가 3개나 존재하기 때문에 어느 Retrofit을 주입받고 싶은지 지정해주는 역할을 한다.

 

@Provides
@Singleton
fun provideAuthService(@AUTH retrofit: Retrofit): AuthService = 
    retrofit.create(AuthService::class.java)

그 후 주입받고자 하는 곳에서도 이렇게 어떤 객체를 주입받고 싶은지 명시해주면 된다.

 

 

 

 

Hilt Component

 

Component 생명주기

Hilt는 해당 Android 클래스의 수명 주기에 따라 생성된 구성요소 클래스의 인스턴스를 자동으로 만들고 제거한다.

위와같은 생명주기를 연결해주기 위해 Scope를 설정해준다.

 

 

Scope

앱이 결합을 요청할 때마다 Hilt는 필요한 유형의 새 인스턴스를 지정된 범위로 생성이 가능하다.  그리고 해당 binding에 관한 모든 요청은 동일한 인스턴스를 공유해서 사용한다.

즉, 쉽게말해 해당 Activity에서만 쓰고 객체를 없애고 싶다면 @ActivityScope를 사용하여 범위를 강제해준다는 것이다.

만약 해당 Acitivity에서만 쓰는 것이 아니라 앱의 모든 위치에서 매번 동일한 인스턴스를 사용해야한다면 @Singleton을 이용해 SingletonComponent로 지정해서 사용해주는 것이 좋다.

728x90
반응형