💡 Intro
5단계는 하지 않을 것 같다고 했지만.. 이대로 끝나는 것은 아까워서 내가 만든 di 라이브러리를 걷어내고 힐트를 적용해보았다. 이미 한 번 프로젝트에 적용한 상태지만 그 때와는 또 다른 부분에 시야가 트는 좋은 기회가 되었다.
❗️ 기능 요구 사항
- 지금까지 만든 쇼핑 장바구니 앱에 적용된 DI 라이브러리를 Hilt 코드로 교체한다. 기존에 만들어 둔 모듈과 테스트 코드를 삭제하진 않아도 된다.
- 이전 요구사항을 동일하게 만족해야 한다.
🔎 Hilt 적용하기
힐트를 적용하면 1~4단계까지의 고민이 무색해질 정도로 쉽게 구현이 가능하다. 또, 코드가 굉장히 깔끔해진다. 힐트는 어떻게 만든걸까.. 진짜 신기하네
어떻게 적용했는가, 그리고 그 과정에서 어떤 것을 느꼈는가에 더 집중하기 위해 MainActivity를 변경하는 과정을 적어보려 한다.
힐트를 적용하기 앞서 이전 글에서도 보았던 의존성 그래프를 한 번 보고 가자.
초록색 박스는 스코프 애너테이션을 말하고, 파란색 박스는 종속 항목이 설치되는 컴포넌트를 말한다.
컴포넌트는 위에 붙은 스코프 애너테이션의 생명주기를 따라가며, 필요한 종속 항목이 현재 컴포넌트에 없는 경우 상위 컴포넌트를 참조하여 가져올 수 있다.
1️⃣ Application
@HiltAndroidApp
class ShoppingApplication : Application()
ShoppingApplication에 있던 코드를 모두 지우고 @HiltAndroidApp
애너테이션을 붙여주면 끝난다.
공식문서에 따르면 @HiltAndroidApp
애너테이션은 애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 비롯하여 Hilt의 코드 생성을 트리거한다.
2️⃣ MainActivity
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
// private val viewModel by lazy {
// ViewModelProvider(this, ViewModelFactory(this))[MainViewModel::class.java]
// }
private val viewModel: MainViewModel by viewModels()
가장 먼저 보이는 것은 ActivityInjectable을 구현하는 것이 아니라 다시 AppCompatActivity를 구현하는 것이다. @AndroidEntryPoint
애너테이션을 붙임으로써 힐트가 해당 안드로이드 클래스에 종속 항목을 제공할 수 있다고 알려줄 수 있다.
그리고 만들어 둔 ViewModelProvider를 사용할 필요없이 by viewModels()
를 통해 뷰모델의 생성을 위임할 수 있다.
3️⃣ MainViewModel
@HiltViewModel
class MainViewModel @Inject constructor(
private val productRepository: ProductRepository,
@Database
private val cartRepository: CartRepository,
) : ViewModel()
@HiltViewModel
을 사용하여 뷰모델 클래스를 힐트로 주입 가능한 형태로 만들어줄 수 있다.
뷰모델 클래스를 주입 가능한 형태로 만들기 위해 해당 클래스에 @Inject
애너테이션을 추가해야 하고, 만약 추가하지 않으면 컴파일 에러가 발생한다.
@HiltViewModel
애너테이션을 사용한 뷰모델은 HiltViewModelFactory에 의해 생성 가능하며, AndroidEntryPoint 애너테이션이 달린 Activity 또는 Fragment에서 기본적으로 사용할 수 있다. @HiltViewModel
애너테이션은 @Inject
가 포함되어 있는 생성자를 가지고 있어야 한다. 힐트는 해당 생성자의 매개변수에 정의된 종속성이 주입한다. 이 때 @Inject
이 붙어있는 생성자는 하나만 있어야 한다.
4️⃣ Module(SingletonComponent)
@Module
@InstallIn(SingletonComponent::class)
object SingletonProvidesModule {
@Provides
@Singleton
fun provideCartDao(@ApplicationContext context: Context): CartProductDao {
val database = Room
.databaseBuilder(context, CartDatabase::class.java, "krrong-database")
.build()
return database.cartProductDao()
}
@Provides
@Singleton
@Database
fun provideDatabaseCartRepository(cartProductDao: CartProductDao): CartRepository {
return DatabaseCartRepository(cartProductDao)
}
@Provides
@Singleton
@InMemory
fun provideInMemoryCartRepository(): CartRepository {
return InMemoryCartRepository()
}
}
@Module
애너테이션은 의존성 주입 그래프에 필요한 객체들을 제공하기 위한 모듈을 정의하는데 사용한다. 쉽게 말하면 @Module
을 통해 힐트가 주입해주는 종속 항목들을 어떻게 만들고 제공할지를 알려주는 역할을 하는 것이다.
@Provides
애너테이션은 해당 메서드가 종속 항목을 제공하는 메서드임을 알려준다.
@Singleton
애너테이션은 해당 메서드가 제공하는 종속 항목의 수명을 결정한다.
@InstallIn
애너테이션은 종속 항목을 어디에 설치할지를 결정하는 애너테이션이다. 여기서 주의할 점은 설치하는 컴포넌트와 종속 항목의 수명을 결정하는 애너테이션은 항상 쌍을 이루어야 한다는 점이다.
5️⃣ Module(ViewModelComponent)
@Module
@InstallIn(ViewModelComponent::class)
object ViewModelScopeModule {
@Provides
@ViewModelScoped
fun provideProductRepository(): ProductRepository {
return DefaultProductRepository()
}
}
해당 모듈은 뷰모델 컴포넌트에 설치하기 때문에 종속 항목의 수명이 @ViewModelScope
임을 알 수 있다.
🎬 끝내며
의존성 주입을 위해 힐트를 사용하면 아주 간단하게 해결할 수 있다. 내가 한 달 동안 만든 바퀴는 굴렁쇠였고 힐트는 제네시스 바퀴였다. 굴렁쇠 대신 제네시스에 탄 느낌은 흔들리지 않는 편안함을 받았다. 승차감이 너무 좋았다. 하지만 DI미션을 통해 굴렁쇠를 만들어보지 않았다면 힐트의 승차감을 느끼지 못했을 것이며, 바퀴를 이해하려는 생각조차 하지 않았을 것 같다.
레벨 3동안 진행한 프로젝트는 DI에 대한 개념이 크게 잡혀있지 않았다. 끊임없이 들었던 생각은 의존성 주입이라는 것은 레벨 1부터 했던 것 같은데 왜 지금 시점에서 다시금 이야기가 나오는 것이지? 지금 말하는 DI는 그 DI와 다른 것인가? 였다. 많이 혼란스러웠다..
이번 미션을 끝내면서 그 혼란이 많이 잠재워졌는데, 내가 느끼기엔 이전까지 진행한 것은 절반의 DI라고 생각한다.
내가 이해한 DI는 인자로 들어오는 값을 인터페이스로 추상화하여 다른 구현체가 들어올 수 있도록 하고, 이를 통해 테스트가 가능하게 하며 변경에 유연하도록 하는 것이다. 그런데 왜 절반의 DI라고 생각하냐고? 넣어주는 구현체가 필요한 범위에서 생성되고 유지되는 것이 아니라, 매번 새로 생성해서 넣어주기 때문이다.
예를 들면 Repository 구현체의 경우 필요할 때마다 생성될 필요가 없다. 직접 상태를 가지고 있기 보다는 어디로부터 오는 값을 필요한 곳으로 옮기는 역할로 볼 수 있기 때문이다. 이러한 역할을 하는 친구들은 한 번만 생성되어도 충분하다.
흔히 @Binds
와 @Provides
의 차이는 @Provides
를 사용하면 의존성 주입을 위한 코드가 더 많이 생성된다고 한다. 그래서 @Binds
방식을 추천하는 이야기도 많다. 그런데 얼만큼의 코드가 더 생성이 되는지 실제로 애플리케이션을 설치했을 때 체감이 될 정도로 차이가 나는지 궁금해졌다.
단순히 힐트를 사용하는 것은 공식문서를 참고하면 정말 간단하게 할 수 있다. 하지만 힐트를 ‘잘’ 사용하는 것은 정말 다른 이야기인 것 같다.