개발/Android

DI(종속 항목 삽입)

이도일 2022. 5. 12. 17:21

MVVM을 언제까지 외워서만 할 순 없다.....

DI가 신기술은 아니고, 그래서 기술 리뷰로 들어가서도 안 되는 것 같지만

일단 여기 말곤 쓸 데가 없으니 메모한다.

 

 


DI (종속 항목 삽입) / (의존성 주입) : 외부에서 의존 객체를 생성하여 넘기는 것

1. 의존성 파라미터를 생성자에 작성하지 않아도 되므로, 플레이트 코드를 줄일 수 있음.
2. Interface에 구현체를 쉽게 교체하면서 적절한 행동의 정의가 가능. 테스트 유용해짐.

 

 

 

 

대충 이래서 쓴단다. 근데 뭔 말인지...잘 모르겠다.

그래서 예시를 보기로 했다.

 

아래는 종속 항목 삽입이 되지 않아 문제가 발생할 가능성이 있는 코드이다.

 

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

 

이렇게 사용하게 되면

Car와 Engine클래스가 밀접하게 연결된 상태가 된다. Car인스턴스는 한 가지 유형의 Engine을 사용하므로, 

서브클래스 또는 대체 구현을 사용할 수 없게된다.

Car가 자체 Engine을 구성했다면, 엔진에 동일한 Car클래스를 사용할 수 없고,

두 가지 유형의 Car를 생성해야한다.

 

종속 항목 삽입을 이용하면 다음과 같은 경우를 줄일 수 있다.

 

+ 하나의 예시를 더 추가한다.

예를 들어 샌드위치를 만들 때, 이 샌드위치 클래스가 DI가 잘 구축되어있다면, 

빵과 토핑(삽입되는 객체들)을 여러 종류로 바꾸는것만으로 다양한 샌드위치 객체를 만들 수 있다!

 

종속 항목 삽입은 아래의 두 가지 방법으로 구현이 가능하다!

 

1. 생성자 삽입 : 클래스의 종속 항목을 생성자에 전달하는 방법

2. 필드 삽입( setter삽입) : 클래스가 생성된 후 인스턴스화 되도록 만드는 방법

 

아래는 1의 경우이다.

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

 

아래는 2의 경우이다.

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

 

매번 저런식으로 코드를 짜기는 어려울 수도 있다.

그래서 늘 안드로이드는 해답을 제공한다~!~!~!~!~!

 

 

아래는 자동 종속 항목 삽입을 위한 라이브러리이다.

 

 

1. DAGGER

구글에서 유지 관리하는 라이브러리이다. 자바, 코틀린, + 안드로이드용으로 사용된다.

DAGGER의 필수 개념은 얘네다. Inject / Component / Subcomponent / Module / Scope

 

Inject

- 의존성 주입을 요청한다. 요 어노테이션으로 주입을 요청하면, 연결된 컴포넨트가 모듈로부터 객체를 생성해 넘겨준다.

class CoffeeMaker @Inject constructor(
    private val heater: Heater,
    private val pump: Pump
) {
    fun brew() {
        heater.on()
        pump.pump()
        Log.d("coffeMaker", "[_]P coffee! [_]P")
        heater.off()
    }
}

 

Component 

- 연결된 모듈을 이용해 의존성 객체를 생성하고, 인젝트로 요청받은 인스턴스에 생성한 객체를 주입한다. Dagger의 주된 역할.

@Component(modules = [CoffeeMakerModule::class])
interface CoffeeComponent {

    // provision method 유형
    fun make() : CoffeeMaker

    // member-injection method 유형
    fun inject(coffeeMaker: CoffeeMaker)
}

- Component 인스턴스를 구현하는 방법은 create() / build()가 있다.

DaggerCoffeeComponent.create().make().brew() / 이렇게 쓰거나

DaggerCoffeeComponent.builder().build().make().brew() / 이렇ㄱ ㅔ쓰면 된다

 

Subcomponent

