brunch

You can make anything
by writing

C.S.Lewis

안드로이드 Hilt 딥 다이브

DI와 Dagger 그리고 Hilt 에 대해서

안녕하세요 저희는 서비스 개발팀에서 안드로이드 개발을 하고있는 두루와 스티븐입니다.

오늘은 Dagger와 Hilt에 대해 깊게 조사해본 것을 소개하겠습니다.


Hilt는 프로젝트에서 의존성 주입을 실행하는 상용구를 줄이는 Android용 의존성 주입 라이브러리입니다. Hilt를 들어가기 전 의존성 주입(DI)를 사용하는 이유와 힐트의 기본형인 Dagger에 대해 알아보겠습니다.

의존성 주입이란?

특정 객체의 인스턴스가 필요한 경우 이를 직접 생성하지 않고

외부에서 생성된 객체를 전달하는 기법입니다.

각 객체는 다른 객체의 생성에는 관여하지 않고 객체를 필요로 하는 부분과

독립된 별도의 모듈이 객체의 생성과 주입을 전달합니다.


예를들어서 Engine을 사용하는 Car 클래스가 있을경우

Car 클래스는 Engine 클래스에 의존하고 있습니다.

즉 Engine 클래스의 생성자가 변경되거나 파생 클래스가 생긴다면 Car 클래스또한 수정해야 합니다.

그림으로 본다면 Car 클래스와 Engine 클래스는 밀접하게 연결되어 있습니다.

의존성 주입을 사용한다면 어떻게 변하는지 보겠습니다.

main함수에서 Engine 인스턴스를 생성한 후 이를 사용하여 Car 인스턴스를 구성합니다.

이제 Car 클래스는 Engine 클래스의 생성자가 변경되거나 파생클래스를 사용할 경우

Engine 인스턴스를 만들어주는 부분만 수정하고 Car 인스턴스에 주입시켜주면 되기때문에

Car 클래스와 Engine 클래스는 밀접하게 연결되어있지 않습니다.

그림으로 본다면 Car 클래스와 Engine 클래스는 밀접하게 연결되어있지 않습니다.

의존성 주입의 장점

목적에 맞게 동작을 변경하기 쉽습니다. 
의존성 주입을 사용하면 특정 객체를 필요한 객체를 외부에서 전달받으므로
이를 조작하면 필요에 따라 다른 동작을 하는 객체를 간편하게 생성할 수 있습니다.


생성한 객체를 쉽게 재사용할 수 있습니다. 
의존성 주입을 사용하면 객체를 생성하는 작업을 특정 모듈에서 전담하게 되므로
객체를 생성하는 방법과 이의 인스턴스를 효율적으로 관리할 수 있습니다.
위의 코드는 main문에서 Engine 인스턴스를 만들고 주입했지만
특정 모듈에서 어떤 인스턴스를 만들어 주입한다면 다양한 곳에서 쉽게 해당 객체를 사용 가능합니다.


객체를 생성하거나 사용할 때 발생할 수 있는 실수를 줄여줍니다.
같은 역할을 하는 객체를 각각 다른 곳에서 별도로 생성하도록 코드를 작성하는 경우
해당 객체를 생성하는 모든 부분의 코드를 수정해야 하므로 작업이 복잡해지고 실수를 하기도 쉽습니다, 반면에. 의존성 주입을 사용하면 객체를 생성해주는 부분 한 곳만 변경하면 되므로 수정이 간편합니다.
또한, 해당 객체를 사용하는 모든 부분에 변경 결과가 일괄적으로 적용되므로
변경할 부분을 누락하는 실수를 원천 차단할 수 있습니다.


대거(Dagger)

대거는 스퀘어에서 만든 최초의 라이브러리로, 자바 기반 프로젝트에서 의존성을 주입할 수 있게 도와줍니다.

