벌써 우아한테크코스 레벨 4!
배우면 배울수록 배워야 할 것도 많아지고 기억해야 할 것도 많아지는 기분이다.
레벨 3에선 팀 프로젝트 진행을 위해 미션이 사라졌었다.
레벨 4엔 "심화 미션" 이라는 이름으로 다시 미션이 돌아왔고 더 깊은 공부를 할 수 있게 되었다.
그 첫 번째는 “자동 DI 구현하기”
Dagger Hilt 나 Koin 을 제대로 사용해본 적 없이 수동 DI 만 구현해본 나에게 자동 DI 가 무슨 말인지 이해조차 되지 않았다.
그래서 처음엔 수동 DI 부터 만들어 보았다.
private val viewModelFactory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(MainViewModel::class.java) -> MainViewModel(
productRepository = repositoryContainer.productRepository,
cartRepository = repositoryContainer.cartRepository,
)
modelClass.isAssignableFrom(CartViewModel::class.java) -> CartViewModel(
cartRepository = repositoryContainer.cartRepository,
)
else -> throw IllegalArgumentException("ViewModel 타입 오류 : ${modelClass.name}")
} as T
}
}
ViewModelFactory 를 위와 같이 작성했다.
viewModel 의 타입을 확인해서 그 에 맞는 repository 를 container 에서 꺼내어 주입해준다.
자동 DI 와 수동 DI 의 차이점
자동은 주입 로직은 ViewModel 이 100개 생긴다고 100개의 주입 로직이 생기는 것이 아니다.
위 코드는 새로운 ViewModel 이 생길 때 마다 ViewModelFactory 로직 수정이 불가피하다.
하나의 로직으로 100개의 ViewModel 이 자동 주입되도록 구현해야한다.
자동 DI 를 구현하기 위해서는 Code Generation,Reflection 을 사용해야한다.
개발하면서 잘 사용하지 않는 리플렉션…무엇일까?
리플렉션은 RunTime 에 객체의 프로퍼티와 메서드에 접근할 수 있는 방법이다.
리플렉션에 대한 내용은 다음 글 참고!
Hilt 의 방식을 참고해 만들었다.
1. DIApplication 정의
Hilt 에서 @HiltAndroidApp 을 붙이면 컴파일 할 때 Hilt_Application 를 상속받도록 구조를 변경시켜 버린다.
// 어노테이션을 붙이면
@HiltAndroidApp
class MyApplication : Application()
// 아래와 같이 변한다
class MyApplication : Hilt_Application()
위는 코드젠(Code Generation) 방식이라서 가능한 것이다. 나는 Reflection 을 사용할 것이기 때문에 직접 DIApplication 을 상속받도록 해주었다.
내 DIApplication 은 Injector 에게 Container 와 Module 을 주입한다.
abstract class DIApplication : Application() {
lateinit var injector: Injector
override fun onCreate() {
super.onCreate()
inject()
}
abstract fun inject()
}
class ShoppingApplication : DIApplication() {
// 모듈: 주입 방법 명세
// 컨테이너: 인스턴스 저장소
override fun inject() {
injector = Injector(Container(), NormalModule)
}
}
2. Module 정의
module 은 내가 의존성을 어떻게 주입해주고 싶은지 Injector 에게 명세하는 역할을 한다.
object NormalModule : Module {
// 싱글톤 객체를 해당 Qualifier 어노테이션과 타입이 일치하는 곳에 주입한다.
@Singleton
@Qualifier(qualifiedName = "OnDisk")
fun provideOnDiskCartRepository(cartProductDao: CartProductDao): CartRepository =
CartOnDiskRepository(cartProductDao)
@Singleton
@Qualifier(qualifiedName = "InMemory")
fun provideInMemoryCartRepository(): CartRepository = CartInMemoryRepository()
// 타입이 일치하는 곳에 매번 새로운 인스턴스를 반환한다.
fun provideProductRepository(): ProductRepository = ProductDefaultRepository()
...
}
3. Container 정의
싱글톤이거나 혹은 구성변경에도 살아남아야 하는 것들은 저장해 놓을 저장소가 필요하다. Type 과 Qualifier 를 key 로 Map 에 저장했다.
class Container {
private val store: HashMap<StoreKey, Any> = hashMapOf()
fun getInstance(type: KClass<*>, qualifiedName: String?): Any? {
return store[StoreKey(type, qualifiedName)]
}
fun setInstance(instance: Any, type: KClass<*>, qualifiedName: String?) {
store[StoreKey(type, qualifiedName)] = instance
}
data class StoreKey(
val clazz: KClass<*>,
val qualifiedName: String?,
)
}
4. Injector 로 주입
참고로 Injector 는 코드 양이 많아서 많은 부분을 생략했다.
class Injector(private val container: Container, private val module: Module) {
// 생성자를 받아서 생성한다
fun <T : Any> createBy(context: Context, constructor: KFunction<T>): T {
...
}
// 프로퍼티에 주입한다
fun injectProperties(context: Context, target: Any) {
...
}
...
}
위 와 같이 두가지 Public 함수를 제공한다.
1. 생성자에 필요한 것들을 주입해서 인스턴스를 생성
2. 생성된 Target 인스턴스 에 주입이 필요한 Property 가 있는지 확인하고 주입
생성자 주입 함수를 호출하면 다음과 같은 순서로 주입한다.
1. 생성자 Param 들 에 해당되는 인스턴스를 다 가져온다.
1 - 1. 다 가져오면 생성한 후 반환한다.
1 - 2. 프로퍼티 주입 실행
2. 생성자 Param 에 해당되는 인스턴스 하나를 가져온다.
2 - 1. 해당 param 타입의 인스턴스가 Container 에 있다면 거기서 가져온다.
2 - 2. Container 에 없다면 새로 생성한다.(3 으로 이동)
3. 타입에 맞는 인스턴스를 생성한다
3 - 1. 모듈에서 param 에 타입에 맞는 함수를 찾는다.
3 - 2. 그 함수에 param 이 없다면 함수를 호출해 인스턴스를 생성한다.
3 - 3. param 이 있다면 그 param 에 맞는 인스턴스부터 가져온다 (2로 이동, 재귀 함수)
3 - 4. 다 가져왔으면 생성하고 반환한다. (만약 싱글톤이면 컨테이너에 저장한다)
프로퍼티 주입 함수를 호출하면 다음과 같은 순서로 주입한다.
1. 객체의 프로퍼티들을 주입한다.
1 - 1 프로퍼티 중 @Inject 가 붙은 것들을 찾는다.
1 - 2 해당되는 인스턴스를 가져온다 (생성자 주입의 2 로 이동)
1 - 3 인스턴스를 주입한다.
이렇게 재귀적으로 주입하도록 만들면 자동 DI 를 만들 수 있다!
후기
처음엔 Reflection 사용 방법도 잘 모르고 자동 DI 가 어떤 것을 말하는 것인지 이해가 안돼서 힘든 미션이었다.
하지만 자동 DI 를 어렵게 만들고 나서 Hilt 를 접했는데 이해하기 너무 쉬웠다...
예전에는 정말 어렵다고 생각했는데...
Hilt 를 단시간에 깊고 빠르게 이해할 수 있었고 심화 레벨에 걸맞는 좋은 미션이었다 ! ㅎㅎ
미션과 관련해서 생략된 코드가 많은데 해당 Repository 를 참고해주세요!
https://github.com/woowacourse/android-di
'우아한테크코스' 카테고리의 다른 글
내가 꿈꾸는 프로그래머로서의 삶 (0) | 2023.10.08 |
---|---|
[안드로이드] App 로깅 전략 with Firebase Analytics [우아한테크코스 5기 AN_베르] (0) | 2023.08.06 |
Kotlin coroutine 강의로 이해하기 - 2 : [우아한테크코스 5기 AN_베르] (0) | 2023.07.30 |
Kotlin coroutine 강의로 이해하기 - 1 : [우아한테크코스 5기 AN_베르] (0) | 2023.07.16 |
[안드로이드] MVP 패턴을 MVVM 패턴으로 리팩터링하기 (0) | 2023.07.09 |