목차
1. 공부 이유
1. 공부 이유
지난 학기 객체지향 프로그래밍 수업에서 의존성 주입(DI)에 대해 배웠다. 이에 대해 이해한 내용을 정리해보면, 객체들이 각각의 일을 수행하다보면 객체들 사이에 의존성이 생기게 된다. 의존성이 생긴다는 것은 한 클래스가 변화했을 때, 이와 의존관계가 있는 클래스에도 변경 사항이 생긴다는 것을 의미한다. 그렇게 된다면 변경 사항에 대응하기 위해 드는 시간이나 불편함이 생길 것이고 이를 의존성 주입으로 해결할 수 있는 것이다. 그래서 나는 의존성 주입을 객체가 하던 일을 대신해주는 무언가를 만들어서 객체가 이를 신경쓰지 않아도 되게 하는 것으로 이해했다.
Codelab을 통해 학습하거나 예제 코드들을 보다보면 @Inject, @Provide 등 어노테이션들이 달린 코드들을 보았는데 이게 뭐지 싶어서 찾아보니 DI와 관련된 어노테이션이라는 걸 알게 되었고, 이에 대해 이해해야 다른 부분의 학습에서도 이해할 수 있을 것이라 생각해서 Jetpack 권장 라이브러리인 Hilt에 대해 학습하게 되었다.
2. Hilt의 사용법
Hilt를 사용하면 개발자가 수동으로 의존성을 주입해주는 걸 대신해준다.
우선 @HiltAndroidApp annotation을 붙여주면 앱의 생명주기에 연결된 컨테이너가 생성된다.
컨테이너란?
컨테이너는 다른 앱 instance를 만드는 방법을 아는 클래스이다. 의존성을 주입해주기 위해서는 종속 항목을 만드는 법에 대해 알고 있어야 하니까, 이를 컨테이너가 해주는 것이다.
@HiltAndroidApp
class MyApplication : Application()
LogsFragment에서 ServiceLocator를 사용하여 다른 인스턴스를 생성해주었다면 이를 Hilt에서는 필드 삽입으로 대체할 수 있다. @AndroidEntryPoint를 사용하면 LogsFragment의 생명주기와 연결된 컨테이너가 생성된다. 그리고 필드 삽입은 삽입하려는 인스턴스 앞에 @Inject를 붙여주면 Hilt가 내부적으로 LogsFragment의 컨테이너에서 인스턴스를 생성해서 이 필드들을 채워주는 것이다.
그런데 여기까지만 하면 Hilt는 이 인스턴스를 어떻게 만드는지 모른다. 어떻게 만드는지도 Hilt에게 알려줘야 하는 것이다. 그래서 필드 주입할 인스턴스의 클래스 생성자에 @Inject constructor()를 추가해줘야 한다. 이렇게 하면 Hilt는 LogsFragment에서 LoggerLocalDataSource와 DateFormatter 인스턴스가 필요한걸 알 수 있고, 이를 어떻게 생성하는지 알기 때문에 인스턴스를 대신 제공할 수 있을 것이다.
//필드에 인스턴스를 삽입하기 전의 코드
class LogsFragment : Fragment() {
private lateinit var logger : LoggerLocalDataSource
private lateinit var dateFormatter : DateFormatter
fun example() {
logger = (context.applicationContext as LogApplication).serviceLocator.loggerLocalDataSource
dateFormatter = (context.applicationContext as LogApplication).serviceLocator.provideDateFormatter()
}
}
//필드에 인스턴스 삽입
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger : LoggerLocalDataSource
@Inject lateinit var dataFormatter : DateFormatter
}
//추가될 인스턴스에 생성자 주입
@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao : LogDao) { ... }
class DataFormatter @Inject constructor() { ... }
인스턴스 범위 지정
앱 전체에서 여러 개의 인스턴스를 생성하지 않고 하나의 인스턴스만 사용하기 위해서는 위의 코드처럼 @Singleton 을 붙여주어야 한다. 해당 어노테이션을 사용하면 인스턴스의 범위를 애플리케이션 컨테이너로 지정하게 되고, 그렇다면 애플리케이션에서 항상 같은 인스턴스를 제공해줄 수 있다.
Hilt 모듈
위에서 필드 삽입을 위해 생성자 주입까지 해주었지만 문제가 있다. LoggerLocalDataSource를 만드는데 LogDao를 만드는 법을 컨테이너가 모르기 때문에 Hilt가 대신 생성해줄 수가 없다. 생성자 주입을 하려고 했지만, LogDao는 인터페이스였기 때문에 생성자 주입을 할 수도 없다. 이럴 때 Hilt 모듈을 만든다.
//LogDao.kt
//생성자가 없기 때문에 생성자 주입을 해줄 수 없다
@Dao
interface LogDao { ... }
//DatabaseModule.kt
@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
@Provides
fun provideLogDao(database : AppDatabase) : LogDao {
return database.logDao()
}
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext : Context) : AppDatabase {
return Room.databaseBuilder(
appContext,
AppDataBase::class.java,
"log.db"
).build()
}
}
@Module은 Hilt에 모듈임을 알려주는 것이고, @InstallIn은 어느 구성요소가 해당 인스턴스에 대해 만드는 방법을 알건지 알려주는 것이다. 여기서는 애플리케이션 컨테이너로 범위를 지정했으므로 SingletonComponent::class로 작성했다. 다른 클래스에 Hilt 구성요소를 연결해주기 위해서는 링크를 참고하면 된다.
그리고 @Provides를 붙여주면 생성자 주입이 되지 않는 타입을 Hilt에게 어떻게 제공해주어야 하는지 알려주는 것이다. 해당 함수는 인스턴스를 제공해야 할 때마다 실행된다. 리턴 타입은 알려주려는 타입으로, 함수 파라미터는 Hilt가 제공해주기 위해 알아야 하는 것이다. 위의 코드에서는 LogDao를 제공해주기 위해 AppDatabase에 대해서 Hilt에게 알아야 한다고 알려주는 것이다.
여기서 또 AppDataBase에 대해 Hilt가 어떻게 만드는지 모르기 때문에 알려주어야 한다. 그런데 AppDataBase는 Room이 생성해주기 때문에 생성자 주입을 할 수 없다. 앱 전체에서 하나의 인스턴스만 사용할 것이므로 @Singleton을 붙여주고, Hilt에게 알려주기 위해 @Provides도 붙여준다. 그리고 applicationContext에 대해 제공해주어야 하는데 이는 Hilt에서 기본 결합으로 제공해주므로 @ApplicationContext를 붙여주면 된다. 그리고 LogsFragment는 MainActivity에서 호스팅되는데, 호스팅하는 Activity에도 @AndroidEntryPoint를 붙여준다.
//NavigationModule.kt
@InstallIn(ActivityComponent::class)
@Module
//추상 함수를 포함하는 class는 추상 클래스여야 함
abstract class NavigationModule {
@Binds
abstract fun bindNavigator(impl : AppNavigatorImpl) : AppNavigator
}
//AppNavigatorImpl.kt
class AppNavigatorImpl
@Inject constructor(private val activity : FragmentActivity) : AppNavigator {
...
}
인터페이스 구현을 Hilt에게 알려주기 위해서는 @Binds 를 사용하는 방법도 있다. 단 @Binds는 추상 함수에 달아야 하며, 반환 타입은 구현할 인터페이스, 파라미터로는 인터페이스 구현 타입으로 지정해야 한다. @Binds와 @Provide는 한 모듈 내에서 같이 사용할 수 없다. 마찬가지로 AppNavigatorImpl에 관해서도 생성자 주입을 통해 Hilt에게 알려주면 된다.
3. 후기, 이후 계획
협업할 때에는 DI를 적용한 적이 없어서 불편함을 못 느꼈지만, 다른 사람들이 작성한 코드들을 볼 때마다 주석을 이해하지 못해 제대로 이해했다는 느낌을 못 받은 적이 많았다. 이에 대해서 알 수 있어서 이후에 코드를 볼 때 이해하기가 수월해질 것 같다. 다만 @Binds와 @Provides 의 내부 구현에 차이에 대해서 알고 싶어서 찾아봤는데 이해를 하지 못해서 그 점을 못 짚고 넘어가는 것이 아쉽다. 그래서 일단 @Binds는 내가 만든 인터페이스를 사용할 때, @Provide는 외부 라이브러리를 사용할 때 적용하려고 한다. 제대로 이해하지 못했는데 사용한다는 것이 문제가 될 수 있다고 생각하지만, 사용하는 과정에서 이해를 하는 경우도 있기 때문에 사용을 해보려고 한다.
새로운 지식을 쌓는것도 중요하지만, 지식을 활용하는 것도 중요하다고 생각해서 지금까지 공부했던 ViewModel, DI, LiveData, Coroutine 중심으로 실습을 해보려고 한다. 이 과정에서 코루틴과 Flow에 대한 이해도 높이려고 한다.
실습한 내용은 Github 링크를 통해 확인하실 수 있습니다.
참고 문헌
Using Hilt in your Android app
'Kotlin_study > CodeLab' 카테고리의 다른 글
[Codelab] Coroutine에 대해 이해하기 2(with LiveData, Flow) (0) | 2023.01.13 |
---|---|
[Codelab] Coroutine에 대해 이해하기 (0) | 2023.01.11 |
[Codelab] LiveData에 대해 이해하기(+ DataBinding) (0) | 2023.01.09 |
[Codelab] ViewModel에 대해 이해하기 (0) | 2023.01.06 |