대거 라이브러리에서 각 객체간 의존 관계는 어노테이션을 사용하여 정의합니다.
이렇게 정의한 의존 관계는 대거 라이브러리 내의 어노테이션 프로세서를 통해
문제가 없는지 분석 절차를 걸쳐 문제가 없다면 각 객체를 생성하는 코드를 만들어줍니다.

이처럼 의존관계를 검증하는 과정과 필요한 코드를 생성하는 과정이 모두 빌드 단계에서 일어나므로
문제가 있으면 빌드 단계에 검출이 되므로 더 견고한 애플리케이션을 만들 수 있습니다.

이번에는 대거를 구성하는 핵심 요소들을 알아보겠습니다.


모듈

모듈은 필요한 객체를 제공하는 역할을 합니다.

모듈은 클래스 단위로 구성되며 이 클래스 내에 특정 객체를 반환하는 함수를 정의함으로써

모듈에서 제공하는 객체를 정의할 수 있습니다.

대거 라이브러리에서 모듈 클래스로 인식되게 하려면 @Module 어노테이션을 클래스에 추가해야하며

이 모듈에서 제공하는 객체를 정의한 함수에는 @Provides 어노테이션을 추가해야 합니다.

간단한 예를 통해 모듈을 정의하는 방법을 알아보겠습니다.

모듈에는 제공할 객체의 종류와 각 객체를 생성하는 코드를 작성합니다.

특정 객체를 생성할 때 다른 객체가 필요한 경우, 즉 의존 관계에 있는 객체가 있는 경우

객체를 생성하는 함수의 매개변수로 의존 관계에 있는 객체를 추가합니다.


컴포넌트

모듈이 객체를 제공하는 역할을 했다면

컴포넌트는 모듈에서 제공받은 객체를 조합하여 필요한 곳에 주입하는 역할을 합니다.

하나의 컴포넌트는 여러개의 모듈을 조합할 수 있습니다. 

따라서 목적에 따라 각각 분리된 여러 모듈로부터 필요한 객체를 받아 사용할 수 있습니다.

대거의 컴포넌트는 @Component 어노테이션을 붙인 인터페이스로 선언하며

이 어노테이션의 modules 프로퍼티를 통해 컴포넌트에 객체를 제공하는 모듈을 지정할 수 있습니다.

컴포넌트를 통해 객체를 전달받을 대상은 모듈과 유사하게 인터페이스 내 함수로 정의하며

아무런 값을 반환하지 않고 객체를 전달받을 대상을 매개변수로 받는 형태로 정의합니다.

BurgerModule을 제공하는 컴포넌트인 FastFoodComponent의 예입니다.

@Component 어노테이션 내 modules 프로퍼티로 BurgerModule을 지정하고

Store 클래스에서 FastFoodComponent에서 제공하는 객체를 주입할 수 있도록

정의한 모습을 확인할 수 있습니다.

컴포넌트와 모듈, 그리고 각 모듈에서 제공하는 객체 간의 의존 관계는

그래프로 표시할 수 있으며, 이를 객체 그래프라고 부릅니다.

컴포넌트를 통해 객체를 주입하는 항목은 @Inject 어노테이션으로 표시합니다.

컴포넌트가 값을 주입하는 시점에 객체가 할당되므로

값을 주입받는 프로퍼티는 lateinit var로 선언해야 합니다.

컴포넌트에서, 객체를 주입받는 클래스를 정의한 후 프로젝트를 빌드하면 대거는 객체를 주입할 때

사용할 수 있는 컴포넌트의 코드를 생성해줍니다.

대거가 생성해 주는 컴포넌트의 클래스 이름은 Dagger{컴포넌트 이름} 규칙을 따릅니다.

힐트

힐트는 Dagger를 기반으로 빌드되었으며 다음과 같은 목적을 가집니다.   

보일러 플레이트 코드 감소 (객체 생성 방법과 주입 위치만 정의함)

분리된 빌드 의존성

환경 설정의 간소화

개선된 테스트 환경

표준화된 컴포넌트(안드로이드 클래스)

힐트는 대거 컴포넌트와 코드를 생성하여