- 컴포넌트는 계층관계를 만들 수 있다. Subcomponent는 Inner Class방식의 하위 컴포넌트이다. 즉 서브의서브의서브도 가능.

  이 친구는 그래프를 형성한다. 인젝트로 주입을 요청받으면 서브컴포넌트에서 먼저 의존성을 검색하고, 부모까지 올라가며 검색한다.

 

Module

- 컴포넌트에 연결되어 의존성 객체를 생성한다. 생성 후 스코프에 따라 관리도 한다.

@Module
class CoffeeMakerModule {
    @Provides
    fun provideHeater() : Heater = A_Heater()

    @Provides
    fun providePump(heater: Heater) : Pump = A_Pump(heater)
}

 

Scope

- 객체의 Lifecycle 범위이다. 안드로이드는 주로 PerActivity, PerFragment등으로 화면의 생명주기와 맞춰 사용한다.

  모듈에서 스코프를 보고 객체를 관리한다.

 

DAGGER의 주요 키워드는 '그래프'이다.(자료구조의 그 그래프..!) 의존성이 요청되면 서브 컴포넌트 -  컴포넌트 - 인젝트 생성자 순으로 검색하여 주입한다. 

 

예제 코드를 짜려고 했는데...............................................

그냥 Dagger보다 안드로이드에서는 후속인 Dagger Hilt를 쓰란다.

그래서 대충 이까지만 설명함...흑흑

 

 

2. DAGGER HILT

구글에서 유지 관리하는 라이브러리이다. 안드로이드용으로 사용된다.(안드로이드에 최적화되어있다)

기존의 Dagger보다 러닝커브가 낮고, 초기 DI 환경 구축 비용을 크게 절감할 수 있다. Dagger기반이므로 요소 설명은 건너뛴다.

 

Hilt에서는 다음과 같은 클래스를 지원한다

- Activity

- Fragment

- View

- Service

- BroadcastReceiver

(다 하노...?)

 

 

 

1. 사전 준비

 

암튼 힐트를 사용하기 위해서 일단 gradle 셋업부터 확인하자.

buildscript {
    ...
    ext.hilt_version = '2.33-beta'
    dependencies {
        ...
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-compiler:$hilt_version"
}

이렇게 설정하면 된다. 

현재 공식문서에 나와있는건 2.33-beta가 아니라 2.28-alpha니까 참고하자.

 

2. Hilt Application Class

 

힐트를 사용하려면 @HiltAndroidApp 어노테이션을 Application Class에 추가해야한다. 

이건 의존성 주입의 시작점을 지정하는 어노테이션이다. 

 

@HiltAndroidApp
class MainApplication : Application()

이렇게.........써주면 됩니다.............

 

3. Android Entry Point

 

객체를 주입할 Android 클래스에 요 어노테이션 추가하면,

자동적으로 생명주기에 따라 적절한~시점에 hilt요소로 인스턴스화되어 처리된다! 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

    }

}

요렇게 써주면 된다~

 

4. Inject constructor

 

constructor()에 @Inject 어노테이션을 추가하고, 이걸로 의존성 인스턴스를 생성하면 된다.

class MainViewModel @Inject constructor(private val repository: MainRepository): ViewModel {

 

5. Hilt 모듈

 

외부 라이브러리를 사용하는 경우(레트로핏 등),는 개발자가 생성자를 만들거나 삽입이 불가능하다.

이럴때는 요 힐트 모듈이 의존성을 생성할 수 있도록 돕는다.

여기서 @InstallIn(component)어노테이션은 어떤 컴포넌트에 install 할 지 정해주는 역할을 한다.

 

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
   ...
}

 

Hilt Component / 생명주기 / Scope

 

Hilt component Injector for Created at Destroyed at Scope
SingletonComponent Application Application#onCreate() Application#onDestroy() @Singleton
ActivityRetainedComponent 해당 없음 Activity#onCreate() Activity#onDestroy() @ActivityRetainedScoped
ViewModelComponent ViewModel ViewModel created ViewModel destroyed @ViewModelScoped
ActivityComponent Activity Activity#onCreate() Activity#onDestroy() @ActivityScoped
FragmentComponent Fragment Fragment#onAttach() Fragment#onDestroy() @FragmentScoped
ViewComponent View View#super() View destroyed @ViewScoped
ViewWithFragmentComponent @WithFragmentBindings 가 붙은 View View#super() View destroyed @ViewScoped
ServiceComponent Service Service#onCreate() Service#onDestroy() @ServiceScoped

 

각 컴포넌트들은 생성 시점부터 ~ 파괴되기 전까지 Injection이 가능하고, 각 컴포넌트마다 자신만의 생명주기를 가짐

 

- SingletonComponent : Application의 생명주기를 가짐. Application이 생성되는 시점에 함께 생성되고, 파괴되는 시점에 함께 파괴됨.

- ActivityRetainedComponent : Activity의 생명주기를 가짐. Activity의 Configuration Change(디바이스 화면전환 등) 시에는 유지됨.

- ViewModelComponent : Jetpack ViewModel의 생명주기를 가짐. 

- ActivitComponent : Activity의 생명주기를 가짐. Activity가 생성되는 시점에 함께 생성되고, 파괴되는 시점에 함께 파괴됨.

- FragmentComponent : Fragment의 생명주기를 가짐. Fragment가 Activity에 붙는 시점에 함께 생성되고, 파괴되는 시점에 함께 파괴됨.

- ViewComponent : View의 생명주기를 가짐. View가 생성되는 시점에 함께 생성되고, 파괴되는 시점에 함께 파괴됨.

- ViewWithFragmentComponent : Fragment의 View 생명주기를 가짐. View 생성 시점에 함께 생성되고, 파괴되는 시점에 함께 파괴됨.

- ServiceComponent : Service의 생명주기를 가짐. Service가 생성되는 시점에 함께 생성되고, 파괴되는 시점에 함께 파괴됨.

 

provides annotation 을 이용해 이렇게 써주면 됩니다

 

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    private const val BASE_URL = "YOUR_BASE_URL"

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(AuthInterceptor())
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
                .client(okHttpClient)
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
    }

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}
class RemoteDataSource @Inject constructor(private val apiService: ApiService) {

    suspend fun getCarSearchInfo(
        query: String,
        sort: String? = "accuracy",
        page: Int? = 1,
        size: Int? = 80
    ): Response<CarSearchInfo> =
        apiService.getCarSearchInfo(query, sort, page, size)
}

 

6. Hilt 한정자

 

Hilt에서는 미리 정의 된 한정자를 제공해준다.

예를들어, Context가 필요한 경우에 간편하게 사용할 수 있도록 아래와 같은 한정자를 제공해준다.

 

- @ApplicationContext

- @ActivityContext

 

@Module
@InstallIn(SingletonComponent::class)
object SomethingModule {

    @Provides
    @Singleton
    fun provideSomething(@ApplicationContext context: Context): Something {
        //context 사용
        ...
    }
}

 

7. Hilt + Jetpack

 

hilt는 MVVM에 적용시키려고 공부중이니까....jetpack의 viewmodel과 함께 사용하는 법을 알아두는게 좋을 것 같다.

 

dependencies {
  ...
  implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
  // When using Kotlin.
  kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
  // When using Java.
  annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
  
  implementation 'androidx.fragment:fragment-ktx:1.3.0'
}

defendency 추가하고,

 

ViewModel 에서 @HiltViewModel 어노테이션과 @Inject 어노테이션을 사용해 ViewModel 의존성 주입을 활성화한다.

 

@HiltViewModel
class MainViewModel @Inject constructor(
    private val repository: MainRepository,
    private val savedStateHandle: SavedStateHandle
    ) : ViewModel() {
    ...
}

 

생성자 파라미터로 MainRepository를 주입받을수 있고, SavedStateHandle 정보 또한 간편하게 주입 받을 수 있다.

다음은 위에서 생성된 MainViewModel을 @AndroidEntryPoint가 붙은 Activity에서 사용하는 예시다.

 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
    }
}

 

끝.