[Android] 의존성 주입 도구 Hilt 뽀개기 : 어노테이션, 한정자, 스코프, 진입점

 

이번 글은 Hilt에 대해서 알아보자. 매번 안드로이드 프로젝트를 구현할때 의존성 주입 도구로 활용하고 있어서, 한번쯤 글로 정리하여 남겨두고자 한다.

 

 

의존성 주입

의존성

클래스가 다른 클래스의 기능, 객체를 필요로 하는 관계를 의미한다. 이러한 의존성을 주입 도구 (여기에서는 hilt)를 사용하면 코드의 결합도를 낮춰 유지보수성을 높이고, 테스트 용이성을 증가시킬 수 있다. 

 

근데 여기서 그냥 좋은거구나 써야겠네 라고 넘어가기엔 hilt의 러닝 커브가 좀 큰 편이며, 굳이 왜 의존성을 주입해야하는지 이해가 가지않는다. 다른 클래스의 객체가 필요하면 그냥 선언해서 사용하면 되는거 아닐까 ? 답은 아니다. 

 

 

 

의존성을 직접 관리하면 생기는 문제점

 

다음과 같이 MainActivity에서 MyRepository를 직접이용한다고 해보자. 

class MainActivity : AppCompatActivity() {
    private val repository = MyRepository()
}

 

1. 결합도가 높아진다

  • MainActivity가 MyRepository의 구체적인 구현을 직접 알고 있기 때문에, MyRepository를 바꾸려면 MainActivity도 수정해야 한다.
 
 

2. 테스트가 어렵다

  • MyRepository가 실제 데이터베이스를 사용한다면, 테스트에서 mock 데이터를 주입하기 어렵다

 

의존성을 직접 관리하는 경우 

class MyDatabase {
    fun queryData(): String {
        return "DB에서 불러온 데이터"
    }
}

class MyRepository {
    private val database = MyDatabase()  // 직접 생성 (문제 발생)
    
    fun fetchData(): String {
        return database.queryData()
    }
}

class MainActivity : AppCompatActivity() {
    private val repository = MyRepository()  // 직접 생성 (문제 발생)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val data = repository.fetchData()
        println(data)
    }
}

 

 

도구를 통해 의존성을 관리하는 경우 

class MyDatabase @Inject constructor() {
    fun queryData(): String {
        return "DB에서 불러온 데이터"
    }
}

class MyRepository @Inject constructor(
    private val database: MyDatabase  // 생성자 주입
) {
    fun fetchData(): String {
        return database.queryData()
    }
}

 

 

의존성 도구를 사용하는 경우, Hilt가 MyDatabase를 자동으로 주입해주면서 결합도는 낮아지고, Database 코드에 변화가 있더라도 내부 구현은 몰라도 되고, Repository의 코드는 변하지 않아도 된다. 

 

 

 

 

 


 

 

 

 

 

그렇다면 이제 hilt의 필요성을 알았으니 주요 어노테이션과 적용 방법을 알아보자. 

 

@HiltAndroidApp

역할

  • Hilt의 DI(Dependency Injection) 설정을 시작하는 어노테이션
  • Hilt는 Application 클래스를 기반으로 의존성 그래프를 생성한다
  • 이 어노테이션이 추가된 Application 클래스에서 Hilt의 전역적인 컨테이너가 설정되므로, 이걸 꼭 추가해야한다. 필수 ! 
@HiltAndroidApp
class MyApplication : Application()

내부적으로 하는 일

  1. Hilt가 자동으로 애플리케이션 전역 DI 컨테이너를 생성
  2. 모든 @Inject 및 @Module을 분석하여 의존성 그래프 구축

 

 

 

 


 

 

 

 @AndroidEntryPoint

역할

  • Activity, Fragment, Service, BroadcastReceiver, View 등에 Hilt로 의존성 주입을 허용하는 어노테이션이다
  • Hilt가 이 어노테이션이 추가된 컴포넌트에 필요한 객체를 자동으로 주입한다
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var myRepository: MyRepository
}

