경험의 기록

0️⃣ 의존성 주입(DI) 이란?

 

먼저, 의존성이란 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에게 인스턴스만 전달해주면 되기 때문에 서브클래스를 사용할 수 있다. 또한 테스트를 하는데 용이하다.

 

 

 

의존성 주입 방법

  • 생성자 삽입 : 위에서 설명한 방법이다. 즉, 클래스의 종속 항목을 생성자에 전달한다.
  • 필드 삽입(또는 setter 삽입) : 활동 및 프래그먼트와 같은 특정 Android 프레임워크 클래스는 시스템에서 인스턴스화하므로 생성자 삽입이 불가능하다. 따라서 대신 필드 삽입을 사용하면 종속 항목은 클래스가 생성된 후 인스턴스화된다.

필드 삽입 예제

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 를 다룬다.

 

 

 

1️⃣ Dagger Hilt 사용하기

Hilt?

자동 의존성 주입 라이브러리로 기존의 Dagger 라이브러리를 기반으로 하여 더 용이하고 편리하게 제작된 라이브러리

 

Gradle 설정

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 버전을 낮춰본다.

Hilt Application

@HiltAndroidApp
class MyApplication : Application() {

}

Hilt를 사용하는 모든 앱은 @HiltAndroidApp 어노테이션 지정된 Application 클래스를 포함해야 한다.
@HiltAndroidApp은 애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 비롯하여 Hilt의 코드 생성을 트리거한다.

 

Component hierachy

컴포넌트의 계층구조를 이해해야 한다.

Hilt에서는 Dagger2 와 달리 Android 환경에서 표준적으로 사용되는 컴포넌트들을 기본적으로 제공한다.

위는 컴포넌트의 계층구조를 나타낸 그림이다.

각 컴포넌트들은 생성 시점부터 파괴되기 이전까지 member injection이 가능하다. 각 컴포넌트는 자신만의 lifetime 를 가진다.

 

 

각 컴포넌트들의 생성위치와 제거위치를 나타낸 표이다.

2.28.1이상 버전에서는 ApplicationComponentSingletonComponent 로 대체 되었다.

개발자 문서에서 제공하는 자료에서는 아직 업데이트가 되지 않은듯 하다.

 

 

Hilt Modules

@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)

하나의 모듈을 여러 컴포넌트에도 설치할 수 있다.

 

하지만 세가지 규칙이 존재한다.

  • Provider다중 component가 모두 동일한 scope에 속해있을 경우에만 scope를 지정할 수 있다. 위에서 봤던 계층구조 그림처럼 ViewComponent와 ViewWithFragmentComponent는 동일한 ViewScoped에 속해있기 때문에, provider에게 동일한 ViewScoped를 지정할 수 있다.
  • Provider는 다중 component가 서로 간 요소에게 접근이 가능한 경우에만 주입이 가능하다. ViewComponent와 ViewWithFragmentComponent는 서로 간의 요소에 접근이 가능하기 때문에 View에게 주입이 가능하지만, FragmentComponent  ServiceComponent  Fragment 또는 Service에게 주입이 불가능하다.
  • 부모 component와 자식 compoent에 동시에 install 될 수 없으며, 자식 component는 부모 component의 module에 대한 접근 할 수 있다.

즉, 상속의 관계를 생각해보면 쉽게 이해할 수 있다.

 

AndroidEntryPoint

객체가 주입되는 대상이라고 보면 된다.

@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 로 사용하면 된다.

 

잘 출력되는 것을 확인할 수 있다.

 

 

 

 

2️⃣ ViewModel 에서 사용하기

Gradle 설정

// 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

 

HanYeop/AndroidStudio-Practice2

(2021.05.20~) 안드로이드 학습 내용 저장소. Contribute to HanYeop/AndroidStudio-Practice2 development by creating an account on GitHub.

github.com

 

 

 

 

 

 

 

 

 

 

 

참고

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

 

 

 

 

 

 

반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading