[DI] Koin

Minji Jeong·2022년 7월 15일
0

Android

목록 보기
28/39
post-thumbnail
이전에 작성했던 dependency injection 포스팅에서 Koin에 대해 짤막하게 소개했었다. 그런데 이번에 Hilt에 대해 공부하고 글을 올리면서 Koin에 대해서도 아직 지식이 부족함을 느꼈고, 따라서 좀 더 깊게 공부해서 부록이 아닌 단독으로 포스팅을 올려야겠다는 생각이 들어 이렇게 글을 쓰게 되었다.
👉 Koin Official Document
👉 관련 포스팅 : Dependency Injection
👉 관련 포스팅 : Hilt

Koin in Android

Koin은 Dagger, Hilt처럼 안드로이드에서 사용되는 대표적인 DI 프레임워크 중 하나로, 순수 코틀린으로 작성되었으며 다른 DI 프레임워크보다 러닝커브가 낮고 경량화되었다.

Koin은 코틀린으로 작성되었기 때문에 코틀린 개발환경에 도입하기 쉬우며, ViewModel 주입을 간단하게 할 수 있는 별도의 라이브러리를 제공하는 장점 덕에 많이 사용된다. 다만 컴파일 타임에 주입대상을 선정하는 다른 DI 프레임워크에 비해 런타임에 Service Locating을 통해 동적으로 인스턴스를 주입하기 때문에 런타임 퍼포먼스가 다소 떨어질 수 있고, 이로 인해 런타임 오버헤드가 발생할 수 있다는 단점도 존재한다.

1. Koin set up in Android Studio

이 포스팅에서 Koin 셋업을 위한 Gradle 설정은 안드로이드 스튜디오 chipmunk 버전을 기준으로 한다.

// settings.gradle
repositories {
    mavenCentral()    
}

// build.gradle(module)
dependencies {
    // Koin for Android
    implementation "io.insert-koin:koin-android:$koin_version"

    // Koin Test
    testImplementation "io.insert-koin:koin-test:$koin_version"
    testImplementation "io.insert-koin:koin-test-junit4:$koin_version"
}

2. Koin DSL

Koin은 Hilt처럼 Annotation을 사용하지 않고, Kotlin DSL을 사용해 개발자가 좀 더 편리하게 의존성을 주입할 수 있도록 API를 제공한다.

Hilt 에서의 모듈 명세

@Module
@InstallIn(ViewModelComponent::class)
internal object RepoModule {
    @Provides
    @ViewModelScoped
    fun provideRepo() : Repository = Repository()
}

Koin 에서의 모듈 명세

val dbModule = module {
	single {
        Repository())
    }
}

val viewModelModule = module {
	viewModel {
    	MainViewModel(get())
    }
}

💡 Kotlin DSL ?

DSL은 Domain Specific Language의 약자로, 직역하면 특정 분야에 최적화된 프로그래밍 언어다. DSL은 기존의 명령형 코드 대신 선언적 코드 형식을 따른다. Kotlin DSL은 가독성 좋고 간략한 코드를 사용하는 코틀린의 언어적인 특징을 사용하여 Gradle 스크립팅을 하는 것을 목적으로 하는 DSL이다.

2.1 Koin Application DSL & Module DSL

Koin은 Application DSLModule DSL을 제공하는데, Application DSL로 Koin 컨테이너의 구성을, Module DSL로 주입되어야 하는 컴포넌트를 표현할 수 있다.

2.1.1 Application DSL

App.kt

class App: Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin{
            androidContext(this@App)
            modules(
            	dbModule,
            	viewModelModule
            )
        }
    }
}

안드로이드 환경에서 Koin을 사용하기 위해선 먼저 Application을 상속한 클래스가 필요하며, 해당 클래스 내에서 Koin 컨테이너를 실행하기 위해 다음의 DSL을 사용할 수 있다.

startKoin { } : Koin 컨테이너를 실행하기 위한 진입점으로, 실행할 모듈 목록이 설정되어 있어야 한다.

Koin 컨테이너 인스턴스를 구성하기 위해선 다음 중 필요한 것들을 적절히 사용하면 된다.

androidLogger( ) : Koin용 안드로이드 Logger를 설정한다.
androidContext(this@App) : Koin 컨테이너에 안드로이드 컨텍스트를 주입한다.
modules( ) : koin 컨테이너에 로드할 모듈들을 설정한다.
androidFileProperties( ) : assets/koin.properties 파일의 Koin properties를 사용하여 키/값을 저장한다.

생성한 컨테이너 클래스는 메니페스트 파일에 등록해야 한다.

AndroidManifest.xml

<application
	...
    android:name=".App">
</application>

2.2.2 Module DSL

MainViewModel.kt

class ViewModel(private val dao: Dao) : ViewModel(){
    	...
}

Module.kt

val dbModule = module {

    fun provideDatabase(application: Application) : AppDatabase {
        return Room.databaseBuilder(application, AppDatabase::class.java, "EXAMPLE_DB")
            .fallbackToDestructiveMigration()
            .build()
    }

    fun provideDao(database: AppDatabase) : Dao {
        return database.Dao
    }
    
    single {
        provideDatabase(androidApplication())
    }

    single {
        provideDao(get())
    }
}

val viewModelModule = module {
	viewModel {
    	MainViewModel(get())
    }
}

Koin 모듈은 우리가 주입하고 결합할 모듈들에 대한 명세를 수집한다. 새로운 모듈을 생성하기 위해선 다음의 Module DSL을 사용해야 한다.

module { } : Koin 모듈을 생성한다.