Activity 및 Fragment와 같은 안드로이드 클래스에 필요한 의존성을 자동으로 주입합니다.

Hilt는 전이 클래스 경로를 기반으로 표준 안드로이드 대거 컴포넌트 세트를 생성합니다.

이를 위해서는 대거 모듈에 힐트 어노테이션(@InstallIn)을 표시하여

어떤 컴포넌트에 포함시켜야하는지 Hilt에게 알려줘야 합니다.

안드로이드 프레임 워크 클래스에서 주입을 받기 위해서는

또 다른 힐트 어노테이션(@AndroidEntryPoint) 을 사용합니다.

이는 대거 주입 코드를 가지고 있는 base 클래스를 생성하고 상속할 수 있도록 합니다.

Hilt Module

Provides

클래스가 외부 라이브러리에서 제공되므로 생성자 삽입이 불가능한 경우(Retrofit, Room)

@Provides 어노테이션을 이용해 인스턴스 삽입을 할 수 있습니다.

그렇다면 이 인스턴스 삽입이 어떻게 이루어지는지 알아보겠습니다.

위 코드는 저희 안드로이드 온보딩에 현재 사용중인 코드입니다.

moshi (json converter)와 Retrofit객체의 경우 빌더패턴으로 사용되므로

@Provides 어노테이션을 이용해 인스턴스 삽입을 하고 있습니다.

내부 코드를 살펴보면 팩토리 객체가 생성되는 생성자에서 현재 모듈 정보를 받고

moshi를 제공하는 함수가 생성됩니다.

현재 코드는 Retrofit이 생성되고 제공하는 코드입니다.

생성자에서 moshi를 제공받아 Retrofit 팩토리를 만들고

retrofit을 제공하는 함수가 생성됩니다.


Binds

인터페이스는 생성자 삽입이 불가능합니다.
대신 Hilt 모듈 내에 @Binds로 주석이 지정된 추상 함수를 생성하여 결합 정보를 제공합니다.

@Binds 주석은 인터페이스의 인스턴스를 제공해야 할 때 사용할 구현을 Hilt에게 알려줍니다.

Binds는 위의 Provides와는 달리 추가적인 코드가 생성되지 않는다는 장점이 존재합니다.


Hilt Application

Hilt를 사용하는 모든 앱은 @HiltAndroidApp이 달린 Application 클래스를 포함해야 합니다.

@HiltAndroidApp은 Hilt 컴포넌트의 코드 생성과 컴포넌트를 사용하는 Application의 기본 클래스를

생성하게됩니다. 코드 생성에는 모든 모듈에 대한 엑세스 권한이 필요하므로 Application 클래스를

컴파일하는 대상에는 전의 의존성에 모든 Dagger 모듈이 있어야 합니다.

전이 의존성이란
어떤 라이브러리를 추가하면 그 라이브러리의 의존성도 함께 의존하게 되는데
이를 전의 의존성이라고 합니다.

@AndroidEntryPoint가 달린 안드로이드 프레임워크 클래스와 마찬가지로

Application에도 멤버 주입이 됩니다.

이는 super.onCreate()가 호출된 후 Application의 필드에 의존성 주입이 이루어지는 것을 의미합니다. (생명주기에 맞게 대거를 사용한다면 Application의 onCreate에 의존성을 주입해야 합니다.)

예를들어 일반적인 Dagger 사용시 MyApplication이 MyBaseApplication을 상속하는 구조이면서

멤버 변수로 Bar를 가지고 있다고 가정해보겠습니다.

대거를 사용한 경우

힐트를 사용하면 다음과 같이 멤버 주입이 됩니다.

@AndroidEntryPoint

Application에서 멤버 주입이 가능하게 설정하고 나면, 다른 안드로이드 클래스들에도

@AndroidEntryPoint 어노테이션을 사용하여 멤버 주입을 하는것이 가능해집니다.

@AndroidEntryPoint를 사용할 수 있는 타입은 다음과 같습니다.   

