안드로이드

리플렉션으로 DI를 만들어보자

강한 맷돼지 2023. 9. 8. 17:17

DI 진하게 맛봐볼래?

우아한테크코스의 꽃인 레벨4의 첫 주제는 "DI툴을 직접 만들어보라"였다.
이를 통해 라이브러리가 전지전능한 존재가 아니라 직접 구현해 볼 수 있는 것임을 느끼고 겸사겸사 DI도 진하게 학습하는것이 목표였다.
또한 DI는 개발 전반에 제일 중요한 개념 중 하나이고 안드로이드 진영만해도 Dagger,Hilt(Dagger 기반이지만), Koin, Kodein??(사실 글쓴다고 자료조사 하면서 첨봄) 등등 라이브러리가 정말 많다.
만약 자동 의존성 주입 라이브러리를 손수 빚어본다면 본질을 파악할 수 있으므로 어느 툴을 쓰던 강력하게 사용할수 있으니 바퀴를 한번 만들어보라였다.
사실 취준을 해야겠다고 생각했던 나에게 너무나도 달콤한 주제라서 그냥 넘어갈수가 없어서 글을 쓰게되었다.


들어가기에 앞서

일단 이 글에서 DI 자체를 설명하자면 길어지니 원리에 대해서는 생략하려고 한다.
구글링 좀만 해보면 car,Engine 예제를 바로 접할 수 있으니 바로 이해 할 수 있을것이다.
안드로이드 진영에서 현재 주로 쓰이는 DI 라이브러리는 Hilt 혹은 Koin이다.
두 라이브러리에대해서 경험이 있다면 뱅크샐러드의 Koin to Hilt 이글을 읽는다면 좀 더 각 라이브러리의 차이점을 체감할수 있을것이다.
각각의 라이브러리의 장단점이 있겠지만 사실상 제공하는 기능을 차이는 크게 없다.(파고들면 있겠지만 사실상 일반적으로 사용하는것들은 각각의 라이브러리 모두 지원한다.)
 
그렇다면 각각의 장단점과 차이는 무엇이 있을까?

Koin

장점

  • 러닝커브가 높지 않다.(물론 이건 Hilt가 아니라 Dagger와 비교해야할것 같다. Hilt는 Koin만큼 쉬워진 편이다.)
  • 100% 코틀린 기반이라 KMM등에서 사용될 수 있다.

단점

  • 런타임에 리플렉션을 이용하여 동작하기 때문에 런타임 퍼포먼스가 떨어진다.(리플렉션으로 동작한다고 알고있었는데 koin은 리플렉션을 최대한 배제 했다고 한다. 그럼 어캐하는거지? 나중에 DI 라이브러리 다 만들고 한번 까봐야겠다. 지금까면 창의력이 없어지니)
  • 컴파일타임에 오류를 잡아낼수가 없다.(치명적 -> DI 관련 테스트를 빡세게 한다면 잡아낼수 있다고는 하지만 이또한 비용이다.)

Hilt

장점

  • 러닝커브가 기반 라이브러리인 Dagger에 비해 엄청 낮아졌다.(근데 Dagger를 실사용해보지 않아서 사실 잘모르겠다.)
  • 컴파일 타입에 주입에 대한 검증을 마치기 때문에 런타임에 오류를 만날 상황이 Koin에 비해 현저히 적다.

단점

  • Koin 보다 조금 더 복잡성이 있긴하다.
  • Dagger기반이기에 java파일을 내부적으로 생성하고 KMM 에서 사용할 수 없다.(최근 KSP로 동작하는 Hilt가 나오고 지원해서 멀티플렛폼 가능성이 열릴수도 있지만 애초에 안드로이드 기반으로 설계되었기 때문에 명칭자체가 안드로이드 컴포넌트로 엮여있어서 KMM에서 사용가능할까 싶다.)

사실 컴파일 타임에 오류를 잡아내고 못잡아내고의 차이가 개발자로서 사용성에 큰 차이가 있기 때문에 사실상 Hilt가 대세로 자리잡고있는 상황이었다.
 
