본문 바로가기

카테고리 없음

Hilt(의존성 주입 라이브러리)

Hilt가 무엇인지 알기전에 기본적으로 DI(의존성 주입)이 어떤 것을 의미하는지 알아야 할 필요가 있다. 아래 공식문서에서 매우 잘 설명해놨으니까 기억이 나지 않는다면 다시 복습해보자.
 
안드로이드에서 말하는 의존성 주입이란?
 
수동으로 사용하는 의존성 주입
(deggar나 hilt를 사용하는게 왜 기존의 의존성 주입을 사용하는 것보다 더 나은지 잘 모르는 경우 참고하자.)
 
 
라이브러리(hilt, dagger)를 사용하지 않은 의존성 주입의 단점
Compared to dependency injection:
 
  • The collection of dependencies required by a service locator makes code harder to test because all the tests have to interact with the same global service locator.
  • Dependencies are encoded in the class implementation, not in the API surface. As a result, it's harder to know what a class needs from the outside. As a result, changes to Car or the dependencies available in the service locator might result in runtime or test failures by causing references to fail.
  • Managing lifetimes of objects is more difficult if you want to scope to anything other than the lifetime of the entire app.
 
-의존성들이 갖는 service locator 들이 많은 테스트하기 어려운 코드를 만든다. 모든 테스트들은 전역 service locator와 상호작용을 해야하기 때문에 코드를 더 난잡하게 만든다.
-의존성은 클래스의 구현체에 인코딩되지 API surface에 되는게 아니다. 결과적으로 어떤 클래스가 필요한지 밖에서는 모른다.
-앱 전반에 거쳐서 객체의 생명주기를 관리하기 어렵다.
 
Hilt를 쓰는 이유
무분별하게 늘어나는 코드(boiler plate)를 지양하기 위해서, 그렇게 늘어나는 코드로 인해서 발생할 수 있는 에러의 위험 가능성. 그리고 각 객체의 생명주기를 개발자가 직접 관리해줘야 하는데 이걸 다 신경쓰면서 개발하다가는 정작 해야할 개발은 해보지도 못하고 힘을 다 써버릴 수도 있다. 그런 이유에서 hilt나 dagger를 사용한다.
위의 링크에 있는 튜토리얼대로 개발을 하는 과정 자체가 dagger로 의존성 주입을 하는 과정과 매우 유사하다. 일련의 해당 과정들을 굳이 해주지 않아도 되니까 Hilt를 쓰는 것이다.
 
hilt란?
Hilt is Jetpack's recommended library for dependency injection in Android. Hilt defines a standard way to do DI in your application by providing containers for every Android class in your project and managing their lifecycles automatically for you.
안드로이드에서 의존성 주입을 위해서 추천하는 라이브러리다. hilt는 당신의 안드로이드 application에 있는 모든 클래스에게 컨테이너를 제공함으로서 그들의 생명주기를 자동적으로 관리해준다.
 
이제부터 서술할 내용은 아래의 링크 codelab을 참고해서 만들었다.
 
Hilt에서 제공하는 다양한 어노테이션들
We can use annotations to scope instances to containers. As Hilt can produce different containers that have different lifecycles, there are different annotations that scope to those containers.
어노테이션을 이용해서 인스턴스의 범위를 컨테이너로 지정할 수 있다. (인스턴스의 범위를 컨테이너에 담는다고 표현하는게 적절한 표현일듯. 아래 영어사전의 내용을 참고하자.)

 

As Hilt can produce different containers that have different lifecycles, there are different annotations that scope to those containers.
Hilt는 다른 컨테이너를 생산할 수 있고 이는 다른 생명 주기를 갖는다. Hilt는 수명 주기가 다른 여러 컨테이너를 생성할 수 있으므로 이러한 컨테이너로 범위가 지정된 다양한 어노테이션이 있다.
 
The annotation that scopes an instance to the application container is @Singleton. This annotation will make the application container always provide the same instance regardless of whether the type is used as a dependency of another type or if it needs to be field injected.
인스턴스의 범위를 애플리케이션 컨테이너로 지정하는 어노테이션은 @Singleton이다. 이 어노테이션은 어플리케이션 컨테이너가 항상 같은 객체를 제공하게 해준다. 설령 의존성이 다른 타입으로 쓰이거나 필드로 의존성이 주입되야하는 상황에서도 말이다. (의존성 주입에는 생성자, 필드 주입 방법이 있다. ServiceLocator도 있긴 한데.. 일단 앞의 두가지 방법을 제대로 숙지하면서 읽자.)
 
The same logic can be applied to all containers attached to Android classes. You can find the list of all scoping annotations in the Component scopes section in the documentation. For example, if you want an activity container to always provide the same instance of a type, you can annotate that type with @ActivityScoped.
안드로이드 클래스에 연결된 모든 컨테이너에는 동일한 로직을 적용할 수 있다. (아마 모든 객체에 같은 방식으로 적용하는게 가능하다는 말인 듯 하다.) 영역을 지정하는 모든 어노테이션을 Component scopes 섹션에서 확인 가능하다. 예를 들어서 액티비티 컨테이너에게 항상 같은 객체의 타입을 제공하고 싶다면, @ActivityScoped어노테이션을 사용할 수 있다.
 
@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}
애플리케이션 컨테이너가 항상 같은 LoggerLocalDataSource객체를 제공하게 하고 싶다면 위와 같이 @Singleton 어노테이션을 사용하면 된다.
 
If a binding is available in a container, then it is also available in all containers below that one in the component hierarchy. Therefore, if an instance of LoggerLocalDataSource is available in the application container, it will also be available in activity and fragment containers.
계층구조의 상위 컨테이너에서 사용할 수 있는 결합은 계층구조의 하위 수준에서도 사용할 수 있다. 따라서, 애플리케이션 컨테이너에서 LoggerLocalDataSource의 인스턴스를 사용할 수 있다면 액티비티 컨테이너와 프래그먼트 컨테이너에서도 동일한 인스턴스를 사용할 수 있다.
 
이제 Hilt는 LoggerLocalDataSource인스턴스를 어떻게 제공하면 되는지 알고 있다. (@Inject 어노테이션을 사용해서 생성자 주입을 쓰는 방법을 채택했다는 사실을 안다.)
그런데 생성자의 매개변수를 보면 LogDao가 있는데 이 녀석은 인터페이스다. Hilt 입장에서는 LogDao인스턴스를 어떻게 제공하는지도 알아야 한다.
 
LogDao 는 인터페이스임으로 생성자가 없다. 그래서 @Inject 어노테이션을 사용할 수 없다. 이때 사용하는 것이 Hilt modules 다.
 
Hilt modules
Modules are used to add bindings to Hilt, or in other words, to tell Hilt how to provide instances of different types.
힐트에서 사용하기 위해서 모듈은 사용되는데 이는 즉, Hilt에게 객체를 어떻게 다른 타입으로 제공하는지 알려주는 것이다. (모듈을 사용하여 Hilt에 다양한 유형의 인스턴스 제공 방법을 알려 줍니다.  by.공문)
(Hilt입장에서는 LogDao객체를 어떻게 제공 받아야 하는지 모르니까 그 방법을 Module을 통해서 알려준다는 말이다.)
 
In Hilt modules, you can include bindings for types that cannot be constructor-injected such as interfaces or classes that are not contained in your project. An example of this is OkHttpClient - you need to use its builder to create an instance.
인터페이스나 프로젝트에 포함되지 않은 클래스와 같이 생성자가 삽입될 수 없는 유형의 결합을 Hilt 모듈에 포함한다. 예를 들어서 OkHttpClient 말이다. -> 이 라이브러리는 빌더 패턴을 통해서 객체를 만든다.
 
A Hilt module is a class annotated with @Module and @InstallIn.
Hilt 모듈 클래스는 @Module과 @Install 어노테이션을 사용해서 만들어진다. 설명을 따라가다보면 나오는 아래의 코드 스니펫을 참조하자.
 