Activity

Fragment

View

Service

BroadcastReceiver

힐트와 ViewModel의 사용은 직접적으로 지원하지 않는 대신에 Jetpack Extension을 통해 지원합니다. 다음 예제는 어떻게 Activity에 어노테이션을 추가 할 수 있는지 보여줍니다.

다른 타입의 경우도 Activity와 동일한 방법으로 지원됩니다.

Activity에서 멤버 주입을 하기 위해서는 @AndroidEntryPoint를 추가 합니다.

실제 DetailFragment의 생성된 코드, Fragment의 onAttach에서 의존성들이 주입되고있습니다.


@InstallIn

힐트의 모듈은 표준 대거 모듈로 @InstallIn이라는 추가적인 어노테이션을 갖습니다.

@InstallIn은 Hilt의 표준 컴포넌트들 중 어떤 컴포넌트에 모듈을 설치할지 결정합니다.

Hilt 컴포넌트가 생성될 때 모듈들은 추가된

@InstallIn과 함께 알맞은 컴포넌트 또는 서브 컴포넌트에 설치됩니다.

대거와 같이 컴포넌트에 모듈을 설치하면 해당 모듈에 바인딩된 의존성은

컴포넌트 내 다른 바인딩 또는 다른 하위 컴포넌트의 바인딩이 접근하는 것을 허용합니다.

바인딩 된 의존성에 @AndroidEntryPoint 클래스가 접근하는 것 또한 가능합니다.

해당 컴포넌트에 대한 바인딩 스코프를 지정할 수도 있습니다.


@InstallIn 사용하기

모듈에서 @InstallIn 어노테이션을 추가하는 것으로 Hilt 컴포넌트에 모듈이 설치됩니다.

Hilt를 사용할 때 대거 모듈 상에 이러한 @InstallIn 어노테이션은 필수지만

이 검사는 선택적으로 비 활성화 할 수 있습니다.

@InstallIn 어노테이션에 어떤 컴포넌트가 모듈이 설치될 적당한 Hilt 컴포넌트인지 명시해야합니다.

예를 들면 애플리케이션 스코프에서 어떤 바인딩이든 사용할 수 있게 모듈을 설치하려면

SingletonComponent를 사용해야 합니다.


컴포넌트 계층

기존 사용하던 대거와 다르게 힐트 사용자는

대거 컴포넌트를 직접 정의하거나 인스턴스화 할 필요가 없어졌습니다.

대신에 힐트는 이미 정의된 컴포넌트를 통해 생성되는 클래스들을 제공하고 있습니다.

Hilt는 안드로이드 Application의 다양한 생명주기에 자동으로 통합되는 내장 컴포넌트 세트

해당 스코프 어노테이션과 함께 제공합니다.

다이어그램은 표준 Hilt 계층을 보여주고 있습니다.

각 컴포넌트 위에 달린 어노테이션은 컴포넌트 바인딩의 생명주기를 지정하는 데 사용됩니다.

각 컴포넌트 아래에 있는 화살표는 하위 컴포넌트를 가르키고 있습니다.

보통 하위 컴포넌트의 바인딩은 상위 컴포넌트의 바인딩이 가지고 있는 의존성들을 가질 수 있습니다.

@InstallIn이 달린 모듈의 바인딩에 스코프가 지정될 때 반드시 모듈이 설치되는
컴포넌트의 스코프와 일치해야 합니다. 
예를들면 @InstallIn(ActivityComponent.class) 모듈은
@ActivityScope만 사용할 수 있습니다.

@HiltAndroidApp 어노테이션으로 정의한 Application클래스가 있는

gradle 모듈에 해당 트리가 생성됩니다.

그리고 해당 App모듈에서 관계도의 아래에 해당하는 Module들이 생성되는 방법을 알고 있습니다.

di 모듈, ViewModel이나 Fragment의 injector 정보를 알고있습니다.


구성요소 계층 구조