하지만 요즘 KMM이 고개를 처들고 있는 상황에서 Koin을 사용해야하는 것이 강제되었기에 Koin을 사용하는 프로젝트들이 종종 보였던것 같다. 안드로이드만 바라보고 개발한다면 Hilt의 압승이지 않을까 싶다.
 
DI 라이브러리에 대한 외국형들의 견해


이러한 차이는 어디서 오는걸까?

일단 Hilt와 Koin의 장단점을 야기하는 근본적 원인은 각각의 구현방법에있다.

Koin

koin의 경우 Service Locator 패턴을 이용해서 Runtime에 의존성을 주입해주는 형태이다.
(사실 이또한 serviceLocater 패턴이니 아니니 의견이 분분하다.)

이 때문에 runtime 성능이 낮아질수 있고(Reflection은 GC를 많이 동작시키니까) 또한 컴파일타임에 검증이 안되는것이다.
->런타임에 동작하는데 컴파일타임에 뭘 어떻게 해요

 

Hilt

Hilt는 Kapt(요즘 나온건 Ksp)를 이용하여 뚝딱뚝딱 컴파일 타임에 클래스를 죄다 만들어서 주입하고 들어가는것이다.(kapt가 안 익숙하다면 하나를 떠올려보자 kapt가 없으면 databinding코드는 누가 만들어 주나?)
 
어쨋든 이정도 정보만 가지고있었는데 이번에 미션으로 내려온 DI 만들기에 힌트로 Hilt가 어떻게 동작하는지 나와있었다.

 

 

이렇게 Kapt(ksp)를 이용해서 어노테이션이 달려있는 것들을 찾아내서 뚝딱뚝딱 Base class를 만들어서 상속시켜놓고 주입하고 있었던것이다. 진짜 신기하다.
 
그렇기 때문에 빌드중 저런 파일들이 다 만들어지고 이때 왠만한 오류는 다 잡히는것이다.


자 근데 여기서 미션이 Hilt나 Koin을 학습하라는게 아니라 만들어보라는거다.

즉 뭐가 어떻게 구현되었는지는 힌트나 참고사항일 뿐이지 내알바가 아니다.
 
나는 Hilt,Koin 두 라이브러리 모두 가볍게 사용해 보아서 대충 어떻게 만들어야할지 감이왔다.
일단 Hilt방식으로 만들자니 Kapt(ksp)에 대한 학습이 거대한 벽으로 다가왔고 그것을 레아에게 지금 하고가는게 맞냐고 물어봤더니 현업가서 하라고 혼났다.(사실 시간이 없기도하다 Kapt 공부해보고 싶긴한데... 군침이 싹돈다.)
 
그래서 필요한 기술스택에 대한 학습이 되어있고 구현방법이 얼추 눈에 보이는 Koin의 방식을 메인으로 의존성 주입 라이브러리를 만들어보려고 결정했다.
(물론 나는 Hilt를 주로 사용해 왔어서 Hilt와 Koin의 혼종 그 어딘가가 나올것 같다.)


즐거운 라이브러리 만들기 준비물 📦

자 라이브러리를 만들기 앞서 대충 학습해야하는 준비물들에 대해 학습해보고 살펴보는 시간을 가져보자(준비물 안가져온 사람 딱대)

준비물1 : 흑마법 Reflection

수업시간에 Reflection 학습테스트를 통해서 많은 것들을 할 수 있다는 것을 예시로 보여주셨다.
기존의 나의 인식으로는 Reflection의 경우 성능을 떨어트릴 수 있는 요소로써 기피해야 하는 대상이었는데 적절히 사용한다면 정말 많은것을 할 수 있다고 느꼈다.
또한 private 같은 접근제한자 조차 다 제거 해버릴 수 있기 때문에 사실상 원래 테스트에서 고민했던 Private함수 같은 것들을 테스트 할수 있음에 기쁨에 차있었다.
 
