의존성 주입 (DI)
- 프로그래밍에서 구성요소간의 의존 관계가 내부가 아닌 외부를 통해 정의되게 하는 디자인 패턴 중의 하나
- 의존성 주입의 목적은 객체를 생성하고 사용하는 관심사를 분리하는 것
- 클래스와 클래스간에 관계를 맺을 때, 내부에서 직접 생성하는 것이 아닌, 외부에서 주입을 함으로써 관계를 맺게 만드는 것을 의미
장점
- 코드의 재사용 및 의존성 분리 -> 클래스 간의 결합도를 줄임
- 리팩토링이 수월함
- 테스트 편의성 -> 클래스가 의존성을 관리하지 않으므로 테스트를 더 쉽게 수행할 수 있음
단점
- 의존성 주입을 위한 선행작업이 필요하고 코드를 추적하고 읽기가 어려워짐
Dagger2
- 안드로이드 앱에서 의존성 주입을 구현하는 데 사용되는 서드파티 라이브러리
Module, Provide
- Component에 연결되어 의존성 객체를 생성, 생성 후 Scope에 따라 객체를 관리함
- @Module은 클래스에만 붙이고, @Provides는 반드시 @Module 클래스에 선언된 메서드에만 붙임
- Module클래스는 의존성 주입에 필요한 객체들을 Provide메소드를 통해 관리
- Provide 메소드의 파라미터도 컴포넌트 구현체로부터 전달 받을 수 있음
Inject
- 필드, 생성자, 메서드에 붙여 Component로 부터 의존성 객체를 주입 요청하는 annotation
- 객체를 주입받기 때문에 객체의 생성이 클래스에 의존적이지 않고 보일러플레이트코드를 작성할 필요없이 클래스를 테스트하기가 수월해짐
- 인터페이스는 생성자가 없으므로
@Inject
를 붙일 수 없음
Component
- Interface 또는 abstract class에만 사용이 가능
- 연결된 Module을 이용하여 의존성 객체를 생성하고, Inject로 요청받은 인스턴스에 생성한 객체를 전달(주입)
- 의존성을 요청받고 전달하는 Dagger의 주된 역할을 수행
SubComponent
- 부모 Component가 있는 자식 Component
- Component와 달리 코드 생성은 부모 Component에서 이루어짐
- Component와 마찬가지로 interface 또는 abstract class에서 사용
Scope
- Component별로 @Scope annotation으로 주입되는 객체들을 관리
- 생성된 객체의 Lifecycle 범위
참조
Hilt
- Dagger를 기반으로 만든 DI 라이브러리
- 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써 애플리케이션에서 DI를 사용하는 표준 방법을 제공
- Dagger2보다 표준화된 사용법을 제시하고 보일러 플레이트 코드를 감소 시킴
참조
Koin
- 안드로이드에서 사용할 수 있는 코틀린 DI 라이브러리
장점
- 러닝커브가 낮아 쉽고 빠르게 DI를 적용할 수 있음
- Kotlin 개발 환경에 도입하기 쉬움
- 별도의 어노테이션을 사용하지 않기 때문에 컴파일 시간이 단축
- ViewModel 주입을 쉽게 할 수 있는 별도의 라이브러리를 제공
사용법
build.gradle 설정
dependencies {
implementation("io.insert-koin:koin-core:3.1.2")
implementation("io.insert-koin:koin-android:3.1.2")
}
샘플 클래스 생성
class SampleRepository() {
val sampleData = "Sample Data!"
}
class SampleController(val repository: SampleRepository) {
fun printSampleData() {
Log.d("Print sample data", repository.sampleData)
}
}
class SampleViewModel : ViewModel() {
private var _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
}
모듈 선언
module
로 주입 받고자 하는 객체를 모듈로 만들어 선언
val appModule = module {
single { SampleRepository() }
factory { SampleController(get()) }
}
val viewModelModule = module {
viewModel { SampleViewModel() }
}
single
: 컨테이너 내에서 단 한번만 생성됨
factory
: 컨테이너 내에서 요청 시점마다 새로운 객체를 생성
get()
: 컨테이너 내에서 알맞은 의존성을 주입 (위 코드에서는 SampleRepository가 주입됨)
viewModel
: 컨테이너 내에 해당 viewModel을 등록(ViewModelFactory 안에 Koin이 알아서 등록해줌)
컨테이너 생성 (모듈 등록)
- Application 클래스의 onCreate LifeCycle에서
startKoin
을 호출하고 위에서 선언한 모듈 변수를 넘겨줌
- Application의 생명주기에 맞게 생성되고 삭제됨
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@SampleApplication)
modules(appModule)
modules(viewModelModule)
}
}
}
Kodein