[Kotlin] Android DI와 Hilt

mi-fasol·2023년 11월 27일
1

Kotlin

목록 보기
5/5

전에 스프링을 공부할 때 DI에 대해 작성한 적이 있었는데, 이번엔 안드로이드의 DI에 대한 이야기를 하려 한다.

DI

DI는 Dependency Injection의 약자로 의존성 주입을 뜻한다.
특정 한 객체가 다른 객체를 필요로 할 때 이 의존성을 제공하는 기술이 DI다.

객체가 다른 객체를 필요로 하면 외부에서 해당하는 객체를 생성하여 필요한 객체에 넘겨주게 된다.

DI는 아래와 같은 장점이 있다.

  1. 코드 재사용성 향상
    • 결합도 감소
  2. 테스트 용이성
    • 의존성을 가짜 객체나 Mock 객체로 대체하여 테스트 수행 가능
  3. 코드의 유연성 및 확장성, 가독성 향상
    • 새로운 기능을 추가하거나, 기능을 수정할 때 미치는 영향 감소
  4. 의존성 추적 용이성
    • 클래스가 사용하는 종속성의 명확한 파악 가능

Android Developers에 있는 공식 코드를 활용해 이야기를 해 보겠다.

두 개의 코드가 있는데, 둘의 차이점은 무엇일까?

첫 번째 코드

class Car {

	// 여기
    private val engine = Engine()

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

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

두 번째 코드

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

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

얼핏 보기에는 비슷해 보이지만 위의 코드는 DI를 사용하지 않은 코드고, 두 번째 코드는 DI를 사용해 의존성을 주입한 예제 코드다.

첫 번째 코드의 주석 부분을 보면 Car 클래스가 자체적으로 Engine을 생성해 사용하고 있다.
이 코드는 둘 간의 의존성이 높아 결합도가 높아지고, Car 클래스가 Engine을 직접 인스턴스화 해서 Engine의 서브 클래스들을 사용할 수 없다.

반면 두 번째 코드는 Main 코드에서 Engine 인스턴스를 생성해 Car 클래스에 파라미터로 넘겨주고 있다.
그렇기에 Car 클래스의 재사용성이 높아지고, Engine의 구현이 수정돼도 Car 클래스에는 영향을 미치지 않는다.

안드로이드에서 의존성을 주입해 주는 방법은 두 가지가 있다.

  • 생성자 삽입 (Constructor Injection)
  • 필드 삽입 (Field Injection)

생성자 삽입

생성자 삽입은 위의 두 번째 코드에서 설명한 방법이다.

클래스의 종속 항목을 생성자에 전달한다.
코드는 앞서 말했으므로 생략하고 넘어가겠다.

필드 삽입

Activity, Fragment 등 특정 안드로이드 프레임워크 클래스에서는 생성자 삽입이 불가한 경우가 있다.
이럴 때 필드 삽입을 통해 클래스가 생성된 후 종속 항목을 인스턴스화 해주는 방법이 있다.

class Car {
    lateinit var engine: Engine

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

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

위처럼 Car 클래스를 먼저 생성해주고, 그 후에 Engine을 인스턴스화 해주면 된다.

여기서 연계해서 설명할 개념이 하나 생긴다.

바로 Hilt다.

Hilt

Hilt란 Dagger를 기반으로 의존성을 주입하고, 수명 주기를 자동으로 관리하기 위한 안드로이드의 DI 라이브러리다.

라이브러리인만큼 build.gradle 파일에 플러그인을 추가해 줘야 한다.
또한, Java 8 기능을 사용하므로 프로젝트 내에서 Java 8을 사용하고자 한다면 아래와 같은 코드를 app/build.gradle에 추가해야 한다.

android {
  ...
  compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
  }
}

Hilt는 아래와 같은 특징이 있다.

  1. AndroidX와 통합
    • 안드로이드 앱의 생명주기와 관련된 의존성 주입 지원
  2. 안드로이드 컴포넌트와 자동 통합
    • Application, Activity, Fragment, ViewModel과 자동으로 통합
  3. Daager의 간소화
    • Dagger의 복잡성을 간소화 하고 Dagger 컴포넌트 및 모듈의 생성, 관리를 자동화
  4. 코드 간소화 및 테스트 용이성
    • DI 관련 코드를 간소화 하고, 테스트에서 필요한 의존성을 쉽게 대체 가능

이 Hilt를 사용하기 위해서는, @HiltAndroidApp 어노테이션을 사용해야 한다.

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

매니페스트에도 아래와 같이 추가해야 한다.

 <application android:name=".ExampleApplication"

Hilt로 의존성을 주입하고 나면, @Injection 어노테이션을 사용하여 가져올 수 있다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

@Binds 어노테이션도 있다.

@Binds 어노테이션은 인터페이스에 대해 종속성을 삽입할 때 사용하는 어노테이션이다.

예를 들어 한 인터페이스를 두 개의 레포지토리가 상속받는 경우, Hilt는 어느 레포지토리를 선택해야 할지 혼란을 겪게 된다. 이럴 때 @Binds로 상속받은 인터페이스를 구분지을 수 있게 된다.

@Module
@InstallIn(ApplicationComponent.class)
public abstract class MyModule {

    @Binds
    @Singleton
    abstract MyInterface bindMyInterface(MyImplementation myImplementation);
    
}

또한, @Provides 어노테이션으로 인스턴스를 삽입할 수도 있다.

@Provides 어노테이션은 Room이나 Retrofit 등의 외부 라이브러리에서 제공되는 클래스라 프로젝트 내에서 소유할 수 없는 경우Builder 패턴을 사용하여 인스턴스를 생성할 경우 주로 사용된다.

아래와 같은 규칙을 따라야 한다.

  • return 유형은 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt에 알림
  • 파라미터는 해당 유형의 종속 항목을 Hilt에 알림
  • 함수 본문은 해당 유형의 인스턴스를 제공하는 방법을 Hilt에 알림
    • Hilt는 해당 유형의 인스턴스를 제공해야 할 때마다 함수 본문을 실행
@Module
@InstallIn(ApplicationComponent.class)
public class ExternalLibraryModule {

    @Provides
    @Singleton
    ExternalLibraryClass provideExternalLibraryClass() {
        return new ExternalLibraryClass();
    }
    
}

@Binds와 @Provides 모두 Dagger 모듈 내에 정의된다.


오늘은 이렇게 안드로이드의 DI와 Hilt에 대해서 알아보았는데, 사실 아직 Hilt를 제대로 사용할 수 있다고 말하진 못할 것 같다.

Hilt를 사용해서 직접 프로젝트를 만들어서 의존성을 주입해 볼 생각이라, 추후 간단하게 코드를 작성하고 다시 포스팅을 하도록 하겠다.

profile
정위블

0개의 댓글