@Module tells Hilt that this is a module and @InstallIn tells Hilt the containers where the bindings are available by specifying a Hilt component.
@Module 어노테이션은 힐트에게 이 클래스가 모듈임을 알리고 @InstallIn 어노테이션은 어느 컨테이너에서 Hilt 구성요소를 지정하여 결합할 수 있는지 Hilt에게 알려준다.
(아래 예시 코드에서는 SingletonComponent컨테이너에서 HIlt구성요소를 지정해서 결합한다는 의미다.)
 
 
You can think of a Hilt component as a container. The full list of components can be found here.
Hilt 구성요소를 container라고 생각할 수 있다. 모든 구성요소를 확인하기 위해서는 위의 링크를 참조하자.
 
For each Android class that can be injected by Hilt, there's an associated Hilt component. For example, the Application container is associated with SingletonComponent, and the Fragment container is associated with FragmentComponent.
각 안드로이드 클래스는 Hilt에 의해서 의존성 주입이 가능하고, Hilt컴포넌트와 관련이 있다. 예를 들어서 Application 컨테이너는 SingletonComponent와 관련이 있고, Fragment 컨테이너는 FragmentComponet와 관련이 있다.
(힐트의 컴포넌트가 어떤 역할을 하는지는 아직까지 서술되지 않아서 잘 모르겠다. 일단 이런게 있다는 사실만 알아두자.)
 

Creating a module

모듈생성
 
Let's create a Hilt module where we can add bindings. Create a new package called di under the hilt package and create a new file called DatabaseModule.kt inside that package.
이건 뭐 그냥 따라하면 될듯. 코드랩에서 따라하라는 내용은 일단 따로 공부하면서 따라하면 되는거니까 패스하겠다.
 
Since LoggerLocalDataSource is scoped to the application container, the LogDao binding needs to be available in the application container. We specify that requirement using the @InstallIn annotation by passing in the class of the Hilt component associated with it (i.e. SingletonComponent:class):
 
아래 사진처럼 이전에 LoggerLocalDataSource에 싱글톤 어노테이션을 붙혔다. 이는 application container영역에 해당하는데, LogDao 또한 application container에서 결합할 수 있어야 한다.
여기서는 애플리케이션 컨테이너에 연결된 Hilt 구성요소 클래스(예: ApplicationComponent:class)를 전달하여 @InstallIn 어노테이션으로 요구사항을 지정합니다.

 

package com.example.android.hilt.di
 
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {
 
}
 
In the ServiceLocator class implementation, the instance of LogDao is obtained by calling logsDatabase.logDao(). Therefore, to provide an instance of LogDao we have a transitive dependency on the AppDatabase class.
 
LogDao 객체를 hilt에게 알려주기 위해서는 AppDatabase 클래스의 의존성을 알려줘야 한다.
 
In Kotlin, modules that only contain @Provides functions can be object classes. This way, providers get optimized and almost in-lined in generated code.
코틀린에서, @Provides만 포함하는 메소드는 object 클래스가 될 수 있다. 이 방식으로 providers는 최적화되고 생성된 코드에 in-lined 된다.
 

Providing instances with @Provides

@Provides 어노테이션을 사용해서 객체 제공
 
We can annotate a function with @Provides in Hilt modules to tell Hilt how to provide types that cannot be constructor injected.
@Provides 어노테이션을 사용해서 Hilt 모듈에 있는 메소드에게 생성자를 사용한 의존성 주입이 되지 않는다는 사실을 알릴 수 있다.
 
The function body of a function that is annotated with @Provides will be executed every time Hilt needs to provide an instance of that type. The return type of the @Provides-annotated function tells Hilt the binding type, the type that the function provides instances of.. The function parameters are the dependencies of that type.
@Provides 어노테이션이 있는 메소드 본문은 Hilt에서 이 유형의 인스턴스를 제공해야 할 때마다 실행됩니다.
 
In our case, we will include this function in the DatabaseModule class:
 