구성요소에 모듈을 설치하면 이 구성요소의 다른 결합 또는 구성요소 계층 구조에서

그 아래에 있는 하위 구성요소의 다른 결합의 종속 항목으로 설치된 모듈의 결합에 엑세스할 수 있습니다.


컴포넌트 멤버 주입

앞에서 다룬 @AndroidEntryPoint 섹션에서는 안드로이드 클래스가 멤버 주입을 하는 방법을 다뤘습니다. Hilt 컴포넌트들은 각각 안드로이드 클래스에 맞는 의존성을 주입을 해야 할 의무가 있습니다.

다음 표는 안드로이드 클래스에 적합한 Hilt 컴포넌트를 보여줍니다.


컴포넌트의 수명

컴포넌트의 수명은 다음 두가지 관점에서 볼 때 바인딩의 수명과 관련되기 때문에 중요합니다.   

컴포넌트가 생성되고 종료될 때, 해당 스코프 어노테이션이 지정된 바인딩 또한 수명을 함께합니다.

컴포넌트 수명은 멤버 주입된 값들이 사용할 수 있는 시기를 나타냅니다.

컴포넌트의 수명은 일반적으로 안드로이드 클래스에 대응하는 인스턴스 생명과 소멸을 따라갑니다.

다음 표는 스코프 어노테이션과 각 컴포넌트에 맞는 수명을 목록으로 보여주고 있습니다.

스코프 바인딩 vs 비 스코프 바인딩

기본적으로 모든 대거의 바인딩은 스코프 어노테이션이 없는 비 스코프 바인딩입니다.

이는 각 바인딩이 요청될 때마다 대거는 새로운 인스턴스를 생성하는 것을 의미 합니다.

그러나 대거는 컴포넌트에 스코프 어노테이션을 지정할 수 있습니다.

스코프가 지정된 컴포넌트에서 해당 스코프 바인딩은 컴포넌트 인스턴스당 한번만 생성되고

해당 바인딩에 대한 모든 요청에 동일한 인스턴스 제공

일반적으로 오해하는 부분이 @FragmentScoped가 지정된 바인딩이
모든 Fragment 인스턴스에 대해 동일한 바인딩 인스턴스를 공유할 것이라고 생각하는 점입니다.
하지만 실제로 그렇지는 않고 각 Fragment 인스턴스는
새로운 Fragment 컴포넌트 인스턴스를 얻기 때문에 각기 다른 Fragment 인스턴스는
각자만의 스코프 된 바인딩을 얻게 됩니다.

모듈에서 스코프 어노테이션 사용하기

모듈에서 바인딩에 대해 비슷한 방법으로 스코프 어노테이션을 사용 가능합니다.

스코프 어노테이션이 지정된 바인딩 선언만 해당 컴포넌트와 수명을 함께하여
각 바인딩 요청들에 대해 동일한 인스턴스를 제공합니다.


스코프 어노테이션은 언제 사용할까?

바인딩에 대해 스코프 어노테이션을 지정하는 것은 코드 생성 크기 그리고 런타임 성능에 영향을 미치므로

가능한 스코프 어노테이션을 조금만 사용하는 것이 좋습니다.

바인딩에 대해 스코프를 어노테이션을 사용해야 하는지 결정하는 일반적인 규칙은

동일한 인스턴스를 보장해야 할 만큼 코드의 정확성이 필요한 경우

성능상의 이유로만 스코프 어노테이션을 사용해야 한다면,먼저 성능이 문제인지 확인한 뒤

표준 Hilt 컴포넌트 스코프 어노테이션 대신 @Reusable을 사용하는 것이 좋습니다.   

@Resuable ← 이미 생성한 인스턴스가 존재하면 싱글톤처럼 재 사용하고, 없을 경우 인스턴스를 새로 생성해서 사용. Non-Scope 이기 때문에, GC에 의해서 메모리에서 해제될 수도 있으며, @Singleton 처럼 항상 같은 인스턴스를 제공한다는 보장이 없다.


