우아한테크코스

자동 DI 라이브러리 만들기 : [우아한테크코스 5기 AN_베르]

베르_최성훈 2023. 10. 2. 15:10

벌써 우아한테크코스 레벨 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 에 객체의 프로퍼티와 메서드에 접근할 수 있는 방법이다.

 

리플렉션에 대한 내용은 다음 글 참고!

[ 리플렉션 링크 ]

 

Reflection [우아한테크코스 5기 AN_베르]

자동 DI 블로그 글을 쓰다가 리플렉션에 대한 정리를 먼저 하고 넘어가는게 좋다고 판단되어 주제로 선정하였다. 자동 DI 를 구현하려면 리플렉션을 먼저 알아야한다. Reflection 이 뭐지? kotlin in Act

seonghoonc.tistory.com

 

 

Hilt 의 방식을 참고해 만들었다.

 

1. DIApplication 정의

 

Hilt 에서 @HiltAndroidApp 을 붙이면 컴파일 할 때 Hilt_Application 를 상속받도록 구조를 변경시켜 버린다.

// 어노테이션을 붙이면
@HiltAndroidApp
class MyApplication : Application()

// 아래와 같이 변한다
class MyApplication : Hilt_Application()

위는 코드젠(Code Generation) 방식이라서 가능한 것이다. 나는 Reflection 을 사용할 것이기 때문에  직접 DIApplication 을 상속받도록 해주었다.

 

DIApplicationInjector 에게 ContainerModule 을 주입한다.

 

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

 

GitHub - woowacourse/android-di

Contribute to woowacourse/android-di development by creating an account on GitHub.

github.com