모듈 내의 객체들을 정의하기 위해선 다음의 DSL을 용도에 맞게 사용해야 한다.

factory { } : 요청할 때마다 매번 새로운 객체를 생성한다. factory로 제공되는 객체는 컨테이너에 저장하지 않기 때문에 다시 참조할 수 있다.
single{ } : 앱이 실행되는 동안 계속 유지되는 싱글톤 객체를 생성한다. 싱글톤 scope로 해당 객체를 만들어서 사용할 수 있다. 보통 repository, retrofit, db등을 사용할 때 사용한다.
viewModel { } : viewModel 키워드로 모듈을 등록하면 Koin이 해당 ViewModel을 ViewModelFactory에 등록한 뒤 현재 컴포넌트와 바인딩하고, 주입 받을 때도 viewModelFactory에서 해당 ViewModel 객체를 불러온다.
get() : 모듈에 제공된 객체들 중 해당 부분에 들어갈 수 있는 객체를 찾아 넣는다.

3. Injection in Android classes

이제 정의해둔 모듈들을 액티비티, 프래그먼트, 서비스와 같은 안드로이드 컴포넌트에 주입해보자. 모듈을 컴포넌트에 주입하기 위해 사용하는 메서드는 다음과 같다.

// Example
class MainFragment : Fragment() {
	private val presenter : Presenter by inject()
    private val presenter : Presenter = get()
	private val viewModel : ViewModel by viewModel()
}

by inject() : get과 같이 알맞은 의존성을 주입하나, val 변수에만 사용할 수 있다. lazy 방식의 주입으로, 해당 객체가 사용되는 시점에 의존성을 주입한다.
get() : 해당 코드 런타임에 바로 객체를 주입한다.
by viewModel() : ViewModel 객체를 lazy하게 주입한다.
getViewModel() : by viewModel()과 다르게 바로 ViewModel을 주입한다.

또한 하나의 ViewModel 인스턴스는 여러 프래그먼트와 호스트 액티비티 사이에서 공유되어 사용될 수 있다. 공유된 ViewModel은 프래그먼트에서 다음의 메서드를 사용해서 주입될 수 있다.

by sharedViewModel() : 공유된 ViewModel 인스턴스를 lazy하게 주입한다.
getSharedViewModel() : by sharedViewModel()과 다르게 바로 공유된 ViewModel을 주입한다.

val weatherAppModule = module {
    viewModel { WeatherViewModel(get(), get()) }
}
class WeatherActivity : AppCompatActivity() {
    /*
     * Declare WeatherViewModel with Koin and allow constructor dependency injection
     */
    private val weatherViewModel by viewModel<WeatherViewModel>()
}

class WeatherHeaderFragment : Fragment() {
    /*
     * Declare shared WeatherViewModel with WeatherActivity
     */
    private val weatherViewModel by sharedViewModel<WeatherViewModel>()
}

class WeatherListFragment : Fragment() {
    /*
     * Declare shared WeatherViewModel with WeatherActivity
     */
    private val weatherViewModel by sharedViewModel<WeatherViewModel>()
}

Activity, Fragment 외부에서 koin을 사용해 객체 주입하기

액티비티나 프래그먼트가 아닌 일반 클래스에서도 의존성 주입이 필요할 때가 있다. 하지만 일반 클래스에서는 get()이나 by inject()를 사용해 의존성 주입을 할 수 없다. 안드로이드에서 Koin을 사용할 시 컨테이너는 안드로이드 컴포넌트의 생명주기에 맞추어 생성과 파괴가 되도록 만들어져야 한다. 액티비티나 프래그먼트 내부에서 컨테이너를 생성한 경우 해당하는 생명주기를 따르겠지만, 일반 클래스의 경우 생명주기가 없기 때문이다.

여튼 이렇게 일반 클래스에서 Koin을 사용하기 위해 KoinComponent라는 것을 제공하고 있다. KoinComponent를 사용하면 by inject(), get() 등을 사용해 의존성을 주입할 수 있다.

class MyComponent : KoinComponent {

    // lazy inject Koin instance
    val myService : MyService by inject()

    // eager inject Koin instance
    val myService : MyService = get()
}

하지만 다음의 StackOverFlow, Medium post를 확인해보면, 생성자 파라미터를 통해 의존성을 주입받을 수 없는 상황을 제외하면 이 방법은 권장되지 않는다. 따라서 KoinComponent보단, 액티비티나 프래그먼트에서 Koin을 사용해야 하는 클래스를 생성할 때 생성자를 통해 객체를 주입하는 것이 좋다.

class MainActivity : AppCompatActivity() {
	private val dataStoreModule : DataStoreModule by inject()
    
    override fun onCreate(savedInstance: Bundle){
    	super.onCreate(savedInstanceState)
        ...
        Aclass(dataStoreModule)
    }
}
class Aclass(private val dataStoreModule: DataStoreModule) {
	...
}

4. Koin with Compose

3.1 Composable에 의존성 주입

val androidModule = module {
    single { 
    	MyService() 
    }
}
@Composable
fun App() {
    val myService = get<MyService>()
    val myService by inject<MyService>()
}

3.2 Composable에 ViewModel 주입

module {
    viewModel { 
    	MyViewModel() 
    }
}
@Composable
fun App() {
    val vm = getViewModel<MyViewModel>()
    val vm by viewModel<MyViewModel>()
}

References

https://blog.banksalad.com/tech/migrate-from-koin-to-hilt/
https://kotlinworld.com/123
https://jaejong.tistory.com/153
https://medium.com/swlh/dependency-injection-with-koin-6b6364dc8dba

profile
Mobile Software Engineer

0개의 댓글