내부적으로 하는 일

  1. Hilt가 내부적으로 서브 컴포넌트를 생성하여 필요한 객체를 주입
  2. @Inject가 선언된 필드나 생성자를 보고 자동으로 의존성 주입
  3. @AndroidEntryPoint가 있는 클래스는 Hilt를 지원하는 기본 클래스에서만 상속 가능
    • Activity → AppCompatActivity(), ComponentActivity()
    • Fragment → androidx.fragment.app.Fragment
    • Service → android.app.Service
    • View → android.view.View
    • BroadcastReceiver → android.content.BroadcastReceiver

 

주의할 점

  • Fragment에 적용할 때, 프래그먼트는 액티비티에 종속적이므로, 해당 액티비티에도 @AndroidEntryPoint가 있어야 한다
  • ViewModel에는 @HiltViewModel을 사용해야 한다

 

 


 

 

 

@Inject

역할

  • 객체를 자동으로 주입하는 어노테이션
  • 생성자 주입, 필드 주입, 메서드 주입에 사용

생성자 주입 (Constructor Injection) 

class MyRepository @Inject constructor() {
    fun getData() = "Hello Hilt!"
}

 

 

@HiltViewModel
class MyViewModel @Inject constructor(
    private val repository: MyRepository
) : ViewModel()

필드 주입 (Field Injection)

class MainActivity : AppCompatActivity() {
    @Inject lateinit var myRepository: MyRepository
}

메서드 주입 (Method Injection)

class MyClass {
    lateinit var myRepository: MyRepository

    @Inject
    fun setRepository(repository: MyRepository) {
        myRepository = repository
    }
}

 

 

내부적으로 하는 일

  1. @Inject가 붙은 클래스는 Hilt가 자동으로 객체를 생성할 수 있도록 한다
  2. @Inject로 선언된 생성자/필드/메서드를 보고 의존성을 해결
  3. @Provides나 @Binds 없이도 Hilt가 자동으로 인스턴스를 제공한다

주의할 점

  • 인터페이스에는 @Inject를 사용할 수 없다. 아래의 @Binds 또는 @Provides를 사용해야 한다

 

 


 

 

@Module, @Provides, @Binds

역할

  • 직접 객체를 생성할 수 없는 경우(인터페이스, 외부 라이브러리 등) 의존성을 제공하는 방법이다
  • @Module을 통해 Hilt에게 어떤 객체를 제공할지 정의한다

@Provides

  • 클래스가 외부 라이브러리에서 제공되어 클래스를 소유하지 않은 경우 (Retrofit, OkHttpClient, Room DB 등)
  • 빌더 패턴으로 인스턴스를 생성해야 하는 경우
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    fun provideMyRepository(): MyRepository {
        return MyRepository()
    }
}

 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var myRepository: MyRepository
}

@Binds

  • 인터페이스를 구현한 클래스를 제공할 때 사용
interface MyRepository {
    fun getData(): String
}

class MyRepositoryImpl @Inject constructor() : MyRepository {
    override fun getData() = "Hello Hilt!"
}

@Module
@InstallIn(SingletonComponent::class)
abstract class AppModule {
    @Binds
    abstract fun bindMyRepository(
        myRepositoryImpl: MyRepositoryImpl
    ): MyRepository
}

 

@HiltViewModel
class MyViewModel @Inject constructor(
    private val repository: MyRepository
) : ViewModel()

 

 

 

 


 

 

 

@HiltViewModel

역할

  • ViewModel에서 Hilt로 의존성 주입을 할 수 있도록 설정하는 어노테이션

내부적으로 하는 일

  1. Hilt가 ViewModel의 생명주기에 맞춰 객체를 관리
  2. ViewModelScope에 맞게 생성 및 유지
  3. @AndroidEntryPoint가 선언된 Activity 또는 Fragment에서 ViewModel을 주입받을 수 있음
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModels()
}

 

 

 

 


 

 

 

 

 

Hilt에 미리 정의된 한정자 