하지만 많은 크루원들과 레아와 이에 대한 이야기를 나눠보고 Reflection에 대한 사용은 신중해야겠다는 생각이 그대로 유지되었다.
일단 Reflection은 객체지향을 해치는 흑마법이라는 말을 들어봤을 것이다. 사실 나는 이 부분에 대해서 최초 딱히 공감하지 못했다.
하지만 모든 Private 함수들 또한 모두 테스트를 해서 안정성을 높일것이라는 나의 주장에 레아가 그렇다면 구현을 변경하는 일은 굉장히 어려워질것 이라는 조언을 해주셨는데 한방에 이해가 갔다. 아 리플렉션은 진짜 객체지향을 망가트릴 수 있겠구나 라는 생각이 들었고 적절히 꼭 필요한 상황에서만 사용해야겠다는 결론에 이르렀다.
 
그래서 리플렉션에 사용해 대해 이야기 해보자면
일단 수업에서는 간단하게 조금 사용하는 것들만 보여줬는데 이부분은 그렇게 간단하게 넘어갈 부분이 아니라고 판단해서 Kotlin In Action 10장 애노테이션과 리플렉션을 정리해보았다.
 
어노테이션 부분도 재미있는데 이부분은 일단 생략하려한다.(여러가지 메타데이터들을 각 요소들(함수,프로퍼티,필드,클래스 등등)에 넘겨주어 처리할수 있도록 도와주는 기능이라 생각하면된다. -> Kapt를 학습하여 코드를 build타임에 생성 해주는 s것까지 익힌다면 더 강력해진다.)
 
일단 리플렉션의 부터 살펴보자

리플렉션: 실행 시점에(동적으로) 객체의 프로퍼티와 메서드에 접근할수 있는 기능

리플렉션의 사용처는 타입과 관계없이 객체를 다뤄야하거나 객체가 제공하는 메서드나 프로퍼티 이름을 오직 실행 시점에만 알 수 있는 경우이다.
두 가지 이유 모두 라이브러리를 만들때 많이 겪는 일반적인 상황에 대한 대처에 관한것이다.
 
코인액에서는 이 예시를 직렬화 라이브러리를 통해서 들어놨는데 직렬화 라이브러리의 경우 어떤 객체든 Json으로 변경할수 있어야하고 실행전까지 어떤 클래스를 직렬화 할지 알 수 가 없다. -> 즉 라이브러리를 만들려면 잘써야한다.
 
그래서 코틀린에서 리플렉션은 두가지로 나뉜다.
 

1. Java.lang.reflect 패키지를 통해 제공되는 표준 리플렉션

코틀린이 자바 기반이라 그냥 왠만한 기능들 자바것을 이용하는 것처럼 리플렉션도 자바것을 이용해야하고 이용할 수 있다.
이는 리플렉션을 쓰는 자바 라이브러리도 호환된다는 중요한 키워드이기도 하다.
 

2. Kotlin.reflect 코틀린 리플렉션 Api

자바에는 없는 코틀린의 개념(null관련 타입, 프로퍼티)같은것들을 지원하기 위해 존재한다. 자바와 같이쓰이는 개념은 자바 리플렉션을 쓰라고 코틀린에서 완전 대체할수 있는 복잡한 기능을 제공하지는 않는다고 한다.
 

KClass

KClass 선언 -> 사용할수 있는 메서드
KClass 공식문서
 
이 두 링크를 통해 사용하고싶은 메서드를 찾을때 사용하자
이 링크들을 따라가면 KClass를 통해서 얻어낼 수 있는 함수들이 나열되어있다.
이는 kotlin-reflect를 통해 제공하는 확장함수라고 한다.
 

KCallable

KClass 에서 members 같은것들을 살펴보면 자료형이 KCallable 로 되어있다.

 override val members: Collection<KCallable<*>>

KCallable은 함수와 프로퍼티를 아우리는 공통 상위 인터페이스로 안에는 call이라는 메서드가 있다.
call 메서드를 사용하면 함수를 호출하거나 프로퍼티의 getter를 호출할 수 있다.
call 함수의경우 인자를 vararg 리스트로 전달한다(Any 타입) 즉 원하는 인자를 넘겨서 호출시켜줄 수 있는것이다.

