# Table of Contents
# 안드로이드 의존성 주입 라이브러리들
안드로이드에서 사용하는 대표적인 의존성 주입 라이브러리는 세 가지 입니다.
Koin
Dagger 2
Hilt
Koin
은 코틀린 환경에서 사용할 수 있는 의존성 주입 라이브러리 입니다. 다른 의존성 주입 라이브러리에 비해 상대적으로 배우고 쉽습니다. 그러나 Koin
은 런타임에 리플렉션을 통해 의존성을 주입해주므로 성능이 저하될 수 있습니다.
Dagger
나 Hilt
는 의존성 주입에 어노테이션을 사용합니다. 어노테이션은 컴파일 타임에 코드로 변환되므로 컴파일 타임은 길어질 수 있으나 런타임에 성능이 저하되지 않습니다. 따라서 큰 규모의 프로젝트에서는 Dagger
나 Hilt
가 권장됩니다.
# 설정
Koin
을 사용하기 위해 의존성을 추가합니다.
dependencies {
// Koin
implementation "io.insert-koin:koin-android:$koin_verison"
}
# 필드 주입
LoginPresenter
클래스는 다음과 같습니다.
// LoginPresenter.kt
class LoginPresenter {
fun login() {
// do something
}
}
이 객체를 LoginActivity
에 주입하려고 합니다.
// LoginActivity.kt
class LoginActivity : AppCompatActivity() {
private val loginButton: Button by lazy { findViewById(R.id.activity_login_btn_login) }
private val loginPresenter: LoginPresenter // 이 곳에 주입하려고 합니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginButton.setOnClickListener {
loginPresenter.login()
}
}
}
우선 module
을 사용하여 객체를 어떻게 주입할지 선언해야합니다. factory()
를 사용하면 의존성 주입이 필요할 때 마다 새로운 인스턴스를 생성합니다.
// presenterModule.kt
import org.koin.dsl.module
val presenterModule = module {
// LoginPresenter 타입의 변수에 객체를 주입
factory<LoginPresenter> { LoginPresenter() }
}
Application
클래스에서 다음과 같이 Koin
을 초기화합니다. modules()
안에 위에서 정의한 모듈을 전달합니다.
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
class App: Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@App)
modules(presenterModule)
}
}
}
이제 by inject()
를 통해 의존성을 주입할 수 있습니다. 아래와 같이 클래스의 멤버변수에 의존성을 주입하는 방법을 필드 주입
이라고 합니다.
import org.koin.android.ext.android.inject
class LoginActivity : AppCompatActivity() {
private val loginButton: Button by lazy { findViewById(R.id.activity_login_btn_login) }
private val loginPresenter: LoginPresenter by inject() // 의존성 주입
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginButton.setOnClickListener {
loginPresenter.login()
}
}
}
# 파라미터가 있는 경우
LoginPresenter
의 생성자에 파라미터가 있는 예제를 살펴봅시다.
class LoginUseCase {
fun execute() {
// do something
}
}
class LoginPresenter(
private val loginUseCase: LoginUseCase // 이 곳에도 의존성을 주입해야합니다.
) {
fun login() {
loginUseCase.execute()
}
}
이를 위해 useCaseModule
을 정의합니다.
val useCaseModule = module {
// LoginUseCase 타입의 변수에 LoginUseCase 객체를 주입합니다.
factory<LoginUseCase> { LoginUseCase() }
}
그리고 presenterModule
을 수정합니다. LoginPresenter
의 생성자에 LoginUseCase
객체를 전달해야하기 때문입니다. 이때 get()
을 사용하면 Koin
이 적절한 인스턴스를 알아서 주입해줍니다.
// presenterModule.kt
import org.koin.dsl.module
val presenterModule = module {
factory<LoginPresenter> {
LoginPresenter(get())
}
}
새로운 모듈을 Application
의 modules()
에 추가합니다.
class App: Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@App)
modules(presenterModule, useCaseModule)
}
}
}
# 인터페이스에 구현체 주입
인터페이스에 구현체를 주입하는 경우에도 Koin
을 사용할 수 있습니다. UserRepository
인터페이스와 이를 구현한 UserRepositoryImpl
클래스가 있다고 가정합시다.
interface UserRepository {
fun login()
}
class UserRepositoryImpl: UserRepository {
override fun login() {
// do something
}
}
LoginUseCase
클래스의 생성자에 UserRepositoryImpl
객체를 주입하려고 합니다.
class LoginUseCase(
private val userRepository: UserRepository
) {
fun execute() {
userRepository.login()
}
}
이를 위해 새로운 모듈을 생성합니다.
// repositoryModule.kt
val repositoryModule = module {
// UserRepository 타입의 변수에 UserRepositoryImpl 타입의 객체를 주입합니다.
factory<UserRepository> { UserRepositoryImpl() }
}
useCaseModule
도 수정합니다.
// useCaseModule.kt
val useCaseModule = module {
factory<LoginUseCase> { LoginUseCase(get()) }
}
새로운 모듈을 Application
클래스에서 추가합니다.
class App: Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@App)
modules(presenterModule, useCaseModule, repositoryModule)
}
}
}
# 싱글톤
factory()
를 사용하면 의존성을 주입할 때 매번 새로운 객체를 생성합니다. 반면 single()
을 사용하면 주입되는 객체를 싱글톤으로 유지합니다.
val repositoryModule = module {
single<UserRepository> { UserRepositoryImpl() }
}
# ViewModel 주입
Koin
을 사용하면 MVVM
아키텍처에서 ViewModel
을 쉽게 주입할 수 있습니다. LoginViewModel
을 LoginActivity
에 주입해보겠습니다.
class LoginViewModel: ViewModel() {
fun login() {
// do something.
}
}
ViewModel
을 위한 모듈을 정의합니다. 이 때 factory()
나 single()
대신 viewModel()
을 사용합니다.
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val viewModelModule = module {
// viewModel()
viewModel<LoginViewModel> { LoginViewModel() }
}
액티비티에서는 by viewModel()
을 통해 ViewModel
을 주입받을 수 있습니다.
import org.koin.androidx.viewmodel.ext.android.viewModel
class LoginActivity : AppCompatActivity() {
private val loginButton: Button by lazy { findViewById(R.id.activity_login_btn_login) }
private val loginViewModel: LoginViewModel by viewModel() // ViewModel 주입
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginButton.setOnClickListener {
loginViewModel.login()
}
}
}
모듈을 Application
클래스에 추가합니다.
class App: Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@App)
modules(viewModelModule)
}
}
}
# 뷰모델의 생성자에 파라미터가 있는 경우
Koin
을 사용하지 않는 프로젝트에서는 생성자의 파라미터가 있는 뷰모델을 생성하기 위해 ViewModelFactory
를 정의해야합니다. 그러나 Koin
을 사용하면 ViewModelFactory
를 자동으로 생성해주기 때문에 직접 ViewModelFactory
를 정의할 필요가 없습니다.
class LoginViewModel(
private val loginUseCase: LoginUseCase
) : ViewModel() {
fun login() {
loginUseCase.execute()
}
val viewModelModule = module {
viewModel<LoginViewModel> { LoginViewModel(get()) }
}
import org.koin.androidx.viewmodel.ext.android.viewModel
class LoginActivity : AppCompatActivity() {
private val loginButton: Button by lazy { findViewById(R.id.activity_login_btn_login) }
// 뷰모델 주입
private val loginViewModel: LoginViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginButton.setOnClickListener {
loginViewModel.login()
}
}
}