ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Android Hilt를 이용한 의존성 주입
    카테고리 없음 2021. 6. 21. 20:59
    반응형

    Android에서 DI(Dependency Injection)은 프로그래밍에 많이 사용되는 테크닉이며 안드로이드 개발에 적합하다. 또한, 

    • 리팩토링 용이, 재사용성 : 의존성이 필요한 클래스를 직접 생성하지 않으므로 클래스를 외부에서 교체하기 쉽다. 
    • 테스트 작성 용이 : 여러 구현의 클래스를 주입해 다양한 케이스를 확인할 있다.

    측면에서 개발에 용이하게 도와준다. 

     

    Dependency injection이란?

    한 클래스는 종종 다른 클래스를 참조한다. 예를 들면, `Car`라는 클래스는 `Engine`이라는 클래스를 참조하고 있다. 이렇게 클래스를 필요로하는 것을 의존성(dependency)이라고 한다. 아래 예제에서는 `Car`라는 클래스가 동작하기 위해서 `Engine`이라는 클래스를 참조하는 내용이다. 

    `Engine`의 object를 얻어오기 위해서는 아래 세 가지 방법이 있다.

    1. `Car`라는 클래스 안에서 `Engine` 이라는 클래스를 직접 생성한다.
    2.  Android API인 `Context`의 `getter`나 `getSystemService()`를 이용한다.
    3. `Car`의 생성자나 메소드의 파라미터로 받아온다.

    여기서 세 번째 방법이 의존성 주입(Dependency Injection)이다. 이 방법을 사용하면 1번의 방법처럼 직접 생성하는 일 없이 클래스를 참조할 수 있다.

    즉, 

    A design pattern that allows an object to supply the dependencies of another object.

     

    아래 예제는 의존성 주입 없이 `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`의 자식 클래스를 사용하기가 쉽지 않아진다. 이런 식으로  `Engine`을 직접 생성하면 `Car`를 재사용할 때, `Engine`을 상속한 `Gas`나 `Electric`이라는 클래스를 재사용하기가 쉽지 않다.

     

    다음 예제에서는 의존성 주입을 이용해서 `Engine`의 객체를 주입받아보자.

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

    `main()` 함수로부터 `Engine`의 객체를 생성해 `Car`의 생성자에게 전달해보자. 이 DI-전제의 예제는 이점이 있다.

    • `Car`를 재사용할 수 있다. `Car` 클래스에게 다른 `Engine`의 구현들을 전달할 수 있다. `Engine`의 자식 클래스인 `Electric`을 `Car`에게 전달할 수 있다. `Car`는 별다른 수정 없이 새로운 기능을 동작할 수 있다. 
    • `Car`를 테스트하기 쉽다. 여러 시나리오를 정해 테스트를 해볼 수 있다. `FakeEngine`이라는 클래스를 작성하는 등 `Engine`타입을 여러 방면으로 테스트해 볼 수 있다.

     

     

    그래서 안드로이드에서는 이 의존성 주입을 작성하는 두 가지 방법이 있다. 

    1. 생성자를 통한 주입 
    2. Setter를 통한 주입. 안드로이드 시스템에서는 Activity나 Fragment은 시스템이 생성하기 때문에 1번의 방법이 불가능하다. 참조가 필요한 클래스를 먼저 생성한 다음에 의존성을 주입하는 방법이다. 코드를 보면 아래와 같다.
    class Car {
        lateinit var engine: Engine
    
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val car = Car()
        car.engine = Engine()
        car.start()
    }

     

    의존성 주입 자동화

    이전 예제들에서 여러가지 클래스들을 특정 라이브러리에 의존하지 않고 의존성을 직접 생성하고 관리하고 제공했다. 이를 수동으로 관리하는 의존성 주입이라고 한다. `Car`의 예제에서는 의존성이 한 클래스에만 있었지만 더 많은 클래스에 의존성을 가지게 되면 관리가 힘들어질 수 있다. 

    • 수동 의존성 주입의 단점, 
      • 큰 앱은 의존성이 필요한 클래스들을 연결하려면 불필요한 코드가 마구 생성되게 된다. 여러 레이어를 가질 경우 최상위 객체를 가지기 위해서 그 아래 모든 계층의 객체가 필요하게 된다. 예를 들면 자동차를 만들려면 엔진, 변속기, 섀시 및 기타 부품이 필요할 수 있다. 엔진에는 실린더와 점화 플러그가 필요하다. 
      • 전달하기 전에 종속성을 생성할 수 없는 경우, lazy init과 같은 코드를 작성하고 수명을 직접 관리해야한다.

    이 의존성 주입을 자동으로 해주는 라이브러리들이 있다.

    • 런타임에 리플렉션을 기반으로 동작
      • 더보기
        ex) Class<?> cClass = Class.forName("com.theJava.reflection.store.Book");
    • 컴파일 타임에 코드를 생성해 종속성을 연결 

    Dagger라는 라이브러리는 Java, Kotlin, Android에서도 사용할 있는 의존성 주입 라이브러리이다. Dagger 의존성을 직접 관리하여 앱에서 쉽게 사용할 있도록 한다. Dagger1 컴파일 타임에 의존성을 연결해서 리플렉션 기반으로 의존성을 주입하는 것에 대한 성능 이슈를 해결한다. 

    단, 의존성 주입의 단점으로

    • 간단한 프로그램을 만들 때는 번거로움
    • 코드의 가독성을 떨어뜨릴 수 있음

    가 있다.

     

    의존성 주입의 대안

    의존성 주입의 대안으로 Service locator 디자인 패턴을 사용하는 것이다. Service locator도 마찬가지로 클래스들의 의존성을 줄일 수 있는 방법이다. Service locator라는 클래스를 생성하면, 의존성 클래스를 생성하고 들고 있는 역할을 한다. 

    object ServiceLocator {
        fun getEngine(): Engine = Engine()
    }
    
    class Car {
        private val engine = ServiceLocator.getEngine()
    
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val car = Car()
        car.start()
    }

    Service locator 디자인 패턴은 의존성 주입과 약간 다르다. 의존성이 필요한 클래스는 Service locator를 통해 클래스를 요청하고 주입한다. 반면, 의존성 주입을 이용하면 필요한 시점 이전에 클래스를 요청하고 주입해놓는다.

    • Service locator를 이용하면 테스트가 힘들어진다. 동일한 글로벌 Service locator를 통해 테스트를 해야하기 때문이다. 
    • 의존성 주입을 이용하면 객체를 앱의 전체 기간으로 관리하지만 Service locator는 특정 기간을 지정해야하기 때문에 관리가 어렵다. 

     

    Jetpack Hilt 예제 

    Hilt는 Jetpack의 의존성을 주입해주는 라이브러리 중에 하나이다. Hilt는 모든 Android 클래스에 컨테이너를 제공하고 자동으로 생명주기를 관리한다. Hilt Dagger 기반으로 빌드되어 Dagger 제공하는 컴파일 시간 정확성, 런타임 퍼포먼스 Android Studio 지원을 누릴 있다. 

     

    Dagger 차이점?

    Dagger와 Hilt는 구글에서 만들긴 했지만 Dagger는 안드로이드 뿐만 아니라 자바 앱에서도 동작하도록 디자인되어있다. 안드로이드에서 종속성 주입에는 Hilt를 사용하도록 권고하고 있고, Hilt는 Dagger를 기반으로 Dagger 종속성 주입을 안드로이드 애플리케이션에 통합하는 표준 방법을 제공합니다. SingletonComponent, ActivityComponent, FragmentComponent등 안드로이드 컴포넌트 생명주기에 맞게 인스턴스를 관리하는 방법을 제공한다. 

     

    의존성 주입의 보일러플레이트 예제 코드

    // ServiceLocator.kt
    import android.content.Context
    import androidx.fragment.app.FragmentActivity
    import androidx.room.Room
    import com.example.android.hilt.data.AppDatabase
    import com.example.android.hilt.data.LoggerLocalDataSource
    import com.example.android.hilt.navigator.AppNavigator
    import com.example.android.hilt.navigator.AppNavigatorImpl
    import com.example.android.hilt.util.DateFormatter
    
    class ServiceLocator(applicationContext: Context) {
    
        private val logsDatabase = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "logging.db"
        ).build()
    
        val loggerLocalDataSource = LoggerLocalDataSource(logsDatabase.logDao())
    
        fun provideDateFormatter() = DateFormatter()
    
        fun provideNavigator(activity: FragmentActivity): AppNavigator {
            return AppNavigatorImpl(activity)
        }
    }
    
    // LogApplication.kt
    import android.app.Application
    
    class LogApplication : Application() {
    
        lateinit var serviceLocator: ServiceLocator
    
        override fun onCreate() {
            super.onCreate()
            serviceLocator = ServiceLocator(applicationContext)
        }
    }
    

    LogApplication 클래스는 ServiceLocator클래스를 참조하고 있다. ServiceLocator 요청에 따라 종속 클래스를 생성하고 저장한다. 클래스는 소멸과 함께 소멸되므로 앱의 수명주기에 종속되는 컨테이너(a container of dependencies)이다. 

    컨테이너는 참조가 필요한 클래스의 인스턴스들을 생성해주고 수명주기를 관리해 인스턴스를 제공하는 역할을 한다. 이를 종속 클래스의 그래프(the graph of dependencies)를 관리한다고 한다. 

    컨테이너는 인스턴스를 가져올 있는 메소드를 노출한다. 메소드는 항상 다른 인스턴스 또는 동일한 인스턴스를 반환할 있다. 

     

    단, 위의 설명대로 ServiceLocator 클래스 코드는 보일러플레이트 코드를 생성한다. 따라서 Android 앱을 대규모로 개발하려면 Hilt를 추천한다.

    Hilt 개발자가 직접 작성해야하는 ServiceLocator 같은 코드를 자동으로 생성하여 Android 앱에 보일러플레이트 코드를 제거한다. 

     

    Hilt 사용하기 

    root/build.gradle에 다음과 같이 코드를 추가한다.

    buildscript {
        ...
        ext.hilt_version = '2.35'
        dependencies {
            ...
            classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
        }
    }

    app/build.gradle 에는 Annotation processors인 kotlin-kapt 플러그인 및 hilt dependency 들을 추가한다. 

    ...
    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-android-compiler:$hilt_version"
    }
    

     

    이제 Application 클래스에 @HiltAndroidApp 어노테이션을 추가한다. 

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

    @HiltAndroidApp은 앱이 의존성 주입을 가능하도록 Hilt가 코드를 생성하게 하는 트리거이다. 이 어노테이션이 붙은 클래스를 어플리케이션 컨테이너(Application container)라고 한다. 이 컨테이너 클래스는 앱의 수명주기와 밀접하게 연결된다. 또한 애플리케이션 컨테이너는 앱의 상위 컨테이너이므로 다른 컨테이너는 이 상위 컨테이너에서 제공하는 클래스에 액세스할 수 있습니다.

     

    Hilt를 이용해 클래스 필드에 의존성 주입

    LogsFragment라는 클래스에서 onAttach에서 필드를 채워보자. 레거시 코드에서는 Servicelocator를 사용하여 직접 LoggerLoccalDataSource와 DateFormatter 인스턴스를 채우고 있다. 

    AS-IS

    class LogsFragment : Fragment() {
    
        private lateinit var logger: LoggerLocalDataSource
        private lateinit var dateFormatter: DateFormatter
    
        override fun onAttach(context: Context) {
            super.onAttach(context)
    
            populateFields(context)
        }
    
        private fun populateFields(context: Context) {
            logger = (context.applicationContext as LogApplication).serviceLocator.loggerLocalDataSource
            dateFormatter =
                (context.applicationContext as LogApplication).serviceLocator.provideDateFormatter()
        }
        
        override fun onResume() {
            super.onResume()
    
            logger.getAllLogs { logs ->
                recyclerView.adapter =
                    LogsViewAdapter(
                        logs,
                        dateFormatter
                    )
            }
        }
        ...
    }

    TO-BE

    @AndroidEntryPoint
    class LogsFragment : Fragment() {
    
        @InMemoryLogger
        @Inject lateinit var logger: LoggerDataSource
        @Inject lateinit var dateFormatter: DateFormatter
    
        override fun onResume() {
            super.onResume()
    
            logger.getAllLogs { logs ->
                recyclerView.adapter =
                    LogsViewAdapter(
                        logs,
                        dateFormatter
                    )
            }
        }
    }

     

    ServiceLocator를 사용하는 대신 Hilt를 사용하여 이런 유형의 인스턴스를 생성하고 관리할 수 있다. LogsFragment에서 Hilt 사용하려면 @AndroidEntryPoint 주석을 사용한다.

    @AndroidEntryPoint 사용하면 Android Lifecycle 따르는 의존성 컨테이너를 생성한다. 

    @AndroidEntryPoint
    class LogsFragment : Fragment() {
        ...
    }

    Hilt는 현재 Application, Activity, Jepack Fragment, View, Service, BroadcastReceiver 를 지원한다. 

    @AndroidEntryPoint를 사용하면 Hilt는 LogsFragment의 수명 주기에 연결된 종속 컨테이너(Dependency container)를 생성하고 LogsFragment에 인스턴스들을 주입한다. 

    Hilt @Inject 이용하면 인스턴스들을 주입 있다. @Inject 어노테이션이 붙은 필드들에 인스턴스가 주입된다. 

    @AndroidEntryPoint
    class LogsFragment : Fragment() {
    
        @Inject lateinit var logger: LoggerLocalDataSource
        @Inject lateinit var dateFormatter: DateFormatter
    
        ...
    }
    

    이를 클래스 필드 주입이라고 한다. 

    필드 삽입을 실행하려면 Hilt에서 제공하는 Android 클래스 필드에 @Inejct 주석을 사용해야한다. private한 필드에는 주입되지 않는다. 

    또한 Hilt LogsFragment 종속 컨테이너에서 빌드한 인스턴스를 사용해서 onAttach() 수명주기 메서드 내에서 이러한 필드를 채운다. 그 밖에 어떤 수명주기 콜백에서 인스턴스가 주입되는지 알 수 있다. 

    필드 주입을 하려면 Hilt에게 어떤 클래스의 인스턴스를 제공해야하는지 알려줘야한다. 예제에서는 Hilt LoggerLocalDataSource DateFormatter 인스턴스 제공을 어떻게 해야하는지 알아야한다. 

     

    @Inject로 클래스 필드에 인스턴스 제공 

    ServiceLocator.kt 파일에서 provideDateFormatter() 호출하면 어떻게 항상 다른 DateFormatter 인스턴스가 반환되는지 있다.

    // ServiceLocator.kt 
    fun provideDateFormatter() = DateFormatter()

    Hilt 이와 동일하게 결과를 얻고자 한다. 방법은 클래스 생성자에 @Inject주석 추가하면 된다. 

    class DateFormatter @Inject constructor() { ... }

    이제 Hilt DateFormatter 인스턴스를 어떻게 가져와야하는지 알게 되었다. LoggerLocalDataSource 마찬가지로 작업한다. 

    class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
        ...
    }

    Hilt가 알고 있는 서로 다른 타입들의 인스턴스를 제공하는 방법을 알려주는 것을 바인딩(binding)이라고 한다. 위의 예제에서는 두개의 바인딩이 사용되었고 Hilt가 DateFormatter와 LoggerLocalDataSource의 인스턴스들을 제공하는 방법을 알려준 것을 의미한다. 

    다만 레거시코드에서는 LoggerLocalDataSource 인스턴스는 항상 새로 생성되지 않고 같은 인스턴스를 리턴하게 되어있다. 인스턴스를 컨테이너에 범위가 지정되어있다(scoping an instance to a container) 한다.

    // ServiceLocator.kt
    val loggerLocalDataSource = LoggerLocalDataSource(logsDatabase.logDao())

     

    위의 어노테이션들을 통해서 컴파일이 어떻게 이루어졌는지 확인해보자.

    class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
        ...
    }

    @Inject를 붙인 LoggerLocalDataSource는 Provider라는 클래스를 상속받아 LoggerLocalDataSource_Factory 클래스를 생성한다.

     

    dagger.internal.DaggerGenerated;
    import dagger.internal.Factory;
    import javax.inject.Provider;
    
    @DaggerGenerated
    @SuppressWarnings({
        "unchecked",
        "rawtypes"
    })
    public final class LoggerLocalDataSource_Factory implements Factory<LoggerLocalDataSource> {
      private final Provider<LogDao> logDaoProvider;
    
      public LoggerLocalDataSource_Factory(Provider<LogDao> logDaoProvider) {
        this.logDaoProvider = logDaoProvider;
      }
    
      @Override
      public LoggerLocalDataSource get() {
        return newInstance(logDaoProvider.get());
      }
    
      public static LoggerLocalDataSource_Factory create(Provider<LogDao> logDaoProvider) {
        return new LoggerLocalDataSource_Factory(logDaoProvider);
      }
    
      public static LoggerLocalDataSource newInstance(LogDao logDao) {
        return new LoggerLocalDataSource(logDao);
      }
    }

    이제 get()이 호출되면 그 타이밍에 LoggerLocalDataSource 인스턴스를 반환하게 된다. 

    더보기

    @startuml
    class javax.Provider {
     fun T: get() 
    }

    class dagger.Factory 
    class generated.LoggerLocalDataSource_Factory {}

    javax.Provider <|-- dagger.Factory 
    dagger.Factory <|-- generated.LoggerLocalDataSource_Factory
    @enduml

     

    그리고 LoggerLocalDataSource 인스턴스를 주입받아 사용하는 LogFragment는

    @AndroidEntryPoint
    class LogsFragment : Fragment() {
    
        @Inject lateinit var logger: LoggerLocalDataSource
        @Inject lateinit var dateFormatter: DateFormatter
    
        ...
    }
    

    MembersInjector를 상속받아 injectMemebers()가 호출되는 타이밍에의 인스턴스를 얻게 된다. 

    더보기

    @startuml
    class dagger.MembersInjector {
    fun injectMembers(T instance)
    }

    class generated.LogsFragment_MembersInjector {
    }


    dagger.MembersInjector <|-- generated.LogsFragment_MembersInjector
    @enduml

    import com.example.android.hilt.data.LoggerDataSource;
    import com.example.android.hilt.di.InMemoryLogger;
    import com.example.android.hilt.util.DateFormatter;
    import dagger.MembersInjector;
    import dagger.internal.DaggerGenerated;
    import dagger.internal.InjectedFieldSignature;
    import javax.inject.Provider;
    
    @DaggerGenerated
    @SuppressWarnings({
        "unchecked",
        "rawtypes"
    })
    public final class LogsFragment_MembersInjector implements MembersInjector<LogsFragment> {
      private final Provider<LoggerDataSource> loggerProvider;
    
      private final Provider<DateFormatter> dateFormatterProvider;
    
      public LogsFragment_MembersInjector(Provider<LoggerDataSource> loggerProvider,
          Provider<DateFormatter> dateFormatterProvider) {
        this.loggerProvider = loggerProvider;
        this.dateFormatterProvider = dateFormatterProvider;
      }
    
      public static MembersInjector<LogsFragment> create(Provider<LoggerDataSource> loggerProvider,
          Provider<DateFormatter> dateFormatterProvider) {
        return new LogsFragment_MembersInjector(loggerProvider, dateFormatterProvider);
      }
    
      @Override
      public void injectMembers(LogsFragment instance) {
        injectLogger(instance, loggerProvider.get());
        injectDateFormatter(instance, dateFormatterProvider.get());
      }
    
      @InjectedFieldSignature("com.example.android.hilt.ui.LogsFragment.logger")
      @InMemoryLogger
      public static void injectLogger(LogsFragment instance, LoggerDataSource logger) {
        instance.logger = logger;
      }
    
      @InjectedFieldSignature("com.example.android.hilt.ui.LogsFragment.dateFormatter")
      public static void injectDateFormatter(LogsFragment instance, DateFormatter dateFormatter) {
        instance.dateFormatter = dateFormatter;
      }
    }

     

    @Scope 어노테이션

    Hilt는 인스턴스 생성이 필요한 클래스에 어노테이션을 붙임으로써 컨테이너들에 다른 생명주기를 갖도록 제공할 수 있다. 

    • 범위가 지정되지 않음 (unscoped) - 어노테이션이 지정되지 않았을 때, 인스턴스 주입이 필요할 때마다 항상 새로운 인스턴스를 생성한다.
    • 커스텀 범위가 지정됨 (Custom scoped) - @Singleton 어노테이션과 같이 앱 컨테이너 등의 지정된 컴포넌트 범위동안 같은 인스턴스를 제공한다. 

    인스턴스 범위를 앱 컨테이너로 지정하는 어노테이션은 @Singleton이다. 이 주석을 사용하면 앱 컨테이너에서 항상 같은 인스턴스를 제공한다. 

    Android 클래스에 연결된 모든 컨테이너에는 동일한 로직을 적용할 수 있다. 예를 들면 Activity 컨테이너에 항상 특정 유형에 동일한 인스턴스를 제공하려면 @ActivityScoped 어노테이션을 달면 된다.

    예제에 LoggerLocalDataSource 인스턴스를 항상 동일한 인스턴스를 제공하기 위해 @Singleton 어노테이션을 추가한다. 

    @Singleton
    class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
        ...
    }

    여러 컨테이너에서 하나의 인스턴스를 사용할 수 있다. 예를 들면 앱 컨테이너에서 LoggerLocalDataSource의 인스턴스를 사용할 수 있다면 Activity와 Fragment 컨테이너에서도 동일한 인스턴스를 사용할 수 있다.

    이제 Hilt에서 LoggerLocalDataSource 인스턴스 제공 방법을 알게 되었다. 하지만 생성자에 넘겨야할 인스턴스가 있다. Hilt에게도 LogDao 인스턴스 제공 방법을 알려줘야한다. LogDao는 LoggerLocalDataSource의 전이 종속 클래스(Transitive dependencies)라고 한다. 하지만 LogDao 인터페이스이므로 생성자가 없어 @Injector 없다. LogDao의 인스턴스는 어떻게 전달해줘야할까? 

     

    Hilt 모듈

    모듈을 사용해 Hilt에 서로다른 타입들의 인스턴스를 제공하는 방법을 알려주는 것을 바인딩이라고 했다. Hilt 모듈에 인터페이스와 같이 생성자로 인스턴스를 생성할 수 없는 바인딩 유형을 Hilt 모듈에 추가한다. 새로운 바인딩 유형을 추가하기 위해선 모듈 사용이 적합하다. 

    Hilt 모듈은 @Module과 @InstallIn 어노테이션이 추가된 모듈이다. @Module은 Hilt가 모듈임을 알려주고 @InstallIn은 어떤 컨테이너에서 Hilt를 이용해 어떤 안드로이드 컴포넌트와 바인딩을 할 수 있는지 Hilt에 알려준다. 

    Hilt에서 주입할 수 있는 Android 클래스마다 연결할 수 있는 Hilt 컴포넌트가 있다. 예를 들면 Activity 컨테이너는 ActivityComponent와 연결되며 Fragment는 FragmentComponent와 연결된다.

     

    모듈 만들기

    바인딩을 추가할 수 있는 Hilt 모듈을 만들어보자. “hilt” 패키지 아래 di라는 새 패키지를 만들고 DatabaseModule.kt라는 새 파일을 만든다. 

    LoggerLocalDataSource 컨테이너로 범위가 지정되므로 컨테이너에서 LogDao 바인딩을 사용할 있어야한다. 컨테이너와 연결된 Hilt 구성요소 클래스인 SingletonComponent::class 전달해 @InstallIn 주석으 지정한다. 

    import dagger.Module
    import dagger.hilt.InstallIn
    import dagger.hilt.components.SingletonComponent
    
    @InstallIn(SingletonComponent::class)
    @Module
    object DatabaseModule {
    }

    ServiceLocator 클래스 구현에서는 LogDao 인스턴스는 logsDatabase.logDao()를 호출해 가져온다. 따라서 Hilt로 LogDao인스턴스를 제공하려면 AppDatabase#logDao를 호출해야한다. 

    @Module
    object DatabaseModule {
    
        @Provides
        fun provideLogDao(database: AppDatabase): LogDao {
            return database.logDao()
        }
    }

     

     

    @Provides 로 인스턴스 제공 

    인터페이스 타입에 대한 인스턴스 제공은 @Provide 어노테이션을 달아 Hilt에 생성자가 삽입될 수 없는 타입이란걸 알려줄 수 있다. 

    @Provides 어노테이션이 있는 함수의 구현은 Hilt에서 타입의 인스턴스를 제공해야할 때마다 실행된다. 

    @Module
    object DatabaseModule {
    
        @Provides
        fun provideLogDao(database: AppDatabase): LogDao {
            return database.logDao()
        }
    }

    위의 구현은 Hilt에 LogDao 인스턴스를 제공할 때 AppDatabase.logDao()가 실행되어야한다고 알려준다. AppDatabase가 전이 종속 클래스이므로 Hilt에 이 타입의 인스턴스 제공 방법도 알려줘야한다. 

     

    AppDatabase Room에서 생성하지 않으므로 프로젝트에서 소유하지 않는 다른 클래스이기 때문에 ServiceLocator 클래스에서 데이터베이스 인스턴스를 만드는 방식과 동일하게 @Provides  함수를 제공  있다.

    @Module
    object DatabaseModule {
    
        @Provides
        @Singleton
        fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
            return Room.databaseBuilder(
                appContext,
                AppDatabase::class.java,
                "logging.db"
            ).build()
        }
    
        @Provides
        fun provideLogDao(database: AppDatabase): LogDao {
            return database.logDao()
        }
    }

     

    Hilt에 항상 동일한 데이터베이스 인스턴스를 제공하도록 하려면 @Provides provideDatabase 함수에 @Singleton 어노테이션을 추가한다. 

     

    code generation

    Module 클래스에서 AppDatabase를 생성하는 방법이다. provideLogDao()가 호출될 때, DatabaseModule.INSTANCE.provideLogDao()의 구현이 대신 호출되도록 하고 있다. LogDao도 같은 방식으로 인스턴스를 가져오고 있다. 

    public final class DatabaseModule_ProvideLogDaoFactory implements Factory<LogDao> {
      private final Provider<AppDatabase> databaseProvider;
    
      public DatabaseModule_ProvideLogDaoFactory(Provider<AppDatabase> databaseProvider) {
        this.databaseProvider = databaseProvider;
      }
    
      @Override
      public LogDao get() {
        return provideLogDao(databaseProvider.get());
      }
    
      public static DatabaseModule_ProvideLogDaoFactory create(Provider<AppDatabase> databaseProvider) {
        return new DatabaseModule_ProvideLogDaoFactory(databaseProvider);
      }
    
      public static LogDao provideLogDao(AppDatabase database) {
        return Preconditions.checkNotNullFromProvides(DatabaseModule.INSTANCE.provideLogDao(database));
      }
    }

    AppDatabase는 @Singleton 어노테이션을 붙였기 때문에 Singleton 클래스가 생성되어서 인스턴스를 관리하게 된다. 

    public final class DaggerLogApplication_HiltComponents_SingletonC extends LogApplication_HiltComponents.SingletonC {
      private final ApplicationContextModule applicationContextModule;
    
      private volatile Object appDatabase = new MemoizedSentinel();
    
      private DaggerLogApplication_HiltComponents_SingletonC(
          ApplicationContextModule applicationContextModuleParam) {
        this.applicationContextModule = applicationContextModuleParam;
      }
    
      public static Builder builder() {
        return new Builder();
      }
    
      private AppDatabase appDatabase() {
        Object local = appDatabase;
        if (local instanceof MemoizedSentinel) {
          synchronized (local) {
            local = appDatabase;
            if (local instanceof MemoizedSentinel) {
              local = DatabaseModule_ProvideDatabaseFactory.provideDatabase(ApplicationContextModule_ProvideContextFactory.provideContext(applicationContextModule));
              appDatabase = DoubleCheck.reentrantCheck(appDatabase, local);
            }
          }
        }
        return (AppDatabase) local;
      }
    
      @Override
      public void injectLogApplication(LogApplication logApplication) {
      }
    
      @Override
      public LogDao logDao() {
        return DatabaseModule_ProvideLogDaoFactory.provideLogDao(appDatabase());
      }
    ...

     

    앱실행

    이제 Hilt는 LogsFragment에 인스턴스를 주입하는 데 필요한 모든 정보를 갖고 있다. 그러나 Hilt는 앱을 실행하기 전에 작동을 위해 Fragment를 실행하는 Activity를 알아야한다. MainActivity에 @AndroidEntryPoint로 주석을 달아야한다. 

    MainActivity.kt 추가한다.

    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() { ... }

     

    @Binds로 인터페이스 제공

    MainActivity는  함수를 호출하여 ServiceLocator#provideNavigator()에서 AppNavigator 인스턴스를 가져온다. 

    AppNavigator는 인터페이스 이므로 생성자 주입을 사용할 수 없다. 인터페이스에 사용할 구현을 Hilt에 알리려면 Hilt 모듈 내 함수에 @Binds 어노테이션을 사용하면 된다. 

    @Bind 어노테이션은 abstract 함수에 달아야한다(이 함수는 추상 함수이므로 클래스도 추상 클래스여야함). 추상 함수의 반환 타입은 인터페이스 AppNavigator이다. 구현은 AppNavigatorImpl이다. 

    이 정보를 DatabaseModule 클래스에 추가하기 전에 새 모듈 추가가 필요할지 생각해보자. 이름, 컨테이너 생명주기, 클래스내의 어노테이션 조합을 봐야한다.  

    • 효율적인 구성을 위해서 모듈의 이름은 제공하는 정보의 타입이어야한다. 예를 들어, DatabaseModule이라는 모듈에 위 함수를 추가하는 것은 적절하지 않다. 
    • DatabaseModule 모듈은 @Singleton 컨테이너에서 바인딩하므로 Acitivy 컨테이너를 파라미터로 필요로하는 AppNavigator에는 적절하지 않다.
    • Hilt 모듈에는 인터페이스 바인딩 어노테이션과 추상 바인딩 어노테이션을 한 곳에 사용할 수 없다. @Binds와 @Provides 어노테이션과 함께 사용할 수 없다.

     

    따라서 NavigationModule.kt라는 파일을 새로 만들고 @Module 및 @InstallIn(AcitivtyComponent::class) 어노테이션이 달린 NavigationModule이라는 새 추상클래스를 만든다. 모듈 안에서 AppNavigator의 바인딩을 추가한다. 

    @InstallIn(ActivityComponent::class)
    @Module
    abstract class NavigationModule {
    
        @Binds
        abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
    }
    

     

    이제 AppNavigatorImpl 인스턴스 제공 방법을 Hilt에게 알려줘야한다. 

    class AppNavigatorImpl @Inject constructor(
        private val activity: FragmentActivity
    ) : AppNavigator {
        ...
    }
    

    AppNavigatorImpl FragmentActivity 종속된다.  AppNavigator 인스턴스가 Activity컨테이너에서 제공되므로 FragmentActivity가 생성자에서 사용 가능하게 된다.

     

    Activity에서 Hilt 사용

    Hilt는 이제 AppNavigator 인스턴스를 주입할 수 있는 모든 정보를 보유하고 있다. MainActivity에서 아래 작업을 수행한다.

    • navigator 필드에 @Inject를 추가한다.
    • private를 지운다
    • onCreate에서 사용한다. 
    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
    
        @Inject lateinit var navigator: AppNavigator
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            
            if (savedInstanceState == null) {
                navigator.navigateTo(Screens.BUTTONS)
            }
        }
    
        ...
    }

     

    AS-IS

    class ButtonsFragment : Fragment() {
    
        private lateinit var logger: LoggerLocalDataSource
        private lateinit var navigator: AppNavigator
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            return inflater.inflate(R.layout.fragment_buttons, container, false)
        }
    
        override fun onAttach(context: Context) {
            super.onAttach(context)
    
            populateFields(context)
        }
    
        private fun populateFields(context: Context) {
            logger = (context.applicationContext as LogApplication).
                serviceLocator.loggerLocalDataSource
    
            navigator = (context.applicationContext as LogApplication).
                serviceLocator.provideNavigator(requireActivity())
        }

     

    TO-BE

    @AndroidEntryPoint
    class ButtonsFragment : Fragment() {
    
        @Inject lateinit var logger: LoggerLocalDataSource
        @Inject lateinit var navigator: AppNavigator
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            return inflater.inflate(R.layout.fragment_buttons, container, false)
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            ...
        }
    }

     

     

    참고

    koin vs hilt - https://www.programmersought.com/article/77526520842/

    https://dagger.dev/hilt/components.html

    https://blog.corporate.jobrapido.com/dagger2-dependency-injection-at-compile-time/

    reflection - https://better-dev.netlify.app/java/2020/08/15/thejava_7/

    1. ClassLoader에 클래스 정보 로딩
    2. Heap 메모리에 클래스 인스턴스 할당

    https://developer.android.com/training/dependency-injection/hilt-android

    https://developer.android.com/codelabs/android-hilt#1

    반응형
Designed by Tistory.