public fun call(vararg args: Any?): R

 

KFunction

우선 클래스명::class 를 통해 KClass 인스턴스를 얻은것처럼 ::함수명 을 통해 KFunction의 인스턴스를 얻을수 있다.
KFunction은 call 메서드를 일단 내부적으로 들고있다. 이 함수의 경우 범용적으로 쓰일 수 있는대신 타입 안정성을 보장하지않는다. -> 인자의 갯수가 2개를 요구하는데 vararg로 인자를 받기에 1개를 넣어도 3개를 넣어도 런타임에 터진다.
이를 위해 KFunctionN 을 사용할수있는데 맨날 데이터 바인딩 할때 람다를 넘기기위해 봤던 거다.
KFunctionN은 invoke를 통해서 호출할 수 있는데 N의 숫자와 맞는 갯수의 인자를 넘겨야지만 돌아게되어있어 오류를 잡아내기 더쉽다.
또한 반환형까지 KFunction1<Int,Unit>이런식으로 관리되기 때문에 KFunctionN을 통해서 관리하는것이 더 안정적일 것이다.
예시를 통해 살펴보자

fun feedPig(foodCount:Int, waterCount:Int):HappyPig
val kFunction: KFunction2<Int,Int,HappyPig> = ::feedPig
​
//이렇게 호출가능하다
kFunction.invoke(3,3)
kFunction(3,4)

 

KProperty

프로퍼티를 나타내는 리플렉션으로 KProperty 는 call 메서드를 가진다.
Call 메서드는 프로퍼티의 getter를 호출한다.
근데 call 대신 get함수를 사용하는것이 더 안정적이고 와닿는다.
 
KProperty는 2가지로 나뉘게 되는데
 
1. 최상위 프로퍼티의 경우 KProperty0 이고 이경우 get 함수는 인자가 없다. -> 멤버 프로퍼티를 보면 왜인지 알수있다,

// 최상위 프로퍼티임
var counter = 0
​
val kProperty = ::counter
kProperty.setter.call(21) // -> 리플렉션을 이용해서 setter 꺼내와서 값을 설정하는 예시
kProperty.get() //-> 최상위라 속한 인스턴스가 없기 때문에 인자로 뭘 넘겨주지 않아도 된다. 

 
2. 멤버 프로퍼티의 경우 Kproperty1 이고 이경우 get 함수의 인자는 1개이다
-> 인스턴스를 넣어주어 그 인스턴스의 프로퍼티 값을 전달받는다.

class Person(val name: String, val age: Int)
​
val person = Person("Alice",29)
val memberProperty = Person::age -> 리플렉션으로 인스턴스의 어떤 프로퍼티를 가져올지 정하는 Kproperty를 가져온다.
memberProperty.get(person) -> person 인스턴스의 age 프로퍼티의 값을 받아온다.

 
이렇게 코인액에는 가지각색의 K국뽕 리플렉션들이 있는데 그냥 공식문서 보는게 더 나을것같고 학습테스트를 통해 익히는게 제일 좋은것같다. 그래서 그냥 우테코 자료에 나와있는 학습테스트가 제일 직관적이고 한방에 와닿아서 첨부하겠다.