컴포넌트가 제공하는 기본 바인딩

각 Hilt 컴포넌트는 기본 바인딩 세트와 함께 사용자가 정의한 바인딩들을 의존성 주입하게 됩니다.


Entry point란?

Entry point(진입점)은 Dagger를 사용하여 의존성 주입을 할 수 없는 코드에서 제공된

Dagger 객체를 얻을 수 있는 방법. Dagger가 관리하는 오브젝트 그래프에 코드가 처음 들어가는 지점

만약 Dagger 컴포넌트에 익숙하다면

Entry point는 Hilt가 상속하여 생성할 컴포넌트의 인터페이스입니다.


Entry point는 언제 필요할까?

Dagger를 적용하지 않는 라이브러리를 인터페이싱하거나 Hilt에서 지원하지 않는 안드로이드 구성요소가 Dagger 객체에 접근이 필요할 때 Entry point가 필요하다.

일반적으로, 대부분의 entry point는 Activity, Fragment와 같은 안드로이드 플랫폼

인스턴스화 하는 곳에서 필요하다. @AndroidEntryPoint는 Entry point들을 다루기 위해

특화된 도구이며 안드로이드 클래스의 Entry point에 접근할 수 있도록 한다.


Entry point, 어떻게 사용하나?

EntryPoint 생성하기

Entry point를 생성하기 위해서는 각 바인딩 타입에 대한 접근 가능한 메서드를 사용하여

인터페이스를 정의 @EntryPoint 어노테이션을 추가해야 한다.

그런 다음 @InstallIn을 추가하여 Entry point가 설치될 컴포넌트 지정


EntryPoint에 접근하기

Entry point에 접근하기 위해서는 EntryPoints클래스를 생성하여 컴포넌트 인스턴스를

매개변수로 전달하거나 컴포넌트 홀더 역할을 하는 @AndroidEntryPoint 객체를 전달한다.

매개변수로 전달하는 컴포넌트가 @EntryPoint 인터페이스에 추가되어 있는

@InstallIn 어노테이션이 전달하는 컴포넌트와 일치하는지 확인해야합니다.

앞에서 정의한 Entry point 인터페이스를 사용하는 방법은 다음과 같습니다.


Dagger-Hilt Keyword
    Inject
        의존성 주입을 요청하는 부분으로, Inject Annotation 으로 주입을 요청하면
        연결된 Component가 Module로부터 객체를 생성하여 건네준다.
    Component
        연결된 Module을 이용하여 의존성 객체를 생성하고
        Inject로 요청받은 인스턴스에 생성한 객체를 주입한다.
        의존성 주입 요청을 받고 주입하는 주된 역활을 한다.
    SubComponent
        Component는 계층관계를 만들 수 있으며, SubComponent는
        Inner Class 방식의 하위 계층 Component 이며
        SubComponent는 그래프를 생성한다. Inject로 주입을 요청 받으면
        SubComponent 에서 먼저 의존성을 검색하고, 없으면 부모로 올라가면서 검색한다.
    Module
        Component 에 연결되어 의존성 객체를 생성하며, 생성 후 Scope에 따라서 관리 또한 하게 된다.
    Scope
        생성된 객체의 Lifecycle 의 범위이며, Activity, Fragment 등등의 생명주기에 맞추어 사용한다.
        Module 에서 Scope를 보면서 객체를 관리한다.

Dagger의 의존성을 주입하는 Flow
    @Inject → Sub Component → Module → Scope에 존재하면 return, 없을 경우 생성
    Sub Component 에서 맞는 타입이 없을 경우 상위 Component → Module → Scope에 있으면
    return, 없을 경우 생성


여기까지 Hilt Deep Dive에 대한 소개를 마치겠습니다.

내년에는 더욱 성장하는 한 해가 되었으면 좋겠습니다.

참조

android hilt : https://developer.android.com/training/dependency-injection/hilt-android?hl=ko

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari