# Table of Contents

# 안드로이드 의존성 주입 라이브러리들

안드로이드에서 사용하는 대표적인 의존성 주입 라이브러리는 세 가지 입니다.

  • Koin
  • Dagger 2
  • Hilt

Koin은 코틀린 환경에서 사용할 수 있는 의존성 주입 라이브러리 입니다. 다른 의존성 주입 라이브러리에 비해 상대적으로 배우고 쉽습니다. 그러나 Koin은 런타임에 리플렉션을 통해 의존성을 주입해주므로 성능이 저하될 수 있습니다.

DaggerHilt는 의존성 주입에 어노테이션을 사용합니다. 어노테이션은 컴파일 타임에 코드로 변환되므로 컴파일 타임은 길어질 수 있으나 런타임에 성능이 저하되지 않습니다. 따라서 큰 규모의 프로젝트에서는 DaggerHilt가 권장됩니다.

# 설정

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())
    }
}

새로운 모듈을 Applicationmodules()에 추가합니다.

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을 쉽게 주입할 수 있습니다. LoginViewModelLoginActivity에 주입해보겠습니다.

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()
        }
    }
}