@Module
object DatabaseModule {
 
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
 
The code above tells Hilt that database.logDao()needs to be executed when providing an instance of LogDao. Since we have AppDatabase as a transitive dependency, we also need to tell Hilt how to provide instances of that type.
위의 코드는 Hilt에게 database.logDao() 가 LogDao 객체가 제공될때 실행되야 할 필요가 있다고 알려준다. AppDatabase를 전이 종속으로 갖고있기 때문에, Hilt에게 해당 객체가 어떻게 제공되는지 알려줘야 할 필요가 있다.
 
Our project doesn't own the AppDatabase class either, because it is generated by Room. We can't constructor inject AppDatabase, but we can use an @Provides function to provide it, too. This is similar to how we build the database instance in the ServiceLocator class:
이 예시 프로젝트는 AppDatabase 클래스를 갖고 있지 않는다. 이는 Room을 통해서 생성되기 때문이다. AppDatabase를 생성자의존성 주입으로 만들 수 없다. 하지만 @Provides 어노테이션을 사용해서 의존성 주입을 Hilt에게 제공할 수 있다.
 
AppDatabase는 Room에서 생성하지 않으므로 프로젝트에서 소유하지 않는 다른 클래스이기 때문에 ServiceLocator 클래스에서 데이터베이스 인스턴스를 빌드하는 방식과 비슷하게 @Provides 함수를 사용하여 제공할 수도 있습니다.
 
import android.content.Context
import androidx.room.Room
import com.example.android.hilt.data.AppDatabase
import com.example.android.hilt.data.LogDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
 
@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
 
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
 
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}
}
 
Since we always want Hilt to provide the same database instance, we annotate the @Provides provideDatabase() method with @Singleton.
Hilt가 항상 같은 DB 객체를 제공해주기 원한다면, 위와 같은 어노테이션을 사용하면 된다.
 
Each Hilt container comes with a set of default bindings that can be injected as dependencies into your custom bindings. This is the case with applicationContext. To access it, you need to annotate the field with @ApplicationContext.
각 Hilt 컨테이너는 맞춤 결합에 종속 항목으로 삽입될 수 있는 일련의 기본 결합을 제공합니다. 이는 applicationContext의 사례로, 액세스하려면 필드에 @ApplicationContext 주석을 달아야 합니다.
(문장을 보고 해석하려고 했는데 이해가 안되니까 해석이 잘 안된다. 의역이 안되네;; 혹시 문장의 의미를 의역할 수 있는 시점이 오면 글을 좀 수정하자.)
 
힐트나 대거에서 얘기하는 컴포넌트란 무엇일까?
What is a component in dagger?
@Component tells Dagger to generate a container with all the dependencies required to satisfy the types it exposes.
 
컴포넌트는 대거에게 의존성 주입이 가능한 모든 객체의 컨테이너를 만들라고 지시한다.
 

Running the app

앱 실행
 
Now, Hilt has all the necessary information to inject the instances in LogsFragment. However, before running the app, Hilt needs to be aware of the Activity that hosts the Fragment in order to work. We'll need to use @AndroidEntryPoint.
코드랩에서 여기까지 따라왔으면 Hilt는 LogsFragment가 필요로 하는 모든 객체의 정보를 갖게된다. 그러나 앱을 실행하기 앞서서, Hilt는 Framgnet의 부모가 되는 Activity가 무엇인지 알고 있어야 한다.
@AndroidEntryPoint 어노테이션을 사용해야 한다.
 
 
Open the ui/MainActivity.ktfile and annotate the MainActivitywith @AndroidEntryPoint:
 
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { ... }
 
Now, you can run the app and check that everything still works.
Let's continue refactoring the app to remove the ServiceLocatorcalls from the MainActivity.
 
 
뭐.. 여기까지 따라왔으면 앱은 잘 작동할 것이고 ServiceLocator를 호출하는 부분을 MainActivity에서 제거하고 코드를 리팩토링해보자.
의역하자면 ServiceLocator 를 사용해서 의존성 주입(Manual Dependnecy Injection)을 Hilt로 바꾸는 것을 보여주려고 하는 것 같다.
 

8. Providing interfaces with @Binds

@Binds 어노테이션을 사용해서 인터페이스를 제공
MainActivity.kt
onCreate(){
 
...
 
//provideNavigator 메소드를 들어가면 ServiceLocator로 이동한다.
navigator = (applicationContext as LogApplication).serviceLocator.provideNavigator(this)
...
 
 
}
 
ServiceLocator.kt
class ServiceLocator(applicationContext: Context) {
 
...
// 그리고 AppNavigator는 인터페이스다. 아래 코드를 보면 안다.
fun provideNavigator(activity: FragmentActivity): AppNavigator {
return AppNavigatorImpl(activity)
}
}
 
 
/**
* Interfaces that defines an app navigator.
*/
interface AppNavigator {
// Navigate to a given screen.
fun navigateTo(screen: Screens)
}
 