package woowacourse.shopping.study
​
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.declaredMemberExtensionFunctions
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.functions
import kotlin.reflect.full.memberExtensionFunctions
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.full.staticFunctions
​
class Person(var firstName: String, val lastName: String, private var age: Int) {
    fun greeting() {}
    private fun fullName() {}
    private fun Int.isAdult() {}
​
    companion object {
        fun noname(age: Int): Person = Person("", "", age)
    }
}
​
class ReflectionTest {
​
    @Test
    fun `변경 가능한 공개 프로퍼티 값 변경`() {
        val person = Person("Jason", "Park", 20)
        Person::firstName.set(person, "Jaesung")
        assertThat(person.firstName).isEqualTo("Jaesung")
    }
​
    @Test
    fun `읽기 전용 공개 프로퍼티 값 변경`() {
        val person = Person("Jason", "Park", 20)
        val lastNameField = Person::class.java.getDeclaredField("lastName")
        lastNameField.apply {
            isAccessible = true
            set(person, "Mraz")
        }
        assertThat(person.lastName).isEqualTo("Mraz")
    }
​
    @Test
    fun `클래스 내에서 선언된 프로퍼티`() {
        val declaredMemberProperties = Person::class.declaredMemberProperties
        assertThat(declaredMemberProperties.size).isEqualTo(3)
    }
​
    @Test
    fun `클래스 내에서 선언된 변경 가능한 프로퍼티`() {
        val mutableProperties =
            Person::class.declaredMemberProperties.filterIsInstance<KMutableProperty<*>>()
        assertThat(mutableProperties.size).isEqualTo(2)
    }
​
    @Test
    fun `변경 가능한 비공개 프로퍼티 변경`() {
        val person = Person("Jason", "Park", 20)
        val firstNameProperty =
            Person::class.declaredMemberProperties.filterIsInstance<KMutableProperty<*>>()
                .first { it.name == "firstName" }
        firstNameProperty.setter.call(person, "Jaesung")
        assertThat(person.firstName).isEqualTo("Jaesung")
    }
​
    @Test
    fun `클래스 및 부모 클래스 내에서 선언된 함수`() {
        val personReflection = Person::class
        // fullName, greeting, isAdult, equals, hashCode, toString
        assertThat(personReflection.functions.size).isEqualTo(6)
        // fullName, greeting, equals, hashCode, toString
        assertThat(personReflection.memberFunctions.size).isEqualTo(5)
        // isAdult
        assertThat(personReflection.memberExtensionFunctions.size).isEqualTo(1)
    }
​
    @Test
    fun `클래스 내에서 선언된 함수`() {
        val personReflection = Person::class
        // fullName, greeting, isAdult
        assertThat(personReflection.declaredFunctions.size).isEqualTo(3)
        // greeting, isAdult
        assertThat(personReflection.declaredMemberFunctions.size).isEqualTo(2)
        // isAdult
        assertThat(personReflection.declaredMemberExtensionFunctions.size).isEqualTo(1)
    }
​
    @Test
    fun `멤버 함수 확장 함수 클래스 내에서 선언된 정적 함수`() {
        val personReflection = Person::class
        // fullName, greeting, isAdult, equals, hashCode, toString
        assertThat(personReflection.functions.size).isEqualTo(6)
        // fullName, greeting, isAdult
        assertThat(personReflection.declaredFunctions.size).isEqualTo(3)
    }
​
    @Test
    fun `클래스 내에서 선언된 정적 함수`() {
        assertThat(Person::class.staticFunctions.size).isEqualTo(0)
    }
}
​

 

준비물2 : 코틀린의 강력한 기능 위임 프로퍼티

우리는 일상적으로 by lazy,by viewModels 이런 위임 프로퍼티 기능들을 사용하고 있었다.
Koin을 베이스로 DI 라이브러리 만들기를 진행중이라면 당연스럽게 Koin 의 by viewModel() 이 떠오를것이다.
(난 by viewModels 를 쿵짝쿵짝 바꿔서 마법가루를 뿌리고 어쩌구 해서 by viewModels 를 Koin이 제어하고있는줄 알았는데 교묘하게 s 가 안붙은 by viewModel() 이었다. 어이없다.)
어쨋든 이렇게 Koin에서 필드 주입을 위해 사용되는 위임프로퍼티는 어떻게 동작하는지 동작원리나 사용법을 알아보고 이에 맞춰서 적용해보자.
 
일단 코인액을 봐보자 -> 330page에 바로 용도가 쭈루룩 설명된다.
코틀린은 강력하고 독특한 위임 프로퍼티를 제공하는데 이를 사용하면 다양한 이득을 얻을수 있다. 정리해보자면
값을 단순 필드에 저장하는것을 넘어서 복잡한 방식으로 동작하는 방식을 쉽게 구현할 수 있다. 그렇다면 이런 반박이 나올것이다.
이건 getter setter 를 커스텀해서 하면되자나 - > 맞다 근데 이 행위를 다른 클래스에 전가(위임)함으로 재활용이 가능하다(범용적으로 만들면 어떠한 행위들을 더 쉽게 일반적으로 할 수 있는 것이다. -> 라이브러리에 적절하다고 생각되지 않는가? 군침이 싹돈다.)
 
예를들어 작업을 위임하는 클래스에서 DB,다른 범용적인 저장요소(어떤 Object 같은거?)에 접근해서 필요한 작업을 처리하고 이 일들을 프로퍼티 자체에 전가한다면
라이브러리 혹은 프로퍼티의 by를 통해 했던 어떤 행위들을 직접할 수 있는 것이다. -> 개쩔지 않는가? 듣기만해도 엉덩이가 들썩거린다.
 
일단 자세한 원리 혹은 내용에 대해서는 직접 코인액을 살펴보는것이 좋고(정말 자세히 설명되어있다. ->옵저버 패턴을 구현해보고 이를 코틀린 기본 클래스로 위임하는것도 보여준다, 특히 lazy 설명하는거보면 구조가 바로 보인다. 이짓을 클래스에 위임하는거구나!!!! -> 그리고 lazy 방식으로 초기화하는게 koin에서 람다로 객체 생성방법을 수집하고 객체를 주입하는것과 비스무리하다.) 여기서는 직관적으로 사용법만 다루려고한다.
 
그래서 그런 getter setter 필드 관련 복잡한 로직들을 다른 클래스에 어떻게 위임(전가)하는지를 지금부터 살펴보자
 
코인액 보면 더럽게 어렵게 설명한다. 근데 간단히 이야기해서 operator 로 getValue, setValue 만 설정해주면된다 그때 변수를 만들어서 필드처럼 사용할수도 있고 이런 구현사항은 모든게 다 지맘이다.
 
또한 var, val 에서 getter, setter 요구하는것 처럼 val 로 초기화하는 변수에서는 위임 객체 또한 사실상 setValue 는 없어도 잘 동작한다. 이런 원칙을 가지고 getter,setter 를 재설정해준다고 생각하도 만들면된다.
 
일반적인 템플릿이라고 할법한 코드를 하나 첨부해보겠다.

import kotlin.reflect.KProperty
​
class DelegateProperty<T>(private var value: T,// 여기로 필요한거 더 넘겨받아도된다.) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value
    }
​
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
        value = newValue
    }
}
​
class Example {
    var sampleProperty: String by DelegateProperty()
}

이런 형식에서 val로만 초기화 한다 가정하면 setValue는 없어도 되고 var 초기화할때 사용하고싶다면 getValue, setValue 둘다 존재해야한다.
이런식으로 내부에 필드값 비슷하게 값 저장소를 만들던 어딘가 DB의 무언가를 꺼내오던 여기부터는 창의력 대결인것이다.
결국 용도에 맞게 getValue setValue를 지정해주면된다.
 
 
또한 이를 확장함수로도 해줄수있는데 이미 있는 객체들에도 간단하게 확장함수로 위임이 가능하게 변경할 수 있다.

import kotlin.reflect.KProperty
​
class Pig(var name: String)
​
operator fun Pig.getValue(thisRef: Any?, property: KProperty<*>): String {
    return name
}
​
operator fun Pig.setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    name = value
}
​
class woowaCourse {
    var pigName: String by Pig("wildBoar")
}

이런식으로도 가능하다는 소리이다.
 
 
getValue 와 setValue의 인자로 넘어오는 thisRef 와 property에 대해서 알아보자면
thisRef 로 넘어오는 값은 예시에서는 범용적으로 사용할 수 있도록 Any 를 지정해 놓았는데 그 프로퍼티가 포함된 객체가 넘어온다(만약 타입이 확실하다면 구체적으로 지정해줘도 좋다.) ex) 메인액티비티에서 필드주입을 하고있다면 메인액티비티가 넘어올것이다.(참조값이 넘어온다.)
또한 property의 경우 KProperty 타입으로 by로 지정될 프로퍼티의 정보들이 날아온다.
 
 
이런것들을 모두 종합하여 미션에서 Koin의 inject 를 대신할 수 있는 Injector라는 위임용 클래스를 하나 만들어 봤다.

