[Android] DI는 뭐고 Hilt는 뭔데요!!

moKo·2022년 6월 21일
0

Android

목록 보기
11/13
post-thumbnail

DI (Dependency Injection)

DIDependency Injection의 약자로 있는 그대로 해석해본다면 의존성 주입이다.
프로그래밍에서의 의존성이라 함은 "함수에 필요한 클래스 또는 참조변수나 객체에 의존하는 것"이라고 할 수 있다.

쉽게 말해, A클래스가 객체를 만들기 위해 B를 필요로 하는 것이라고 볼 수 있다.
그럼 B는 A의 의존대상이 되는 것이다.

많은 설명해놓은 글들을 보면 자동차를 대상으로 설명을 해놓으셨는데, 인용해보자면
자동차를 만드는 회사는 타이어를 직접 생산하지 않으니, 타이어 회사에 의존하게 되는 것이고, 타이어 회사가 자동차 회사의 의존대상이 된다 정도로 히해할 수 있겠다.

그럼, 의존성 주입이라는 말은 뭘까?
이것도 예시를 들어서 이해해보자.

밑의 예시는 의존성 주입을 하지 않은 상태의 코드이다.

class Laptop {

    private val cpu = Cpu()

    fun start() {
        cpu.start()
    }
}

fun main(args: Array) {
    val laptop = Laptop()
    laptop.start()
}

이 예제에서는 laptop과 cpu의 결합이 강하다. laptop은 cpu의 자식 클래스를 사용하기가 쉽지 않아진다.

다음 예제는 의존성 주입을 한 상태의 코드이다.

class Laptop(private val cpu: Cpu) {
    fun start() {
        cpu.start()
    }
}

fun main(args: Array) {
    val cpu = Cpu()
    val laptop = Laptop(cpu)
    laptop.start()
}

이런 식으로 작성을 하면, 결합이 낮아지고 Laptop을 재사용하기도, 테스트하기도 쉬워진다.

그래서 DI를 왜써요 복잡하기만 한데 🤔

맞다. DI는 라이브러리를 사용하더라도 어렵고 복잡해서 처음 사용할 때는 많이 헤맬수 있다. 하지만 귀찮고 비효율적인 것을 제일 싫어하는 개발자들이 사용하는데에는 이유가 있지 않을까?

장점으론 여러가지가 있는데 한번 알아보자.

  1. Unit Test가 용이하다.
  2. 코드의 재사용성이 높아진다.
  3. 리팩토링이 수월하다.
  4. 객체 간의 의존성을 줄이거나 없앨 수 있다.
  5. 객차 간의 결합도를 낮추며 유연한 코드를 짤 수 있다.

딱 봐도 유지보수가 용이해서 개발자들이 많이들 사용하는 것 같다.
그리고, 장점들을 보다보면 어디서 많이본 특징들과 비슷하게 느껴진다.

바로 MVVM이 떠오른다. MVVM에 대한 설명은 다음에 포스팅하겠지만
이 MVVM이라는 디자인 패턴도 결합도를 낮추고 Test를 용이하게 하고 유지보수를 편하게 해주는 패턴이다.

이렇게 좋지만 단점도 존재한다.

  1. 의존성 주입을 위한 선행작업이 필요하다.
  2. 코드를 추적하고 읽기가 어려워진다.

그래도 장점이 더 많고, 유지보수가 용이하여 퇴근시간을 당기는데 도움을 줄 수 있음으로 굳이 안쓸이유가 없다..!

자 그럼 다시 Hilt를 알아보러 가보자

Hilt

Hilt 란 Google의 Dagger 를 기반으로 만든 DI 라이브러리이다.

사실 DI 라이브러리는 Hilt만 있는 것이 아니다.
기존에 많이 사용되던 Koin도 존재한다.

하지만 요즘은 KoinHilt로 마이그레이션하는 추세이다.
왜일까?

Koin은 확실히 쉽다. 그래서 처음에 사용하기가 어렵지 않다.
그리고 kotlin 개발환경에 적용하기 쉽게 되어있다.
별도의 어노테이션을 사용하지 않기 때문에, 컴파일 시간도 단축되고 viewModel 주입을 쉽게 할 수 있는 라이브러리도 제공한다.

이렇게만 보면 좋은데 왜 점점 안쓰게 되는 걸까?