Because AppNavigator is an interface, we cannot use constructor injection. To tell Hilt what implementation to use for an interface, you can use the @Binds annotation on a function inside a Hilt module.
AppNavigator가 인터페이스이기 때문에, 생성자를 사용해서 의존성 주입을 할 수 없다. 인터페이스에 사용할 구현체 Hilt에 알리려면 Hilt 모듈 내 함수에 @Binds 주석을 사용하면 됩니다.
 
@Binds must annotate an abstract function (since it's abstract, it doesn't contain any code and the class needs to be abstract too). The return type of the abstract function is the interface we want to provide an implementation for (i.e. AppNavigator). The implementation is specified by adding a unique parameter with the interface implementation type (i.e. AppNavigatorImpl).
@Binds 어노테이션은 추상 메소드를 반드시 알려야한다. (이 함수는 추상 함수이므로 코드를 포함하지 않고 클래스도 추상화되어야 함) 추상 메소드의 리턴타입은 우리가 제공하고 싶은 구현체의 인터페이스다. (예를 들어서 AppNavigator가 그렇다.) 구현체 인터페이스 구현 유형(예: AppNavigatorImpl)으로 고유한 매개변수를 추가하여 지정됩니다.
 
Can we add this function to the DatabaseModule class we created before, or do we need a new module? There are multiple reasons why we should create a new module:
새로운 모듈을 DatabaseModule 파일에 만드는게 아니라 새 파일을 만들어서 만드는게 나은데 그 이유는 다음과 같다.
 
  • For better organization, a module's name should convey the type of information it provides. For example, it wouldn't make sense to include navigation bindings in a module named DatabaseModule.
  • The DatabaseModule module is installed in the SingletonComponent, so the bindings are available in the application container. Our new navigation information (i.e. AppNavigator) needs information specific to the activity becauseAppNavigatorImpl has an Activity as a dependency. Therefore, it must be installed in the Activity container instead of the Application container, since that's where information about the Activity is available.
  • Hilt Modules cannot contain both non-static and abstract binding methods, so you cannot place @Binds and @Provides annotations in the same class.
 
Create a new file called NavigationModule.kt in the di folder. There, let's create a new abstract class called NavigationModule annotated with @Module and @InstallIn(ActivityComponent::class) as explained above:
 
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
 
@Binds
abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}
 
Inside the new module, we can add the binding for AppNavigator. It's an abstract function that returns the interface we're informing Hilt about (i.e. AppNavigator) and the parameter is the implementation of that interface (i.e. AppNavigatorImpl).
새로운 모듈안에서, AppNavigator를 새로 추가할 수있다. (추상메소드의 리턴타입으로 넣은 것을 말하는듯.) 이는 Hilt에 알려 주고 있는 인터페이스를 반환하는 추상 함수(예: AppNavigator)이며 매개변수는 인터페이스의 구현(예: AppNavigatorImpl)입니다.
 
Now we have to tell Hilt how to provide instances of AppNavigatorImpl. Since this class can be constructor injected, we can just annotate its constructor with @Inject.
이제 Hilt에게 AppNavigatorImpl객체를 어떻게 제공할지 알려줘야 한다. 이 구현체는 생성자 의존성 주입을 할 수 있으니까, 해당 생성자에 @Inject 어노테이션을 추가하면 된다.
 
Open the navigator/AppNavigatorImpl.kt file and do that:
위의 파일을 까서 아래코드처럼 생성자에 추가하면 된다.
class AppNavigatorImpl @Inject constructor(
private val activity: FragmentActivity
) : AppNavigator {
...
}
 
AppNavigatorImpl depends on a FragmentActivity. Because an AppNavigator instance is provided in the Activity container , FragmentActivity is already available as a predefined binding.
AppNavigatorImpl 구현체는 FragmentActivity에 의존한다. AppNavigator 객체는 Activity 컨테이너를 제공해주기 때문에, FragmentActivity는 이미 정의된 binding에서 사용하는게 가능하다.