본문 바로가기
개발/Android

[Android] Dependency Injection (a.k.a DI) - 3. 기반코드를 Koin으로 리팩토링 해보자!

by du.it.ddu 2020. 10. 24.
반응형

자, 이전 포스팅에서 작성한 Koin으로 리팩토링 해보자.

github.com/InsertKoinIO/koin

 

InsertKoinIO/koin

Koin - a pragmatic lightweight dependency injection framework for Kotlin - InsertKoinIO/koin

github.com

Koin에 대해선 위 사이트를 방문해보는 것을 추천한다.

간략하게 말하면, Kotlin으로만 작성된 DI Framework이다. Dagger에 비해 상대적으로 쉽게 작성이 가능하다.

단점으로는 런타임, 실제 앱이 실행되는 동안에 DI에 문제가 발생했는지 안했는지 알 수 있다.

(Dagger는 컴파일 타임에 알 수 있다.)

자, 이제 Koin을 사용해보자.


- Dependency 추가

위 깃허브 사이트의 메인에서 하란대로 추가하자.

이 글 작성 시점 최신 버전인 '2.2.0-rc-3' 버전으로 하겠다.

buildscript {
    ext.kotlin_version = "1.4.10"
    repositories {
        google()
        jcenter()
    }
    dependencies {
        ...
        classpath "org.koin:koin-gradle-plugin:2.2.0-rc-3"

    }
}

allprojects {
    ...
}

...

우선 프로젝트 레벨 그래들의 dependencies에 koin 플러그인을 추가한다.

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

android {
    ...
}

dependencies {
    ...

    // Koin AndroidX Scope features
    implementation "org.koin:koin-androidx-scope:2.2.0-rc-3"
    // Koin AndroidX ViewModel features
    implementation "org.koin:koin-androidx-viewmodel:2.2.0-rc-3"
}

그리고 앱 레벨 그래들의 dependencies에 koin을 추가한다.


- 리팩토링

github.com/DuItDDu/Android-Codelabs/tree/master/Android-Dependency-Injection

 

DuItDDu/Android-Codelabs

Android Codelabs. Contribute to DuItDDu/Android-Codelabs development by creating an account on GitHub.

github.com

결과를 먼저 보고싶다면, 위 깃허브에서 koin 브랜치를 체크아웃하면 된다.

변경된 부분은 koin 패키지와 MyApplication이 추가 되었다.

그리고 MainActivity, MainViewModel이 변경되었다.

순서는 koin을 위한 모듈 생성(koin 패키지) -> MyApplication 생성 후 koin적용 -> MainActivity, MainViewModel 수정 순으로 하였다.


- Koin 모듈

// ViewModelModule.kt
val viewModelModule = module {
    viewModel(qualifier = named("Remote")) {
        MainViewModel(get(qualifier = named("Remote")))
    }

    viewModel(qualifier = named("Local")) {
        MainViewModel(get(qualifier = named("Local")))
    }
}

// RepositoryModule.kt
val repositoryModule = module {

    single(qualifier = named("Remote")) {
        DataRepository(get(qualifier = named("Remote")) as RemoteDataSourceImpl)
    }

    single(qualifier = named("Local")) {
        DataRepository(get(qualifier = named("Local")) as LocalDataSourceImpl)
    }
}

// DataSourceModule.kt
val dataSourceModule = module {
    single(qualifier = named("Remote")) {
        RemoteDataSourceImpl()
    }

    single(qualifier = named("Local")) {
        LocalDataSourceImpl()
    }
}

모듈은 위와 같다. 각각의 모듈은 Remote, Local의 Scope name을 갖는다. 실제 현업이라면 상수 혹은 Enum으로 정의하여 활용할 수 있겠다.

ViewModel 생성 시 Scope name에 따라 Remote, Local을 분기한 것이다.


- MyApplication

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin {
            modules(
                listOf(
                    viewModelModule,
                    repositoryModule,
                    dataSourceModule
                )
            )
        }
    }
}

별것 없다. 앞서 생성한 모듈을을 추가하여 startKoin 에 전달한다.

* 메니페스트 파일에 아래와 같이 MyApplication 을 추가하는 것을 잊지않아야 한다.

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        ...
    </application>

- MainActivity, MainViewMode 리팩토링

class MainActivity : AppCompatActivity() {

    private var isRemote: Boolean = false
    private lateinit var dataListAdapter: DataListAdapter

    private val viewModel: MainViewModel by viewModel(qualifier = named(if (isRemote) {
        "Remote"
    } else {
        "Local"
    }))

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    private fun initView() {
        ...
    }

    private fun initViewModel() {
        viewModel.dataList.observe(this) {
            dataListAdapter.dataList = it
        }

        viewModel.loadDataList()
    }
}

class MainViewModel(private val repository: DataRepository) : ViewModel() {

    private val _dataList: MutableLiveData<List<Data>> = MutableLiveData()
    val dataList: LiveData<List<Data>> = _dataList


    fun loadDataList() {
        _dataList.postValue(repository.loadDataList() ?: emptyList())
    }

    fun saveData(data: Data) {
        repository.saveData(data).let {
            if(it) {
                loadDataList()
            }
        }
    }
}

MainActivity는 위와 같이 수정되었다.

기반코드의 initViewModel 메서드에서 했던 행위가 상단의 by viewModel ... 로 수정되었다.

isRemote의 값에 따라 Scope name만 변경되어 전달되고 하는것이 딱히 없다.

이 예시와 달리 isRemote 같은 것이 필요없다면, by viewModel() 로 완료된다.

MainViewModel에선 Factory가 삭제되었다. Koin이 이를 대신해주기 때문이다.


 

자, 이제 ViewModel이 굉장히 많고 다양한 모듈을 가지고, 생성자가 복잡한데 이곳저곳에서 사용되는 모듈이 있다고 상상해보자.

Koin을 사용하지 않으면 Factory만들고, ViewModel을 생성할 때 마다 생성자에 주입할 복잡한 생성자를 갖는 모듈들을 만들어야 한다.

하지만 Koin을 사용하면 Module에서 한 번만 생성하고 언제든지 편하게 가져다 쓸 수 있고, 상황에 따라 객체를 변경할 수도 있다.

이제 DI에 대한 개념과 편리함을 이해했기를 바란다.

다음 포스팅에선 Hilt를 이용하여 동일하게 구현해 볼 것이다.

반응형