Hilt는 몇 가지 미리 정의된 한정자를 제공한다. 예를 들어, 어플리케이션 / 액티비티의 Context가 필요할 수 있으므로, Hilt는 이에 따라 @ApplicationContext, @ActivityContext 를 제공한다. 

 

@ApplicationContext

  • 어디서든 사용할 수 있는 Application의 Context로, 앱의 전반적인 라이프사이클과 함께 살아있다
  • Activity가 사라져도 유지되어서 SharedPreferences, DataBase 등 앱이 종료되지 않는 한 유지되어야 하는 것들에 쓰인다. 

 

@ActivityContext

  • 현재 Activity의 Context로, Activity가 사라지면 함께 사라진다. 

 

 


 

 

 

Hilt의 Component Scope

기본적으로 Hilt의 모든 바인딩은 범위가 없다. 즉, 의존성을 주입할 때마다 새로운 인스턴스가 생성된다. 예를 들어, 아래 코드에서 AnalyticsAdapter는 ExampleActivity가 새로 생성될 때마다 새로운 인스턴스를 받는다. 

 
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
    
    @Inject lateinit var analytics: AnalyticsAdapter  // 주입될 때마다 새 인스턴스 생성됨
}

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

 

 

이때 Hilt는 특정 범위 안에서 같은 인스턴스를 재사용할 수 있게 해, 비용을 줄일 수 있다.

그 범위(scope)는 다음과 같이 공식문서에 나와있다. 

 

 

 

예시로 가장 자주 쓰이는 @Singletion 스코프에 대해 보자. Hilt에서 @Singleton 스코프를 적용하면 앱이 실행되는 동안 해당 객체가 한 번만 생성되고, 모든 곳에서 동일한 인스턴스를 공유할 수 있어 효율적이다. 

 

이렇게 아래와 같이 SharedPreference 처럼 앱 전역에서 쓰이고 동일한 인스턴스를 써도 무방한 경우, @Singleton을 통해 한번만 생성한다. 

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Singleton
    @Provides
    fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
        return context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
    }

    @Singleton
    @Provides
    fun providePreferenceManager(sharedPreferences: SharedPreferences): PreferenceManager {
        return PreferenceManager(sharedPreferences)
    }
}

 

 

 


 

 

 

Hilt가 지원하지 않는 클래스에 의존성 주입 

 

Hilt는 가장 일반적인 안드로이드 클래스를 지원한다. 하지만 Hilt가 지원하지 않는 클래스에서 필드 주입을 수행해야 할 수도 있다.

이러한 경우 @EntryPoint 어노테이션을 사용하여 진입점을 만들 수 있다. 

 

class ExampleContentProvider : ContentProvider() {

    override fun onCreate(): Boolean {
        val appContext = context?.applicationContext ?: throw IllegalStateException()

        // EntryPointAccessors를 사용하여 EntryPoint 인터페이스 가져오기
        val hiltEntryPoint = EntryPointAccessors.fromApplication(
            appContext, ExampleContentProviderEntryPoint::class.java
        )

        // Hilt에서 주입된 객체 가져오기
        val analyticsService = hiltEntryPoint.analyticsService()

        // analyticsService 사용
        analyticsService.logEvent("ContentProvider initialized")

        return true
    }

    ...
}

 

예를 들어, Hilt는 ContentProvider, BroadcastReceiver, WorkManager 등을 직접 지원하지 않는다.

 

ContentProvider가 위와 같이 Hilt를 사용하여 의존성을 가져오도록 하려면, 원하는 결합 유형마다 @EntryPoint로 주석이 지정된 인터페이스를 정의하고 한정자를 포함해야 한다. 그리고 @InstallIn을 추가하여 진입점을 설치할 구성요소를 지정해야 한다. 이때 진입점은 Hilt가 관리하는 코드와 그렇지 않은 코드 사이의 경계이다.

 

 

 

 

 

 

참고자료

 

 

Hilt를 사용한 종속 항목 삽입  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Hilt를 사용한 종속 항목 삽입 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Hilt는 프로젝트에서 종속

developer.android.com