이번에는 DI 라이브러리 중 하나인 Koin에 대해서 정리를 해보고자 합니다. 기존에 DI 라이브러리로 유명한건 Dagger 인데, Dagger가 학습 곡선이 높아서 우선 상대적으로 학습 곡선이 낮은 Koin을 학습하고 그 이후에 Dagger도 같이 볼까 합니다.


DI(Dependency Injection)이란?

DI 라이브러리Koin을 설명하기 전에 우선 DI(Dependency Injection)가 무엇인지부터 설명을 하려고 합니다. 일단 Dependency의존성이고 Injection주입이란 의미라 단순 번역은 의존성 주입입니다.

의존성이란 예를 들어서 A라는 클래스가 내부에서 B라는 클래스를 참조하는 경우 A클래스->B클래스 의존성을 갖는다고 말할 수 있습니다.

class B {
    ...
}

class A {

    val b = B() // <- A클래스 내부에서 클래스 B를 생성하여 사용
    ...
}

근데 이렇게 의존성이 생기게 되면 B클래스의 변화가 생기면 의존성을 갖는 A클래스도 같이 변경을 해야하는데, 만약 B클래스가 바뀌면 B클래스의존성을 갖는 모든 클래스변경이 되어야 합니다.

예를 들어서 B클래스C클래스의존성이 생겨서 이를 생성자에서 넣어야 한다면 아래와 같이 바뀌어야 합니다.

// 새로 추가된 클래스
class C {
    ...
}

class B(private val c: C) {
    ...
}

class A {
    
    val b = B(C()) // <- B클래스가 변경됨으로 이 부분에 변화가 생깁니다.
    ...

}

사실 위의 소스에서도 B클래스DI가 발생하였는데 B클래스에서는 C클래스에 대한 의존성생성자를 통해서 주입했습니다. 이렇게 DI가 발생하면 아래와 같은 장점을 가질 수 있습니다.

  • 재사용성을 높여줍니다.
  • 테스트에 용이합니다.
  • 코드를 단순화 시켜줍니다.
  • 종속된 코드를 줄여줍니다.
  • 결합도를 낮추면서 유연성확장성이 향상됩니다.

IoC (Inversion of Control)

또 하나 더 알아야 할 개념으로 IoC가 있습니다. 제어 역전이란 말로 해석이 되며, 제어 역전은 개발자가 직접 프로그램의 흐름을 제어하는 코드를 작성하지 않고 외부 프레임워크의 흐름 제어를 받도록 하는 개발 원칙으로 IoC를 따라 소프트웨어를 개발하면 인스턴스의 생성이나 이벤트 처리 등의 호출을 프레임워크가 알아서 해주게 됩니다.

간단히 말하면 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하는 것입니다.


Koin 이란?

Koin은 코틀린을 위한 DI 라이브러리로 학습곡선이 높은 Dagger에 비해 상대적으로 낮은 학습곡선을 가지고 있으며 순수 코틀린만으로 작성이 되었으며 어노테이션 프로세싱을 및 리플렉션을 사용하지 않기 때문에 상대적으로 더 가볍습니다.

더 자세한 내용을 알고 싶으시면 Koin공식 사이트에서 확인하실 수 있습니다.


실습

설치하기위해서 gradle에 아래와 같이 설정을 합니다. 버전이나 새로운 업데이트 내용은 Koin Github에서 확인하실 수 있습니다.

아래 내용은 Koin 공식 튜토리얼을 따라하면서 정리한 내용입니다.

우선 project에는 아래와 같이 설정을 합니다.

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.3.41'
    ext.koin_version = '2.0.1' // <- 여기서 Koin 버전 설정
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter() // <- Koin 설치를 위해서 필요
        
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

[gradle.Project]

그 다음 app쪽에 dependencies에 아래와 같이 설정합니다.

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.byjw.kointest"
        minSdkVersion 23
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    // Koin for Kotlin
    implementation "org.koin:koin-core:$koin_version"
    // Koin extended & experimental features
    implementation "org.koin:koin-core-ext:$koin_version"
    // Koin for Unit tests
    testImplementation "org.koin:koin-test:$koin_version"
    // Koin for Java developers
    implementation "org.koin:koin-java:$koin_version"

    // Koin for Android
    implementation "org.koin:koin-android:$koin_version"
    // Koin Android Scope features
    implementation "org.koin:koin-android-scope:$koin_version"
    // Koin Android ViewModel features
    implementation "org.koin:koin-android-viewmodel:$koin_version"
    // Koin Android Experimental features
    implementation "org.koin:koin-android-ext:$koin_version"

    // Koin AndroidX Scope features
    implementation "org.koin:koin-androidx-scope:$koin_version"
    // Koin AndroidX ViewModel features
    implementation "org.koin:koin-androidx-viewmodel:$koin_version"
    // Koin AndroidX Experimental features
    implementation "org.koin:koin-androidx-ext:$koin_version"

}

[gradle.app]

이제 설치가 되었고 구현을 해보고자 합니다. Koin은 크게 아래 단계를 통해서 구현을 하게 됩니다.

  1. 모듈 생성하기 (Koin DSL)
  2. Android Application Class에서 startKoin()으로 실행하기
  3. 의존성 주입

우선 Koin DSL을 이용하여 모듈을 설치하여야 하는데 DSL은 무엇인까요? DSL(Domain Specific Language)으로 번역을 하자면 도메인 특화 언어로 위키피디아에는 "특정한 도메인을 적용하는데 특화된 언어" 라고 정의되어 있습니다.

Koin DSL은 아래와 같이 있습니다.

  • applicationContext : Koin 모듈 생성
  • factory : 매번 inject 할때마다 인스턴스 생성
  • single : 싱글톤 생성
  • bind : 종속시킬 class나 interface를 주입
  • get : 컴포넌트내에서 알맞은 의존성을 주입함

모듈은 아래와 같이 만들 수 있습니다.

package com.byjw.kointest

import org.koin.dsl.module

// 여기서 Koin DSL을 이용하여 모듈을 만들어줍니다.
val appModule = module {
    single<Repository> { RepositoryImpl() } // 싱글톤으로 생성
    factory { MyPresenter(get()) } // 의존성 주입때마다 인스턴스 생성, get()을 이용하면 위에서 선언된 적절한 클래스가 주입됩니다.
}

[MyModule.kt]

모듈에서 사용하는 인터페이스입니다.

package com.byjw.kointest

interface Repository {

    fun getMyData(): String

}

[Repository.class]

아래는 인터페이스의 구현 클래스입니다.

package com.byjw.kointest

class RepositoryImpl : Repository {

    override fun getMyData(): String {
        return "Hello Koin"
    }

}

[RepositoryImpl.class]

아래는 위에서 만든 Repository를 사용하는 Presenter 입니다. 생성자에서 repository의 인스턴스가 의존성 주입이 되는데, 모듈에서의 get()을 통해서 자동으로 주입이 됩니다.

package com.byjw.kointest

class MyPresenter(private val repository: Repository) {

    fun sayHello() = "${repository.getMyData()} from $this"

}

[MyPresenter.class]

이제 startKoin()을 실행시키기 위해 Application Class를 만듭니다.

package com.byjw.kointest

import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            androidContext(this@MyApplication)
            modules(appModule)
        }

    }
}

[MyApplication.class]

여기서 주의해야할 점은 Application Class를 만들었으면 AndroidManifest.xmlandroid:name에 적용 해주어야 합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
          package="com.byjw.kointest">

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:name=".MyApplication"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

[AndroidManifest.xml]

실제 사용은 아래와 같이 by inject()를 통해서 바로 의존성을 주입할 수 있습니다.

package com.byjw.kointest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import org.koin.android.ext.android.inject

class MainActivity : AppCompatActivity() {

    private val myPresenter: MyPresenter by inject() // 이 부분을 통해서 의존성 주입이 일어납니다.

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

        myTextView.text = myPresenter.sayHello()

    }
}

[MainActivity.class]