런타임에 서비스 로케이팅을 통해 인스턴스를 동적으로 주입해주기 때문에 런타임 퍼포먼스가 떨어지고, 리플렉션을 이용하기 때문에 성능 상 좋지 않다.

반면에 HiltKoin에 비해 확실히 어렵다.

컴파일 시에 stub 파일을 생성하고 그래프를 구성하기 때문에 빌드 시간도 Koin에 비해 상대적으로 느리다.

하지만 AnroidX 라이브러리와 호환되며, Android Component별 Scope가 명확하다.

이런 이유들이 요즘 Hilt를 사용하는 이유인 듯 하다.

Hilt 컴포넌트 계층

Hilt에서는 Android환경에서 표준적으로 사용되는 컴포넌트들을 기본적으로 제공합니다. 또한 내부적으로 제공하는 컴포넌트들의 전반적인 라이프 사이클 또한 자동으로 관리해주기 때문에 사용자가 초기 DI환경을 구축하는데 드는 비용을 최소화합니다.

다음 사진은 Hilt에서 제공하는 표준 component hierarchy입니다.

Hilt에서 표준적으로 제공하는 Component 관련 Scope, 생성 및 파괴시점 입니다.

Hilt Dependency

dependencies {
  implementation 'com.google.dagger:hilt-android:2.42'
  annotationProcessor 'com.google.dagger:hilt-compiler:2.42'
}

Hilt Annotation

Hilt는 어노테이션을 통해 적용할 수 있다.

@HiltAndroidApp

Hilt를 사용하는 모든 앱은 application 클래스에 @HiltAndoidApp 이라는 어노테이션을 적용해야 한다. @HiltAndroidApp은 적용된 application 클래스를 포함한 Hilt의 코드 생성을 트리거 시킨다.

@HiltAndroidApp
class ExampleApplication : Application() { ... }

@AndroidEntryPoint

Activity, Fragment, View, Service, BroadcastReceiver 같은 Android Component에 사용할 수 있는 어노테이션으로, 이를 적용한 컴포넌트 내에서 @Inject가 달린 필드에 의존성 주입을 한다. Application 클래스에 대해서는 @HiltAndroidApp이 대신하고 있다.

만약 Fragment에 @AndroidEntryPoint를 지정했을 경우 그 Fragment를 사용하는 Activity에도 @AndroidEntryPoint를 지정해야한다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

@EntryPoint

위에서 @AndroidEntryPoint에서 커버할 수 있는 컴포넌트외에서 의존성 주입이 필요한 경우가 있는데 (ContentProvider 등) 이때 @EntryPoint를 이용할 수 있다.

@EntryPoint
@InstallIn(SingletonComponent::class)
interface FooBarInterface {
  @Foo fun getBar(): Bar
}

이렇게 정의한 EntryPoint에 접근하기 위해서 EntryPoints 클래스를 이용해야한다.

var bar = EntryPointAccessors.fromApplication(context, FooBarInterface::class.java)

@Module

인터페이스나, 빌더 패턴을 사용한 경우, 외부 라이브러리 클래스 등등 생성자를 사용할 수 없는 Class를 주입해야 할 경우 @Module에다 객체 생성방법을 정의해서 @Inject가 가능하도록 만들 수 있다.

@InstallIn

@Module이나 @EntryPoint 어노테이션과 함께 사용해야하며, @InstallIn을 이용해서 모듈을 어느 컴포넌트에 설치할지 정할 수 있다.

아래 예제는 ActivityComponent에 설치하는 Analytics Module의 예시코드다.

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

@Binds

위 예시코드에 @Binds가 들어간김에 이것도 같이 얘기할텐데...인터페이스의 구현체를 제공해야할 때 사용할 수 있다. Repository 인터페이스를 구현하고 이를 따르는 구현체 RepositoryImpl를 많이 사용하는데 그 패턴맞다.

모듈에 정의한 후에는 아래 코드처럼 구현체의 생성자에서 @Inject를 시켜주면 된다.

class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }
 

@Provides

Retrofit이나 Room DB와 같이 빌더패턴을 사용해서 인스턴스를 생성해야할때 @Provides를 사용하면 된다.

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    // Potential dependencies of this type
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

내용 및 사진 출처, 이해를 도와준 분들 🙏 ( 클릭 시 이동됩니다 )

profile
🔥 Feelings fade, results remain

0개의 댓글