Assisted Inject 사용하기

CmplxN·2020년 8월 29일
0

선수지식 : Dagger2, AAC ViewModel(마지막 예제)

Assisted Inject 개요

Dagger2는 Annotation을 이용해 의존성 주입을 해주는 라이브러리다. 컴파일 타임에 관련 프로세싱이 진행되므로 컴파일 시간이 늦어지지만, 다른 DI 라이브러리(Koin, Kodein)보다 실행시 빠르다는 큰 장점이 있다. 단점으로 학습하기 어렵다는 점이 있지만, 못 쓸정도로 어려운 것은 아니다. 그러므로 현재 Dagger2를 학습해 사용하고 있다.

그런데 Dagger2를 이용해 프로젝트를 진행하다 문제에 봉착했다. 런타임 데이터를 이용해 객체를 생성할 필요가 생긴 것이다. 이때 생각해볼 수 있는 방법은 아래와 같다.

  1. 주입 받을 객체는 Dagger2를 이용하고, 런타임 정보는 setter로 객체 생성 후에 받는다.
  2. BindsInstance를 이용해 런타임 데이터를 그래프에 추가하고 생성자 주입을 쓴다.

그러나 두 방법 모두 좋은 방법이라고 할 수 없다. setter를 이용해도 되는 멤버들은 그렇다 쳐도, 객체에 필수 적인 정보도 있는데, 이것을 생성자가 아닌 setter를 사용하면 안될 것이다.
또한, BindsInstance를 이용하면 생성자 주입을 할 수 있다. 하지만 bolier plate code가 늘어나고, DI의 장점을 살리지 못할 단순 데이터를 주입하게 된다.

이때문에 고민하던 중 멘토님 덕분에 Assisted Inject에 대해 알게 되었다.
Assisted Inject를 이용하면, 런타임 데이터를 포함한 객체를 factory 패턴으로 생성할 수 있다.

쉽게 설명하자면, factory에 런타임 데이터만 넘겨주면 의존성 주입까지 된 객체를 얻을 수 있는 것이다. (WOW)
그외에도 대표적으로 쓰이는 곳은 ViewModel에 SavedStateHandle를 주입하려는 경우, WorkManager에서 Worker의 WorkerParameters를 주입하는 경우라고 한다.

Gradle 세팅

물론 Dagger2 관련 dependencies도 설정해줘야 한다.

implementation 'com.squareup.inject:assisted-inject-annotations-dagger2:0.5.2'
kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.2'

Assisted Inject로 POJO 생성

그러면 먼저 Assisted Inject를 이용해서 POJO(Plain Old Java Object)를 만들어보자.

예제에 쓸 클래스는 간단한 클래스로, Dagger에서 주입받을 객체 A(Int), Assisted Inject를 이용할 객체 B(String)로 이루어진 객체다.

class MyClass (
    private val age: Int, // dagger Injection
    private val name: String // Assisted Inject
)

그러면 본격적으로 예제 코드를 보면서 따라가보자.

1. 생성자에 Annotation을 달아 Assisted Inject에 사용할 멤버와 Dagger로 주입 받을 멤버를 정의한다.

참고로 data class를 사용하면 컴파일 에러가 나오므로, data class는 사용하지 않는다.
자꾸 컴파일 에러가 나다가 data class를 일반 클래스로 바꿨더니 정상 동작했다. 다른 원인일 수 있다.

class MyClass @AssistedInject constructor(
    val age: Int,
    @Assisted val name: String
)

2. Activity에서 Dagger로 주입받을 MyClass의 Factory를 만든다.

아래와 같이 Activity에서 실제로 MyClass 객체 생성에 쓰일 Factory 메소드를 만든다.

@AssistedInject.Factory
interface Factory {
    fun create(name: String): MyClass
}

3. Dagger Module을 수정한다.

기존 Module에서 AssistedModule Annotation을 추가한다.
또한 Module Annotation에 AssistedInject_<your_module_name>::class를 추가한다.
이때 ide는 에러라고 할텐데, 이 AssistedInject 클래스가 컴파일 때 generated 될 클래스이기 때문이다.
다른 부분에서 실수가 없었다면 걱정하지 않아도 된다. (그게 말은 쉽지..?)

@AssistedModule
@Module(includes = [AssistedInject_MainActivityModule::class])
class MainActivityModule {
    @Provides // provides Int
    fun provideInt(): Int = 33
    
