먼저, 의존성이란 A 클래스가 자체적인 B 클래스를 구성하는 것을 말한다.
구글의 예시를 통해 알아보자.
그림과 같이 Car라는 클래스가 Engine 라는 클래스를 가져다 쓰고 있는 것이 의존성이다.
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
예시 코드로 살펴 보면, Car 클래스는 내부에서 Engine 클래스를 만들어 사용하고 있다.
이렇게 사용할 경우 서브 클래스를 구현하기 쉽지 않고, 테스트를 어렵게 만든다.
만약 Car 클래스에서 사용하기 위한 Engine의 서브클래스인 ElectricEngine 를 생성하여 사용하려고 한다면 Car 클래스를 재사용하는 것이 아닌 ElectricEngine 를 사용하는 또 다른 Car 클래스를 따로 만들어야 할 것이다.
그래서 위와 같이 외부에서 정해준 Engine을 Car에 삽입해주는 것이 의존성 주입 (DI) 이다.
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
예시 코드로 살펴보면, 메인에서 엔진이 무엇인지 정해주어 Car의 엔진에 할당해주는 것을 알 수 있다.
이렇게 구현할 경우 아까와 같이 ElectricEngine 를 따로 생성한다 해도 Car에게 인스턴스만 전달해주면 되기 때문에 서브클래스를 사용할 수 있다. 또한 테스트를 하는데 용이하다.
필드 삽입 예제
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
❗ 하지만 코드가 복잡해질수록 수동으로 의존성 주입하는 데에는 어려움이 따른다. 그래서 등장한 것이 Dagger, Hilt, Koin 등의 DI 라이브러리 이다.
이 글에선 여러 라이브러리 중에 Hilt 를 다룬다.
자동 의존성 주입 라이브러리로 기존의 Dagger 라이브러리를 기반으로 하여 더 용이하고 편리하게 제작된 라이브러리
plugins {
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
// Dagger Hilt
implementation "com.google.dagger:hilt-android:2.31.2-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.31.2-alpha"
}
모듈 단위의 그래들에 추가해주고
dependencies {
// Dagger Hilt
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.31.2-alpha'
}
프로젝트 그래들에 추가해준다.
만약 추가 시 오류가 발생한다면
gradle 버전을 낮춰본다.
@HiltAndroidApp
class MyApplication : Application() {
}
Hilt를 사용하는 모든 앱은 @HiltAndroidApp 어노테이션 지정된 Application 클래스를 포함해야 한다.
@HiltAndroidApp은 애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 비롯하여 Hilt의 코드 생성을 트리거한다.
컴포넌트의 계층구조를 이해해야 한다.
Hilt에서는 Dagger2 와 달리 Android 환경에서 표준적으로 사용되는 컴포넌트들을 기본적으로 제공한다.
위는 컴포넌트의 계층구조를 나타낸 그림이다.
각 컴포넌트들은 생성 시점부터 파괴되기 이전까지 member injection이 가능하다. 각 컴포넌트는 자신만의 lifetime 를 가진다.
각 컴포넌트들의 생성위치와 제거위치를 나타낸 표이다.
❗ 2.28.1이상 버전에서는 ApplicationComponent가 SingletonComponent 로 대체 되었다.
개발자 문서에서 제공하는 자료에서는 아직 업데이트가 되지 않은듯 하다.
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Singleton
@Provides
fun provideTestString() = "테스트 문자가 주입되었습니다."
}
@InstallIn 어노테이션 은 Hilt의 표준 컴포넌트들 중 어떤 컴포넌트에 모듈을 설치할지를 결정한다.
위의 코드에서는 SingletonComponent 에 모듈을 설치하였다.
@Provides 어노테이션으로 컴포넌트에 제공할 메소드를 정의할 수 있으며, 활동범위를 지정해주는데,
@Singleton 어노테이션으로 싱글톤 패턴으로 생성하여 사용할 때마다 인스턴스가 생성되는 것을 막을 수 있다.
@Module
@InstallIn(ActivityComponent::class)
object AppModule {
@ActivityScoped
@Provides
fun provideTestString() = "테스트 문자가 주입되었습니다."
}
만약 ActivityComponent 에 모듈을 설치한다면
@ActivityScoped 로 지정해야한다.
즉 자신과 관련된 활동범위로 지정되야 한다.
@InstallIn(ViewComponent::class, ViewWithFragmentComponent::class)
하나의 모듈을 여러 컴포넌트에도 설치할 수 있다.
하지만 세가지 규칙이 존재한다.
즉, 상속의 관계를 생각해보면 쉽게 이해할 수 있다.
객체가 주입되는 대상이라고 보면 된다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val TAG = "Test5"
@Inject
lateinit var test : String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(TAG, "출력 : $test ")
}
}
위의 코드에서
@AndroidEntryPoint 를 사용하여 객체주입 대상으로 선언해주고,
String 변수에 @Inject 를 사용하여 주입해준다.
여기에서 모든 모듈을 검색하여 String 문자열을 자동으로 찾아준다.
여기서 Inject 대상이 lateinit을 사용할 수 없는 자료형이라면
@set:Inject = 초기값 형태로 사용한다.
앱을 실행해보면,
로그에 잘 출력되는 것을 확인할 수 있다.
만약 실행 시 오류가 발생한다면, Kotlin 1.5.10 버전 이상을 사용하고 있을 경우
코틀린 버전을 적당히 낮춰준다.
❓ 모듈에 존재하는 String 문자열이 2개라면 어떻게 될까?
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Singleton
@Provides
fun provideTestString() = "테스트 문자가 주입되었습니다."
@Singleton
@Provides
fun provideTestString2() = "2번째 테스트 문자가 주입되었습니다."
}
위와같이 String 문자열이 2개 존재할 경우
빌드 시 오류가 발생하게 된다.
이 경우에는 Named 어노테이션을 사용하여 해결할 수 있다.
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Singleton
@Provides
@Named("String1")
fun provideTestString() = "테스트 문자가 주입되었습니다."
@Singleton
@Provides
@Named("String2")
fun provideTestString2() = "2번째 테스트 문자가 주입되었습니다."
}
각각의 이름을 지정해준다고 생각하면 된다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val TAG = "Test5"
@Inject
@Named("String1")
lateinit var test : String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(TAG, "출력 : $test")
}
}
사용할 때에도 동일하게 사용하고 싶은 문자열을 입력해주면
잘 주입되는 것을 확인할 수 있다.
아까의 예시에서, String2 가 String1을 필요로 하는 경우도 있을 수 있다.
또한, value에 저장해놓은 String 값을 사용하고 싶을 수도 있다.
<resources>
<string name="string_string2">2번째 테스트 문자가 주입되었습니다.</string>
</resources>
string 에 저장되어 있는 값을
@Singleton
@Provides
@Named("String2")
fun provideTestString2(
@ApplicationContext context : Context
) = context.getString(R.string.string_string2)
context의 getString 메소드를 이용해 불러올 수 있는데,
여기서 모듈을 context를 알 수 없으므로 @ApplicationContext 로 컨텍스트를 불러와서 지정해 줄 수 있다.
@Singleton
@Provides
@Named("String2")
fun provideTestString2(
@ApplicationContext context : Context,
@Named("String1") string1 : String
) = "${context.getString(R.string.string_string2)} 그리고, $string1"
또한 다른 의존성이 필요한 경우
액티비티에서 사용할 때 처럼 @Named 로 사용하면 된다.
잘 출력되는 것을 확인할 수 있다.
// ViewModel Hilt
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
// Activity Ktx
implementation "androidx.activity:activity-ktx:1.1.0"
추가해준다.
class MyViewModel @ViewModelInject constructor(
@Named("String2") string2 : String
) : ViewModel(){
init{
Log.d("test5", "$string2")
}
}
아까 만든 String2 를 @ViewModelInject 을 사용하여 생성자로 넘겨준다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel : MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel
}
}
뷰모델을
Acitivity-ktx의 기능인 by viewModels() 로 lazy 형태로 선언해준다.
선언된 viewModel은 사용되기 전까지 생성되지 않는 lazy 이므로
onCreate에서 선언만 해준다.
잘 출력되는 것을 확인할 수 있다.
https://github.com/HanYeop/AndroidStudio-Practice2/tree/master/DaggerHiltEx
참고
https://developer.android.com/training/dependency-injection?hl=ko
https://developer.android.com/training/dependency-injection/hilt-android?hl=ko
https://hyperconnect.github.io/2020/07/28/android-dagger-hilt.html