brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Jul 22. 2024

안드로이드 Hilt

Android Jetpack

Hilt는 의존성 주입 라이브러리이다. 그래서 Hilt에 대해서 알고 싶다면 먼저 의존성 주입이라는 개념을 알아야 한다.


의존성 주입

의존성 주입(Dependency Injection, DI)은 객체가 필요로 하는 의존성을 외부에서 제공하는 디자인 패턴이다. 이를 통해 클래스 간의 결합도를 낮추고, 유닛 테스트를 용이하게 하며, 코드의 재사용성과 확장성을 높일 수 있다.


하나의 클래스가 다른 클래스를 참조할 필요가 있을 때 의존성이 생긴다. 예를 들어 A클래스에서 B클래스의 인스턴스를 필요로 한다면 A클래스는 B클래스에 의존성이 생기는 것이다. 이때 A클래스는 B클래스에 의존한다고 한다. 이러한 의존성은 여러 가지 방법으로 제공할 수가 있다. 가장 간단한 방법으로는 A클래스 내에서 B클래스의 인스턴스를 생성하는 것이다. 코드로 나타내면 다음과 같다.

class A {
    private val b = B()
}

이런 구조에서 문제점은 무엇일까? A클래스를 UserService 클래스로 바꾸고 B클래스를 UserRepository 클래스로 바꿔보자.

이때 UserRepository는 서버 1에서만 User 데이터를 가져온다고 가정하자. 그러면 UserService 객체는 서버 1에서만 데이터를 가져오는 책임을 가진다. 만약 서버 2에서만 User 데이터를 가져오는 책임을 가지는 UserService 객체를 만들려면 어떻게 해야 할까? UserService 클래스가 서버 2에서만 User 데이터를 가져오는 새로운 클래스에 의존하는 것이다. 새로운 클래스를 UserRepository2라고 하자. 그러면 다음과 같이 된다.

이번에는 무슨 문제가 있는가? UserService 클래스는 서버 1에서만 User 데이터를 가져오는 기능을 잃었다. 결국 서버 2에서만 User 데이터를 가져오는 새로운 클래스를 만들어야 한다. 그 클래스를 UserService2라고 하면 다음과 같이 된다.

최종적으로 서버 1과 서버 2에서 각각 데이터를 가져오는 UserService 클래스와 UserService2 클래스가 만들어진다. 하지만 두 클래스를 자세히 보면 중복코드가 있다. 재사용하고 싶은 욕망이 들끓지 않는가? 이때 의존성을 주입해 주면 된다. 용어 자체는 거창하지만 사실 말 그대로 의존성 주입, 즉 여기서는 UserService 클래스에 UserRepository 클래스를 주입하면 되는 것이다. 방법은 간단하게 UserService 클래스 생성자에 UserRepository 인스턴스를 전달하면 된다. (생성자 말고 필드로 주입하는 방법도 있다.)

그런데 문제가 있다. 생성자로 넘겨주는 데이터 타입이 UserRepository이기 때문에 UserRepository2 인스턴스는 넘겨줄 수가 없다. 이럴 때 추상화를 사용하면 된다. UserRepository를 인터페이스로 만들고 해당 인터페이스의 구현체로 UserRepository1Impl과 UserRepository2Impl를 만들어서 각각 서버 1과 서버 2에 접속하도록 하면 된다. 예시 코드는 실제 서버에 접속할 수 없으니 각각의 서버에서 데이터를 가져온 척하는 문자열을 반환하도록 하였다.

이렇게 하면 UserService 클래스를 그대로 사용하면서 필요한 의존성을 주입받을 수 있다. 서버 1에 접속하든 서버 2에 접속하든 UserService 클래스에는 별도의 추가 또는 수정 사항이 발생하지 않는다. 요란했던 의존성 주입의 끝이다. 이렇게 직접 의존성 주입을 하는 것을 수동 DI라고 한다.


Hilt란?

Hilt는 안드로이드 앱의 의존성 주입을 간소화해주는 라이브러리이다. 의존성 주입 라이브러리인 Dagger를 기반으로 만들어졌다. Dagger를 기반으로 안드로이드에서 더 쉽게 사용할 수 있도록 만든 라이브러리가 Hilt라고 보면 된다. Hilt는 안드로이드 컴포넌트의 생명주기를 관리한다. Hilt와 Dagger 모두 컴파일 타임에 의존성 주입을 하기 때문에 런타임에 발생할 오류를 최소화한다.


기본 사용 방법

앞선 예제를 조금 수정한 새로운 예제를 기반으로 한다. ViewModelFactory를 사용하지 않고 간단히 ViewModel에 의존성 주입을 하는 내용을 포함한다.


1) 플러그인 추가

프로젝트 수준의 build.gradle에 아래 플러그인을 추가한다.

id("com.google.dagger.hilt.android") version "2.48.1" apply false

app 수준의 build.gradle에는 다음 플러그인을 추가한다.