    @Provides // AAC VM 예제서 사용할 예정
    fun providePi(): Double = 3.14
}

4. MyClass.Factory를 주입받고 Factory 패턴으로 객체를 생성한다.

Dagger만으로 생성할때와 달리 MyClass자체가 아닌 MyClass.Factory를 주입 받는다.
그리고 MyClass.Factory의 create메소드로 MyClass를 생성하면 된다.

class MainActivity : DaggerAppCompatActivity() {
    @Inject
    lateinit var myClassFactory: MyClass.Factory

    lateinit var name: String
    lateinit var myClass: MyClass

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        name = "cmplxn"
        myClass = myClassFactory.create(name)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Assisted Inject로 AAC ViewModel(VM) 생성

이번에는 Assisted Inject로 AAC ViewModel을 만들어보자.
AAC ViewModel은 직접 생성자를 통해 생성하지 않고, ViewModelStoreOwner와 ViewModelProvider.Factory를 사용해서 만든다.
그렇기 때문에 위에서 말한 방법 그대로는 AAC ViewModel을 만들 수 없다.
일단 만드려고 하는 ViewModel의 원형을 보자.

class MyViewModel (
    private val pi: Double, // Dagger Injection
    private val myClass: MyClass // 위에서 만든 MyClass. Assisted Inject
) : ViewModel()

다른 더 좋은 방법도 있겠지만, 여기서는 간단하게 ViewModel에 companion object로 AssistedInject.Factory를 만들어서 구현해본다.
참고로 ViewModelProvider.FactoryAssisted Inject의 Factory 두개가 나오므로 헷갈릴 수 있으므로 Assisted Inject의 Factory는 AssistedFactory로 하겠다.

1. VM 생성자에 Annotation을 추가하고 Assisted Inject Factory를 만든다.

위에서 본 1번과 2번 과정과 같다.

class MyViewModel @AssistedInject constructor(
    private val pi: Double,
    @Assisted private val myClass: MyClass
) : ViewModel() {

    @AssistedInject.Factory
    interface AssistedFactory {
        fun create(myClass: MyClass): MyViewModel
    }
}

2. VM의 ViewModelProvider.Factory를 생성한다.

ViewModelProvider.Factory를 반환하는 함수를 MyViewModel의 companion object로 만들었다.
참고로 getViewModelFactory()의 인자인 factory는 AssistedFactory다.
즉, MyViewModel에 정의한 AssistedFactory를 인자로 받아 ViewModelProvider.Factory를 반환한다.

companion object {
    fun getViewModelFactory(factory: AssistedFactory, myClass: MyClass) =
        object : ViewModelProvider.Factory {
            @Suppress("UNCHECKED_CAST")
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                return factory.create(myClass) as T
            }
        }
}

3. Dagger Module을 수정한다.

POJO 예제의 모듈을 그대로 쓴다.

4. Activity에서 MyViewModel.Factory를 주입받고 VM을 생성한다.

아까 만든 MyClass 부분은 생략했다. (... 부분)
주입받은 assistedViewModelFactory와 앞선 예제서 만든 myClass
getViewModelFactory()의 인자로 넘겨 ViewModelProvider.Factory를 만든다.
그렇게 만든 ViewModelProvider.Factory와 ViewModelStoreOwner(this)를 이용해 MyViewModel을 생성한다.

class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var assistedViewModelFactory: MyViewModel.AssistedFactory

    ...

    private val myViewModel: MyViewModel by lazy { // 중요한 부분
        ViewModelProvider(
            this, // ViewModelStoreOwner
            MyViewModel.getViewModelFactory(assistedViewModelFactory, myClass) // ViewModelProvider.Factory
        )[MyViewModel::class.java]
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        ...
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
    }
}

5. 실행 결과

MyViewModel의 init에 pi와 myClass를 로그로 찍어 출력하도록 했다.
앞서 만든 MyClass는 주입받은 33을 출력하고 assistedInject를 이용한 이름을 출력한다. (뭔가 잘못 만들었는데 나는 33이 아니다...)
MyViewModel은 주입받은 pi 3.14를 출력하고 MyClass를 출력한다. (따로 toString() override 했음)
아래와 같이 결과가 잘 나오는 것을 확인할 수 있다.
결과

profile
Android Developer

0개의 댓글