android

[안드로이드] Di 라이브러리 Koin 에서 Hilt 로 사용하게 된 이유

sieunju 2021. 8. 7. 13:08
반응형

제목과 같이 잘 쓰고 있던 Koin에서 Hilt로 변경하게 된 이유와 사용법에 대해 설명하고자 합니다. 

Koin 실사용 후기

사용법이 정말 쉽습니다.
이게 가장 큰 장점이자 이 이상 설명할것은 없을 거로 보입니다. 저 같은 Third Party 개발자한테는 정말 좋은 라이브러리입니다. 
어떤 기능이나 설계를 쉽고 빠르게 사용하는게 라이브러리지 비용을 엄청 들여서 사용하는 거는 제 기준 좋은 라이브러리가 아니라고 생각합니다.

하지만, 단점도 존재합니다.

단점을 알기 전에 Koin 전체 적인 개념을 알아야 합니다.

앱이 구동되면 Koin은 Application 클래스에서 startKoin 함수를 통해 Di Cache를 만들어 냅니다.

그래서 필요한 클래스에 KoinCompoent 추가후 "by inject" 라는 함수를 호출하여 알맞게 Di Cache에 있는

클래스 들을 가져와 사용할수 있습니다. 그렇다면 Di Cache를 만들다가 에러가 나오는 코드를 넣는다면??

Di Module 일부러 에러가 나도록 설정후 앱을 실행하면 위와 같이 Runtime Exception 발생합니다.

에러 로그 찾기가 정말 힘들고 힘듭니다. 실제 사용하면서 이것땜에 정말 고생 많이 했습니다. 심지어 에러 로그 자체를 분석하면 MainViewModel에서 생기는 것처럼 보이지만 실상은 MainViewModel이 아닌 다른 Module 클래스에서 생기는 에러입니다.

내 코드를 기계어로 변환하는 과정(Compile)에서는 에러가 발생하지 않는다.
앱을 실행할때 에러가 발생한다. Module Class 잘못 만들면 안정성에 문제가 있다..

이게 가장 큰 단점이라고 생각이 듭니다.

그러던 중 Android Developer에서 Dagger2 기반으로 한 Hilt 라이브러리를 공개했습니다. 나온지는 좀 됐지만 좀 묵혀뒀습니다. (항상 1세대는 믿고 거르는 거라고 배웠습니다....😂)

이제 쫌 잘 익은거 같아서 Hilt 사용하는 방법에 대해 익히고 지금은 Koin에서 Hilt로 갈아탔습니다.

Hilt 특징
- 기본 Dagger2 개념정도는 알아야 Hilt 사용 시 쉽게 이해할 수 있습니다.
- Complile 과정에서 에러가 발생하기 때문에 Koin 보다 안전성이 있습니다.
- 따로 ViewModelModule 클래스를 안만들어도 자동으로 코드를 생성해주기 때문에 (DataBinding처럼) 코드 관리 측면에서 장점이 있습니다.
- 사용자가 Annotation Class를 적제 적소에 추가해주면 알아서 Injection을 합니다.

제가 Koin 에서 Hilt로 이주하면서 가장 매력적으로 다가왔던 게 Application에서 코드를 설정할 필요가 없고, ViewModelModule 등등 특정 Module 들을 따로 만들어줄 필요가 없어서 가장 크게 매력적으로 다가왔습니다.

Application 에서 startKoin, viewModelModule 에서 viewModel 생성이 필요 없습니다.

 

Hilt 사용법

서론이 길었습니다. 사용법에 대해 설명하겠습니다.

https://developer.android.com/codelabs/android-hilt?hl=ko#0

 

Android 앱에서 Hilt 사용  |  Android 개발자  |  Android Developers

이 Codelab에서는 Hilt를 사용하여 종속 항목 삽입을 실행하는 Android 앱을 빌드해 보겠습니다.

developer.android.com

저는 위링크를 참고해서 사용했습니다.

Hilt Version: 2.37
Android Studio Version: 4.1.2

 

Gradle
# build.gradle
buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.37'
    }
}
# app/build.gradle
plugins {
    ...
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}
dependencies {
	...
    implementation('com.google.dagger:hilt-android:2.37')
    kapt('com.google.dagger:hilt-android-compiler:2.37')
}
Application
@HiltAndroidApp
class MainApplication : MultiDexApplication() {

    override fun onCreate() {
        super.onCreate()
        ...
    }
}

여기까지가 기본적으로 Hilt 세팅하는 방법입니다. 정말 간단합니다.

Application에 @HiltAndroidApp 선언해주시면 간단합니다.