plugins {
    ...
    id("kotlin-kapt")
    id("com.google.dagger.hilt.android")
}


2) 라이브러리 의존성 추가

dependencies {
    ...
    implementation("com.google.dagger:hilt-android:2.48.1")
    kapt("com.google.dagger:hilt-compiler:2.48.1")
}

만약 테스트 코드를 작성할 때 필요하다면 별도로 다음 의존성을 추가해야 한다.

testImplementation("com.google.dagger:hilt-android-testing:2.48.1")
kaptTest("com.google.dagger:hilt-android-compiler:2.48.1")
androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1")
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48.1")


3) 애플리케이션 수준의 종속성 컨테이너 설정

MyApplication 클래스를 만들고 Application을 상속받는다. 해당 애플리케이션 클래스에 @HiltAndroidApp 애너테이션을 지정한다. 그러면 해당 애플리케이션 클래스는 Hilt의 컴포넌트들을 애플리케이션의 생명주기에 맞춰 관리하는 루트 컴포넌트 역할을 한다.

AndroidManifest.xml에 아래와 같이 이름을 추가하는 것도 잊지 말자.

<application
    ...
    android:name=".MyApplication"
</application>

Hilt는 다음 안드로이드 컴포넌트에 의존성 주입을 지원한다. 애너테이션도 함께 표시했다.

Application: @HiltAndroidApp

ViewModel: @HiltViewModel

Activity: @AndroidEntryPoint

Fragment: @AndroidEntryPoint

View: @AndroidEntryPoint

Service: @AndroidEntryPoint

BroadcastReceiver: @AndroidEntryPoint

애플리케이션 클래스에서 Hilt를 설정했기 때문에 @AndroidEntryPoint 애너테이션을 설정한 안드로이드 컴포넌트에 의존성 주입을 할 수 있다. 그러면 다음으로 의존성 주입 대상에 애너테이션을 설정할 차례다.


4) UserRepository

UserRepository는 인터페이스다. 인터페이스는 직접 인스턴스를 생성할 수 없기 때문에 별도의 모듈을 만들어서 Hilt에게 인터페이스의 구현체를 어떻게 생성하고 제공해야 하는지 알려줘야 한다.

@Qualifier는 사용자가 직접 정의한 애너테이션을 생성하여 종속성을 구별할 때 사용한다. @InstallIn(SingletonComponent::class)은 UserRepositoryModule이 애플리케이션의 생명주기와 동일한 싱글톤 컴포넌트에 설치되어야 한다는 것을 나타낸다. @Module은 이 객체가 Hilt에게 의존성을 제공하는 모듈이라는 것을 알려준다. 모듈은 의존성을 제공하는 메서드의 집합이다.

@Module: Hilt에게 의존성을 제공하는 클래스를 정의한다. 이 클래스 내에서 @Provides 또는 @Binds 애너테이션을 사용하여 의존성을 제공하는 메서드를 정의할 수 있다. @Module 애너테이션이 붙은 클래스는 Hilt가 의존성을 관리할 수 있도록 정보를 제공한다.

@InstallIn: @Module이 적용된 클래스에 어떤 Hilt 컴포넌트의 생명주기에 설치될 것인지 지정한다.

@Singleton: 애플리케이션의 생명주기 동안 단 하나의 인스턴스만 생성되어 사용된다. 싱글톤으로 애플리케이션 전체에서 공유된다.

@Binds: 추상 메서드에 사용되며, 인터페이스나 추상 클래스의 구현체를 Hilt에게 알려주는 데 사용된다.

@Provides: 구체적인 인스턴스 생성 로직을 포함하는 메서드에 사용된다.

@Qualifier: 동일한 타입의 여러 의존성 중에서 특정 의존성을 주입할 때 사용한다.


5) UserViewModel

@HiltViewModel은 ViewModel의 시작점을 Hilt에게 알린다. @Inject constructor는 클래스의 생성자에 @Inject 애너테이션을 사용하여 Hilt가 이 클래스의 인스턴스를 생성할 때 필요한 의존성을 자동으로 주입하도록 한다. 현재 의존성 주입 대상은 UserRepository다. UserRepository는 인터페이스이고 앞서 모듈을 만들었기 때문에 Hilt가 구현체를 생성하는 법과 어떤 구현체를 제공해야 하는지 알고 있다. 따라서 자동으로 적절한 의존성 주입이 이뤄진다.


6) UserService

UserViewModel과 차이점은 ViewModel이 아니라는 것이다. 의존성 주입받는 구현체도 다르다.


7) MainActivity

@AndroidEntryPoint 애너테이션은 액티비티가 시작하는 지점을 Hilt에게 알려주는 역할이다. 이제 Hilt가 해당 액티비티에 의존성을 주입할 수 있다.

@Inject는 Hilt에 의해 관리되는 의존성을 이 클래스에 주입하도록 한다. 이렇게 @Inject를 통해 Hilt에서 삽입하는 필드는 private가 될 수 없다.


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