본문 바로가기

[Android][Kotlin] Koin #2 - 자세히 알아보기

Koin #2 - Definitions

Definitions

Module 정의

Koin에서 Module은 모든 구성요소를 선언하는 공간, 즉 Koin으로 제공할 객체를 명세하는 곳

module 전역함수를 사용해서 Koin 모듈을 선언

val myModule = module {
    // Dependencies 작성 (제공할 객체)
}

여러 Module 사용

Koin은 구성요소를 반드시 동일한 모듈안에 모두 선언할 필요가 없습니다

Koin은 기본적으로 지연초기화 방식으로 인스턴스 요청 시점에 Module을 통해 인스턴스를 생성하고 의존성을 주입하기 때문에, 서로다른 Module을 여러개로 나누어서 Koin에 사용등록을 하면 요청 시점에 여러 Moudle을 둘러보면서 인스턴스를 생성할 때만 해결하면 됩니다

// ComponentB <- ComponentA
class ComponentA()
class ComponentB(val componentA : ComponentA)

val moduleA = module {
    // Singleton ComponentA
    single { ComponentA() }
}

val moduleB = module {
    // Singleton ComponentB with linked instance ComponentA
    single { ComponentB(get()) }
}

2개의 클래스가 존재하며, ComponentB는 생성자 인수로 ComponentA 인스턴스가 필요합니다

moduleA는 ComponentA 객체를 생성하고 ModuleB는 ComponentB 객체를 생성하는데 ComponentA 객체가 필요

// Start Koin with moduleA & moduleB
startKoin{
    modules(moduleA,moduleB)
}

2개의 Module을 모두 사용등록을 하면 ComponentB 객체 요쳥 시 Koin은 ModuleB에게 ModuleA를 통해 ComponentA 객체를 주입해주고 ComponentB 객체를 생성하여 의존성 주입을 완료합니다

이렇게 가능한 이유는 Koin의 지연초기화 방식으로 Module 등록 시 인스턴스가 즉시 생성되는게 아닌, 요청 시 생성하므로 여러 Module들을 순회하며 서로 상호운용이 가능합니다 

Component 정의

Single

single 컴포넌트는 전체 컨테이너에 영속적인 객체를 생성합니다

즉 해당 객체를 싱글톤으로 제공(App 수명주기 동안 단일 인스턴스)

class AA()

val myModule = module {
    single { AA() }    // AA 클래스 인스턴스를 싱글톤으로 제공
}

AA클래스 인스턴스를 by inject() 또는 get()으로 요청 시 싱글톤의 AA 인스턴스를 제공 

Factory

factory 컴포넌트는 요청할 때마다 매번 새 인스턴스를 생성해서 제공합니다

Dagger의 Provider와 같은 개념으로 생각하면 됩니다

factory 컴포넌트로 제공되는 객체는 컨테이너에 저장하지 않으므로 다시 참조할 수 없습니다

class AA()

val myModule = module {
    factory { AA() }    // AA 인스턴스 요청 시 매번 새로 생성해서 제공
}

Scoped

Scoped 컴포넌트는 명시된 Scope 생명주기에 영속적인 객체를 생성해서 제공합니다

Dagger의 Scope를 생각하면 이해가 쉽습니다

scoped 컴포넌트를 사용하기 위해서는 먼저 필수적으로 scope() 함수를 통해 범위를 선언해야합니다

범위(scope)의 이름을 지정하려면 Qulifiernamed (한정자)가 필요합니다

Qulifiernamed(한정자)는 두 가지 종류로 StringQulifier(문자열한정자) 또는 TypeQulifier(타입한정자)로 구분해서 사용

StringQulifier - 문자열 한정자

class A

val myModule = module {
    // String 문자열 한정자
    scope(named("my_scope")) {
        scoped { A() }
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

// Scope 인스턴스 생성
val myScope = getKoin().getOrCreateScope("id", named("my_scope"))

// Scope 통해 의존성 주입
val a = myScope.get<A>()

먼저 StringQulifier 문자열 한정자 사용방법입니다

Scope 클래스는 ScopeIdname 속성을 생성자 인수로 받습니다

ScopeId는 고유 식별자 / name은 Scope 타입 유형으로 name을 통해 사용할 Scope을 정의합니다

생성된 Scope 인스턴스를 통해 scope 범위에 포함된 scoped 컴포넌트를 통해 의존성 주입을 요청합니다

TypeQulifier - 타입 한정자

class A
class B
class C

val myModule = module {
    factory { A() }

    scope(named<A>()) {		// TypeQulifier 타입한정자 1️⃣
        scoped { B() }
        scoped { C() }
    }

    scope<A> {			// TypeQulifier 타입한정자 2️⃣
        scoped { B() }
        scoped { C() }
    }
}

TypeQulifier 타입한정자 선언은 위 처럼 같은 결과를 다르게 선언 가능합니다. 두개 모두 동일한 의미입니다

B와 C 클래스 인스턴스는 A 인스턴스에 범위가 잡혀있습니다. A 인스턴스 존재 범위내에만 의존성 주입이 가능을 의미

// Scope 범위 대상인 A 인스턴스 주입
val a = get<A>()

// A 인스턴스 기반 Scope 생성
val scopeForA = a.getOrCreateScope()

// Scope로 인스턴스 주입
val b = scopeForA.get<B>()
val c = scopeForA.get<C>()

Scope 인스턴스인 socpeForA는 A 인스턴스에 의존적입니다. A인스턴스의 존재 범위에서만 scope를 통해 의존성주입이 가능하게 됩니다

+ scope 속성 사용

// Scope 범위 대상인 A 인스턴스 주입
val a = get<A>()

// Scope 인스턴스 생성 없이 바로 의존성 주입
val b = a.scope.get<B>()
val c = a.scope.get<C>()

Scope 인스턴스를 별도 생성없이, A인스턴스의 scope 속성을 통해 바로 의존성 주입을 할 수 있습니다

Scope 범위 및 연결된 인스턴스 삭제

// `a` 스코프 삭제 & `b`,`c` 인스턴스 해제
a.closeScope()

closeScope() 함수로 Scope 범위인스턴스와 연결된 인스턴스를 모두 해제하고 삭제합니다

의존성 해결 및 주입

각 Component들로 제공할 인스턴스(객체) 생성에 Component 내에서 추가적으로 의존성 해결과 주입이 필요한 경우

Koin 컨테이너로 의존성을 주입하려면 Activity와 다르게 생성자 주입함수를 사용해야합니다 -> get() 함수

get() 함수는 일반적으로 생성자 값을 주입하기 위해 생성자에 사용합니다

// Presenter <- Service
class AA()
class BB(val aa : AA)

val myModule = module {
    // AA 인스턴스 제공 Component
    single { AA() }
    // BB 인스턴스 제공 Component
    single { BB(get()) }	// 생성자 인자 aa 의존성 해결
}

BB인스턴스 생성부분을 보면 생성자 인자 aa 인스턴스를 get() 함수를 통해 해결합니다

추가 기능 

동일 Type 중복? - Qulifier Named  

만약 동일 Type이 서로 다른 의존성으로 주입이 필요하다면? - Qulifier Named

Dagger와 마찬가지로 다수의 동일 Type을 가질 경우, Koin도 의존성 주입을 위한 Type 구분불가 (예외발생)

Dagger에서는 @Named 어노테이션을 사용해서 Type 구분을 했지만, Koin은 named 속성으로 구분
(Dagger - @Named Annotation / Koin - named 속성)

// Drink 인터페이스
interface Drink {}

// Drink 구현 Coffee 클래스
class Coffee(var name:String) : Drink {}

val myModule = module {

    // Drink 타입 바인딩 + named 속성 설정
    factory<Drink>(named("Espresso") { Coffee("Espresso") }

    // Drink 타입 바인딩 + named 속성 설정
    factory<Drink>(named("Americano") { Coffee("Americano") }
}

Coffee 객체를 Drink 타입에 의존성 주입하는 2개의 factory 컴포넌트를 선언합니다

val test : Coffee by inject()	// ❌ 동일 Type 컴포넌트 중복으로 구분 불가 (예외 발생)

val espresso : Coffee by inject(name = named("Espresso"))	// ✔️ Espresso named 설정된 Component로 제공

val americano : Coffee by inject(name = named("Americano"))	// ✔️ Americano named 설정된 Component로 제공

첫번째 의존성 요청에서 예외를 발생합니다. 단순히 Coffee 타입으로만 요청할 경우 Coffee 타입의 Component가 중복되므로 Koin은 구분하지 못하게 되어 예외를 발생

시작시 인스턴스 생성 - createAtStart

인스턴스(객체)가 지연초기화 아닌, 모듈 선언과 동시에 즉시 생성이 필요한 경우엔 ? createAtStart 속성

Koin은 기본적으로 지연초기화(lazy init)가 기본값으로 모든 객체 생성은 요청 시점에 생성 (single + factory 모두)

하지만 모듈 선언과 동시에 생성이 필요할 경우엔?

createdAtStart 속성을 true로 설정 (Single에서만 의미가 있는 효과)

single(createdAtStart = true) { AA() }

생성자 인수 설정 - parametersOf()

Module에 선언한 객체를 생성할 때 그때그때 다른 인자를 넘겨줘야하는 경우엔 Injection parameter 사용

// Coffee 클래스 - item(제품명), price(가격) 프로퍼티 포함
class Coffee(val item: String, var price: Int) {}

val myModule = module {
    // 생성자 인수로 받을 Injection parameter 선언
    factory { (item: String, price: Int) -> Coffee(item, price) }
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 사용방법

var espresso = get<Coffee>{ parametersOf("Espresso", 4300) }	// Parameter 전달

이렇게 Coffee 객체의 의존성 주입 Component Factory에 injection parameter를 선언하여서 객체 생성마다 다른 인자를 받아 설정할 수 있습니다

SingleTon 타입인 single에서는 여러번 호출 시 적용은 아래와 같습니다

val myModule = module {
     single { (item: String, price: Int) -> Coffee(item, price) }
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// single 컴포넌트 3번 할당예시

var espresso = get<Coffee>{ parameterOf("Espresso", 4300) }	// ✔️ single은 맨 처음 선언만 적용

var americano = get<Coffee>{ parameterOf("Americano", 4100) }	// ❌ 적용X, 이미 설정된 Espresso, 4300원의 Coffee인스턴스 반환

var closeCoffee = get<Coffee>()			// ❌ 적용X, 이미 설정된 Espresso, 4300원의 Coffee인스턴스 반환

Module에서 item과 price 인자를 받아 싱글톤의 Coffee 객체를 생성하도록 선언합니다

3개의 get() 호출은 모두 처음 호출한 4300원의 Espresso 객체가 반환됩니다.

싱글톤 single은 첫 인스턴스를 생성한 다음부터는 의존성 요청 시 인자 전달을 생략해도 정상적으로 주입이 가능
하지만 최초 인스턴스 호출 시에는 반드시 인자 전달이 필수적  

속성 주입 - Property Injection

생성자 파라미터 전달은 parameterOf()로 하고, 해당 클래스의 속성주입이 필요한 경우는 아래와 같이 합니다

class B
class C

class A {
    lateinit var b: B	// lateinit, 지연초기화 가능
    lateinit var c: C	// lateinit, 지연초기화 가능
}

// 모듈 정의
val myModule = module {

    single<A> { A() }	// A 인스턴스 제공

    single<B> { B() }	// B 인스턴스 제공

    single<c> { C() }	// C 인스턴스 제공
}

A 클래스는 속성으로 b와 c의 인스턴스를 포함하고 있는 구조입니다

val a : A by inject()

// 속성 주입 inject Properties
a::b.inject()
a::c.inject()

A클래스의 lateinit으로 비어있는 b, c 속성을 주입합니다

이 방법은 리플렉션 API를 사용하지 않습니다

2개의 속성을 한번에 주입하기 위해선 아래 방법이 존재합니다

val a : A by inject()

// 속성 주입 inject Properties
a.inject(a::b, a::c)

이렇게 한번에 속성 주입도 가능합니다. 이 방법은 리플렉션 API를 사용해서 속성 Type을 추측합니다

Type 지정 + as, bind() 바인딩

Module에서 객체 명세 작성 시 별도의 타입을 지정하지 않을 경우 가장 구체적인 Type으로 자동 지정

myModule.kt
interface Drink {}		// Drink 인터페이스

class Coffee() : Drink {}	// Drink 인터페이스 구현체 Coffee

val myModule = module {
    factory { Coffee() } 	// Coffee 인스턴스 생성 : Coffee Type 의존성 인식
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

var espresso = get<Coffee>() 	// ✔️ OK! 정상적으로 factory의 Coffee() 객체 주입

var espresso = get<Drink>() 	// ❌ Error! 모듈에 Drink 타입의 의존성을 찾지못함 

Drink 인터페이스 구현체인 Coffee 클래스를 선언하고 Module의 객체 명세는 Coffee()로 선언합니다
이렇게 별도로 Type을 지정하지 않을 경우엔, 선언한 Coffee() 생성자의 클래스 Type을 자동 지정합니다 

따라서 get() 으로 Coffee 인스턴스를 요청하면 정상적으로 주입이 가능하고, Drink 인스턴스를 요청하면 찾을 수 없음

Drink 인터페이스의 구현체인 Coffee 인스턴스를 Drink 타입으로 바인딩하고 싶다면? 

myModule.kt
val myModule = module {

    factory<Drink> { Coffee() }			// 1️⃣ Drink 타입만 주입 가능

    factory { Coffee() as Drink }		// 2️⃣ Drink 타입만 주입 가능

    factory { Coffee() } bind Drink::class	// 3️⃣ Coffee + Drink 타입 모두 주입 가능
}

1️⃣ factroy에 Type parameter 명시
    : 해당 Factory 컴포넌트는 Drink Type으로 강제 설정

2️⃣ Coffee 인스턴스를 Drink 타입으로 캐스팅(as) 
    : Coffee()로 생성되는 객체를 as 캐스팅 연산자로 Drink 타입으로 캐스팅
      ( 캐스팅을 하기위해선 Coffee와 Drink가 상속관계로 되어있어야 함 )

3️⃣ 복수 타입 연결 Bind
    : Bind는 복수 타입의 연결 구현, 즉 CoffeeDrink 모두에 연결해서 두 타입으로 의존성 주입이 가능

제네릭 다루기

Koin에서는 제네릭 형식 인수를 구분하지 않기 때문에, 제네릭 형식을 사용할 경우엔 named 속성으로 구분해야합니다

예를 들어, 두 개의 다른 Type인 List를 선언합니다

val myModule = module {

    factory { ArrayList<Int>() }	// 1️⃣ Int 타입 List

    factory { ArrayList<String>() }	// 2️⃣ String 타입 List
}

Int 타입과 String 타입의 서로다른 List를 선언합니다. 분명 서로다른 타입의 List지만 Koin은 동일한 List 타입으로 인식하기 때문에 예외를 발생합니다

val myModule = module {

    factory(named("Ints")) { ArrayList<Int>() }	// 1️⃣ Int 타입 List

    factory(named("Strings")) { ArrayList<String>() }	// 2️⃣ String 타입 List
}

이렇게 named 속성을 설정함으로 Koin이 서로 다른 Type List로 인식하게 만들어줘야 사용이 가능합니다