ViewModel (일반 적인 타입)
@HiltViewModel
class MainViewModel @Inject constructor(
    private val dataSource: NetworkDataSource,
    private val apiService: ApiService,
    private val accountPref: AccountPref
) : BaseViewModel() {
	...
    // Do Something..
}
ViewModel (Activity 간 인자 값을 전달하는 경우)
@HiltViewModel
class TestViewModel @Inject constructor(
    private val argument : SavedStateHandle,
    private val apiService: ApiService
) : BaseViewModel() {
    
    init {
        val id = argument.get<String>(ExtraCode.TEST_ID)
    }
}

Activity 간의 Bundle 데이터를 전달 했다면 위와 같이 SavedStateHandle 변수값을 추가하고 사용하시면 되겠습니다.

Activity, Fragment
@AndroidEntryPoint
class MainActivity : BaseActivity<ActivityMainBinding, MainViewModel>() {
    override val layoutId = R.layout.activity_main
    override val viewModel: MainViewModel by viewModels()
    override val bindingVariable = BR.viewModel
 }

위와 같이 @AndroidEntryPoint 추가 하고 ViewModel를 사용하고 싶다면  ViewModel 위치에 "by viewModels" 선언하면 자동으로 Inject를 하게 됩니다.

보통 Http Network 통신을 자주 사용하는데 해당 Module 에 대해 사용법도 알려드리겠습니다.

Http Network 통신을 실제 사용할때에는
SharedPreference로 사용자 Auth Token을 저장하고
Http Interceptor를 통해 Header 값이라던지 기타 여러 작업을 처리합니다.
@InstallIn(SingletonComponent::class)
@Module
class NetworkModule {

    @Singleton
    @Provides
    fun provideHeaderManager(accountPref: AccountPref) : NetworkHeaderManager = NetworkHeaderManagerImpl(accountPref)

    @Singleton
    @Provides
    fun provideHttpClient(headerManager: NetworkHeaderManager) : RetrofitProvider = RetrofitProviderImpl(headerManager)

    @ExperimentalSerializationApi
    @Singleton
    @Provides
    fun provideApiService(retrofitProvider : RetrofitProvider) : ApiService =
        Retrofit.Builder().apply {
            baseUrl(NetworkConstants.BASE_URL)
            client(retrofitProvider.createClient())
            ...
            // Do Something..
        }.build().create(ApiService::class.java)

    @Singleton
    @Provides
    fun provideNetworkDataSource(apiService: ApiService) : NetworkDataSource = NetworkDataSourceImpl(apiService)
}
HeaderManager: 네트워크 통신을 하면서 헤더에 넣을 정보들을 관리하는 Interface입니다.
RetrofitProvider: Http Network 통신할 때 Retrofit2 사용할 때 HeaderManager를 가지고 헤더 값 처리, Interceptor, Http Client 설정 정보들을 제공하는 Interface입니다. 이후 RetrofitProvider를 가지고 provideApiService 함수 안에 ApiService를 만듭니다.
NetworkDataSource: ApiService를 가지고 네트워크 통신하는데 중간에 데이터를 가공하거나, 페이징 처리 등등 여러 데이터를 가공해서 사용해야 할 때 사용하는 Interface입니다..

 

Util Class

네트워크 통신 말고도 개발할 때 여러 FileProvider, ResourceProvider들이 필요합니다. 이때는 Binds 방식으로 Module 화 했습니다.

@InstallIn(SingletonComponent::class)
@Module
abstract class AppModule {

    @Binds
    abstract fun bindBasePref(basePref: BasePrefImpl): BasePref

    @Binds
    abstract fun bindResourceProvider(resourceProvider: ResourceProviderImpl): ResourceProvider

    @Binds
    abstract fun bindFileProvider(fileProvider: FileProviderImpl): FileProvider
    
    ...
    // Do SomeThing...
}
Provider 클래스 예시
interface ResourceProvider {
    ...
    // Do SomeThing..
}

class ResourceProviderImpl @Inject constructor(
    @ApplicationContext private val ctx: Context
) : ResourceProvider {
	...
    // Do SomeThing..
}

위 코드처럼 ResourceProvider 만들고 AppModule에 Binds 처리해주시면 되겠습니다.

 

자 이제 Hilt에 대한 설정 및 사용법 설명은 끝났습니다. 어떤 점이 변경됐는지 보도록 하겠습니다.

 

Koin(좌) Hilt(우)
Koin(좌) Hilt(우)

간단히 말해서 사용자 입장에서 코드가 많이 줄었고,
Build 할 때 자동 생성되는 코드들이 많아졌다로 볼 수 있겠습니다.

 

마치며..

제가 Koin을 사용하다가 Hilt로 바로 넘어간 경우라 Dagger2의 기본 흐름을 빠르게 습득하고 Hilt 적용하다 보니 중간중간 설명에서 잘못 설명한 부분이 있을 거 같습니다. 만약 있다면 댓글로 남겨주시면 감사합니다.

 

 

반응형