package woowacourse.shopping.util.autoDI
​
import kotlin.reflect.KProperty
​
class Injector(val qualifier: String? = null) {
    inline operator fun <reified T : Any> getValue(thisRef: Any?, property: KProperty<*>): T {
        @Suppress("UNCHECKED_CAST")
        return AutoDI.inject(qualifier)
    }
}
​

by viewModel을 대체할 수 있는것 또한 만들어봤다.

package woowacourse.shopping.util.autoDI
​
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.CreationExtras
import woowacourse.shopping.util.autoDI.dependencyContainer.AutoDiViewModelFactory
​
const val UNSUPPORTED_COMPONENT_ERROR = "지원하지않는 컴포넌트에 ViewModel을 주입하셨습니다."
​
inline fun <reified VM : ViewModel> ViewModelStoreOwner.injectViewModel(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline extrasProducer: (() -> CreationExtras)? = null,
): Lazy<VM> =
    when (this) {
        is ComponentActivity -> {
            when (extrasProducer == null) {
                true -> this.viewModels { AutoDiViewModelFactory }
                false -> this.viewModels(extrasProducer) { AutoDiViewModelFactory }
            }
        }
        is Fragment -> {
            when (extrasProducer == null) {
                true -> this.viewModels(ownerProducer) { AutoDiViewModelFactory }
                false -> this.viewModels(ownerProducer, extrasProducer) { AutoDiViewModelFactory }
            }
        }
        else -> throw IllegalStateException(UNSUPPORTED_COMPONENT_ERROR)
    }
​
inline fun <reified VM : ViewModel> Fragment.injectActivityViewModel(noinline extrasProducer: (() -> CreationExtras)? = null): Lazy<VM> =
    this.activityViewModels(extrasProducer) { AutoDiViewModelFactory }
​

이제 창의력을 마음껏 발휘해서 만들어보자
 

여타 부가물: 코틀린 기본문법인데 기본이라기에는 어려운?

여기다가 문법을 다 정리하면 너무 많아지니 키워드들만 쭈루룩 나열하고 넘어가도록 하겠다.
Inline(접근 제한자와의 앙숙관계), reified, noInline, 확장함수, DSL
이런 부분들을 학습해야한다.


자 이제 준비물들을 가지고 이렇게 저렇게 조합하니 코인 비스무리한것이 나왔다. 물론 내부 자료구조, 순회로직도 엉망이고 여타 기능들은 빠져있지만 (테스트도 좀 빠짐) 일단 기본적으로 의존성 주입하는데는 문제가 없는 내가 그냥 일반적으로 쓰던 의존성 라이브러리 기능들은 모두 구현했다.
 
코드가 궁금하다면 한번 들어가서 봐보는것도 추천드린다.
https://github.com/2chang5/android-di/tree/step1
Util.autoDI 패키지의 것들이 구현한 내용이다.
 
이제 추가적으로 리펙터링 및 기능개발후 속편으로 돌아오려고 한다. 속편의 주제는 다음과 같다.

  1. 안드로이드 컴포넌트 생명주기관련 특화기능
  2. 테스트 편의성을 위한 처리
  3. 힐트의 좋은 부분 뽑아오기(어노테이션을 좀 활용해야할듯 -> 미션임)
  4. 창의적인 추가기능
  5. 서비스로케이터와 DI에 대한 고찰

자 이제 다음 속편을 기다리며 안녕!!!
 
 
p.s 역시 킹갓제너럴 제이슨의 옛날이야기는 재미있는데 JVM 기반의 DI, Spring이 서버에만 국한된 프레임워크가 아니다?
등등 많은 이야기를 들었다. 물론 서버 부분은 소화하기 어려워서 소화불량에 걸렸지만 일반적인 부분은 다음글에 녹여보려한다.