우선 의존성이 무엇인지 알아볼게요
class Engine(){
fun start(){...}
}
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
이처럼 하나의 클래스가 다른 클래스를 사용하는 경우 ,하나의 모듈이 다른 모듈을 사용하는 경우
우리는 의존성이 생긴다고 합니다.
이런 상황에 우리는 Car가 Engine 를 의존한다고 합니다.
Activity 는 viewmodel 을 사용하고
viewmodel 은 repository 를 사용하고 이런 것들 모두 의존성에 해당합니다
이러한 의존성은 다음과 같은 단점을 가집니다.
내부가 아니라 외부에 객체를 생성하여 주입하는 디자인패턴을 의미합니다.
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
class Car(
private val engine: Engine
) {
fun start() {
engine.start()
}
}
Car 생성자는 Engine을 매개변수로 받습니다.
class Car() {
lateinit var engine :Engine
fun start() {
engine.start()
}
}
Car 객체에서 engine이라는 Filed 를 만들어서 이를 주입 받는 방법
권장 되는 방법은 아니예요. - 초기화 안되면 null pointer exception 오류
val car1 = Car(engine = EngineA())
val car2 = Car(engine = EngineB())... Factory 패턴을 사용 각 뷰모델에 맞게 사용할 객체를 생성하고 전달.
class ViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SignUpViewModel::class.java)) {
val repository = AuthRepositoryImpl(ServicePool.authService)
val signUpUseCase = SignUpUseCase(repository)
return SignUpViewModel(signUpUseCase) as T
} else if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
val repository = AuthRepositoryImpl(ServicePool.authService)
val loginUseCase = LogInUseCase(repository)
return LoginViewModel(loginUseCase) as T
} else if{
...
}
}
}
의존성의 변화가 생길때마다 코드를 수정해 주어야겠죠(보일러 플레이트)
또한 기능이 늘어날 수록 ViewModelFactory를 직접 구현해주면서 코드를 작성이 복잡해 지기도 하구요.
Hilt는 의존성 주입을 편리하게 만들어주는 라이브러리로, 기존의 의존성 주입에서 발생하는 보일러플레이트 코드를 줄여줍니다. 여러 주석(annotation) 기반의 설정을 통해 간편하게 의존성을 주입할 수 있도록 도와줍니다.

제가 처음에 생각했던 그림은 다음과 같았아요. 사용하게 선언해주는 부분이 있고
이를 제공해주는 쪽 , 제공 받는쪽으로 나뉜다라는 개념으로 인식을 했습니다.

정확히는 이렇게 이루어져요
전체적인 느낌은 위 그림 처럼 HiltAndroidApp 을 통해 생성하고 , Module, InstallIn, Provides, Binds 를 통해 AndroidEntryPoint , HlitVeiwModel 에 주입 된다 라는 느낌으로 이해하시면 될 거같습니다.

@HiltAndroidApp 를 통해 Hilt code 를 생성해야합니다. application class 에 할당을 해주어야
힐트를 사용하는 안드로이드 앱이라는 것을 명시해서 라이브러리가 인식할 수 있어요

출처: 드로이드 나이츠 2020


@AndroidEntryPoint 주석을 통해 안드로이드 클래스에 hilt에서 DI를 받을 것이라는 것(DI contanier) 을 명시해 줄 수 있어요.
사용처: Activity, Fragment, Service, BroadcastReceiver 등
@HiltViewModel 은 viewmodel에 의존성 주입

@HiltViewModel
class MyViewModel @Inject constructor(
private val repository: MyRepository
) : ViewModel() {
// ViewModel 구현
}
이처럼 MyViewModel 의 경우 @HiltViewModel 를 통해 hilt 에게 의존성 주입을 받는 viewmodel 이라는 것을 명시해 줍니다.
@Inject주석은 Depndency Graph 를 이어주는 역할을 해요

이 부분에서는 @Inject 주석을 통해 repository를 hilt를 통해 입력을 받는다고 보시면 됩니다.
위에 보이는 것 처럼 , Constructor injection 과 Field injection 이 존재합니다.
Field Injection에 비해서, Constructor Injection은 생성시에 어떤 클래스의 컴포넌트가 필요한지 정확하게 알 수 있는 장점이 있는데요.
// 주입받을 객체
class AnalyticsAdapter @Inject constructor() {}
@AndroidEntryPoint
class TestActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// analytics 사용
}
}
간편함: 생성자 주입을 사용할 수 없는 경우 필드 주입은 간편한 대안이 됩니다.
명확성: 클래스의 어떤 필드가 의존성인지 명확하게 드러납니다.
접근 제한자: 필드는 private일 수 없으므로, protected 또는 public으로 선언해야 합니다. 이는 Hilt가 리플렉션을 통해 필드를 주입할 수 있도록 하기 위함입니다.
Null Pointer Exception 위험: 필드 주입은 객체가 초기화되지 않은 상태에서 사용될 수 있는 위험이 있습니다.
객체의 생성자에 의존성을 주입하는 방식입니다. 이는 주로 객체가 생성될 때 필요한 모든 의존성을 명시적으로 선언하고 주입받을 수 있어, 객체의 일관성을 보장합니다.
class AuthRepositoryImpl @Inject constructor(
private val authService: AuthService
) : AuthRepository {
...
}
안정성: 생성자 주입은 객체가 생성될 때 모든 의존성을 주입받아, Null Pointer Exception의 위험이 줄어듭니다.
일관성: 객체가 생성될 때 필요한 모든 의존성을 명시적으로 주입받아 객체의 상태를 일관되게 유지할 수 있습니다.
복잡한 객체 초기화: 생성자에 많은 의존성이 필요한 경우 생성자가 복잡해질 수 있습니다.
외부 라이브러리 클래스 주입 불가: 외부 라이브러리의 클래스는 생성자를 수정할 수 없어 생성자 주입이 불가능합니다.
Interface 주입 금지: 인터페이스는 구체적인 구현체를 명시할 수 없기 때문에 생성자 주입에 사용할 수 없습니다. 이를 해결하기 위해 @Binds 또는 @Provides를 사용합니다. (자세한 건 밑에서 다룰게요)
외부 라이브러리 클래스 주입 금지: Retrofit과 같은 외부 라이브러리의 객체는 생성자를 수정할 수 없어 생성자 주입이 불가능합니다. 이를 해결하기 위해 모듈을 사용하여 의존성을 주입합니다.
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
}
@Provides
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
Field Injection과 Constructor Injection은 각각의 장단점이 있으며, 상황에 맞게 적절히 사용해야 합니다. 일반적으로 생성자 주입이 더 안전하고 권장되지만, 안드로이드 시스템 클래스나 외부 라이브러리 클래스와 같은 특수한 경우에는 필드 주입을 사용할 수 있습니다.
자 그럼 이제 @HiltAndroidApp 를 통해서 사용을 하기로 했고, @AndroidEntryPoint @HiltViewModel 를 통해 각각 어디서 사용하는지 , @Inject 를 통해 받아올 정보를 명시해 주었죠. 그럼 어떤 정보를 제공할지를 확인해 봐야할 거예요. 이에 관련된 주석들을 살펴볼게요
감이 잘 안오시죠? 이제 한번 차근차근 살펴봅시다.

Binding이 정확히 무엇일까요
바인딩은 객체 생성과 제공을 연결하는 것을 의미합니다. 이를 통해 Hilt는 필요한 의존성을 주입할 수 있습니다
Binding 추가?
add bindings for types는 이러한 의존성을 해결하기 위해 Hilt 모듈에서 제공하는 메서드를 통해 해당 타입의 객체를 생성하고 Hilt 컨테이너에 등록하는 것을 의미합니다. 즉, Hilt 모듈은 객체를 생성하여 필요한 곳에 주입할 수 있도록 '바인딩'해줍니다.
"생성자 주입을 할 수 없는 타입"이라는 것은 다음과 같은 경우를 말합니다:
Retrofit 객체처럼 여러 설정이 필요하고, 직접 생성자를 통해 주입할 수 없는 경우.Context)이 필요한 경우.@InstallIn 주석은 Hilt가 어떤 DI(Dependency Injection) 컨테이너에 모듈을 설치할지를 명시합니다. 즉, 모듈의 바인딩이 특정 Hilt 컴포넌트에서 사용 가능하도록 합니다.

SingletonComponent는 Hilt에서 제공하는 DI 컨테이너 중 하나로, 애플리케이션 생명 주기 동안 인스턴스를 유지하는 싱글톤 범위의 컴포넌트입니다.
Hilt는 다양한 컴포넌트를 제공하여 각각 다른 생명 주기와 범위를 관리합니다. 주요 컴포넌트들은 다음과 같습니다 각 상황에 맞게 해당 컴포넌트를 적절하게 사용하는 것이 좋아보여요.

출처 : https://dagger.dev/hilt/components
이런 계층 구조를 가져요
레포지토리 같은 경우는 변경 되어야할 데이터가 있을 때 상황에 맞게 스코프를 설정하고 그외에 상황에서는 singleton이 효율적이다.
위의 예제에서 나오는
@InstallIn(SingletonComponent::class)는 해당 모듈의 바인딩이 애플리케이션 생명 주기 동안 싱글톤으로 관리되는 DI 컨테이너에서 사용 가능하도록 설정합니다. 다른 컴포넌트들도 특정 생명 주기 동안 의존성을 관리하도록 설정할 수 있습니다.

@Provides는 Room, Retrofit과 같은 외부 라이브러리에서 제공되는 클래스이므로 프로젝트 내에서 소유할 수 없는 경우 또는 Builder 패턴 등을 통해 인스턴스를 생성해야 하는 경우에 사용한다.
// Room Database
@Database(entities = [Log::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun logDao(): LogDao
}
// Use
val db = Room.databaseBuilder(context, Appdatabase::class.java, "name.db")
.build()
Room을 예시로 들 때, db를 생성 할 때 외부 라이브러리인 Room 내에서 builder 패턴을 이용하므로 constructor-inject 를 통한 종속성 주입이 불가능하다. 이 때 @Module을 생성하여 @Provides 어노테이션을 사용할 수 있다.
@InstallIn(SingletonComponent::class)
@Module
object DBModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context) : AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"logdata.db"
).build()
}
}
이 때, 함수의 반환 타입은 제공하고자 하는 인스턴스의 type이며, 매개변수는 인스턴스 생성에 필요한 종속성, 함수 내부는 실제 인스턴스의 구현이다.
@Provides만 포함되는 Module의 경우 object 형태 생성할 수 있다. 이러한 경우 provider는 최적화 된 코드를 제공하며, 거의 in line 된 코드로 처리된다.
💡 In Kotlin, modules that only contain `@Provides` functions can be `object` classes. This way, providers get optimized and almost in-lined in generated code. - [공식문서](https://developer.android.com/codelabs/android-hilt#6)
@Binds는 constructor를 가질 수 없는 인터페이스에 대한 종속성 삽입의 경우에 사용한다.
// 인터페이스
interface LogRepository {
suspend fun add(msg: String)
suspend fun getLogs() : List<Log>
}
// 인터페이스 구현체
@Singleton
class LogDBRepository @Inject constructor(
private val logDao: LogDao,
private val ioDispatcher : CoroutineDispatcher
) : LogRepository {
override suspend fun add(msg: String) = withContext(ioDispatcher) {
logDao.add(Log(msg = msg))
}
override suspend fun getLogs(): List<Log> = withContext(ioDispatcher) {
logDao.getLogs()
}
}
다음과 같이 LogRepository 인터페이스를 구현하는 class는 다른 class에서 종속성이 주입되어 사용 시 LogRepository라는 인터페이스 타입을 가지게 된다.
@HiltViewModel
class LogViewModel @Inject constructor(
@LogDB private val logRepository: LogRepository
)
: ViewModel() {
...
}
LogRepository에 대한 constructor-inject 종속성 주입은 interface이므로 불가능하다. 이 때 Module을 생성하여 @Binds 어노테이션을 사용할 수 있다.
@InstallIn(SingletonComponent::class)
@Module
abstract class LogDBModule {
@Binds
@Singleton
abstract fun bindLogDBRepository(impl: LogDBRepository) : LogRepository
}
위와 같이 @Binds 어노테이션을 통해 LogRepository에 대한 종속성을 제공할 수 있다.
이 때, 함수의 반환 타입은 구현하고자 하는 interface type이며, 매개변수는 실제 제공하고자 하는 interface의 구현체 class이다.
@Binds 어노테이션을 사용하기 위해서는 모듈은 abstract class, 함수는 abstract function이어야 한다.
@Provides는 객체 생성에 대한 책임을 집니다.@Binds는 자체적으로 객체 생성을 처리하지 않고 인터페이스와 구현체 간의 관계를 설정합니다.@Module
class MyModule {
@Provides
fun getInjectClass(injectObject: InjectClass): InjectInterface {
return injectObject
}
}
@Module
abstract class MyModule {
@Bindes
abstract fun getInjectClass(injectObject: InjectClass): InjectInterface
}


실제 코드로 구현했을 때 생성하는 코드 양이 적어지게 되어 성능적인 면에서 차이가 나게 됩니다.
@Qualifier는 동일한 타입의 의존성을 구분하기 위해 사용됩니다.
예를 들어, 같은 타입의 여러 구현체가 있을 때 특정 구현체를 명시적으로 지정할 수 있습니다.
예제:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ApiServiceType(val value: String)
@ApiServiceType("auth")
@Inject lateinit var authService: ApiService
@ApiServiceType("payment")
@Inject lateinit var paymentService: ApiService
@Module
@InstallIn(SingletonComponent::class)
object UrlModule {
@AuthBaseUrl
@Provides
fun provideAuthBaseUrl(): String = BuildConfig.AUTH_BASE_URL
@FriendBaseUrl
@Provides
fun provideFriendBaseUrl(): String = BuildConfig.FRIEND_BASE_URL
}
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class AuthBaseUrl
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class FriendBaseUrl
@Retention은 주석의 생명주기를 정의합니다. 주로 사용되는 값은 RUNTIME과 BINARY입니다.RUNTIME: 런타임까지 주석 사용 가능.BINARY: 컴파일된 클래스 파일에 주석을 저장.
//No scope annotation
class MemoRepository @Inject constructor(
private val db:MemoDatabase
){
fun load(id:String){...}
}

출처 droidnight 2020

만약 싱글톤을 사용하지 않는다면 위처럼 레포지토리를 다른 곳에서 참조할 때 서로 다른 instance를 가지게 됩니다.
@Singleton
class MemoRepository @Inject constructor(
private val db:MemoDatabase
){
fun load(id:String){...}
}

이처럼 sigleton 을 사용하면 같은 instance 를 사용할 수 있게 됩니다.
객체 여러번 생성 비효율 → 간혹 가다가 레포지토리 같은걸 전혀 바뀔 일 이 없다 하면 굳이 객체를 두세번 요청할 필요가없겠죠. 그럴때 sigleton 으로 하는데 그 내부에서 데이터의 변경 가능성이 있다하면 이러면 안될거예요.
sigleton으로하면 이전 데이터가 남아있기 때문에 프래그먼트가 싱글톤으로 되어있다, 메모리 누수나 이상한 동작 예상해 볼 수도 있을거 같네요
사용처에 맞게 사용하면 됩니다! 다만 주의할 점이 있어요.
앞서 말한 @InstallIn(~~~)의 형태에 맞춰서 사용해야합니다!

Hilt가 지원하지않는 클래스에서 의존성이 필요한 경우 사용
(예:ContentProvider, DFM, Dagger를 사용하지 않는 3rd-party라이브러리 등)
// EntryPoint생성하기
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface FooBarInterface{
fun getBar():Bar
}
// EntryPoint로접근하기
val bar = EntryPoints.get(
applicationContext,
FooBarInterface::class.java
).getBar()

@EntryPoint
@InstallIn(ApplicationComponent::class)
interface MemoEntryPoint{
fun getRepository():MemoRepository
}
class MemoProvider:ContentProvider()
{
override fun query(...){
val entryPoint=EntryPointAccessors.fromApplication(context,MemoEntryPoint::class.java)
val repository=entryPoint.getRepository()
...
}
}

프로젝트 단위(최상위) build.gradle.kts 에서 다음과 같이 추가해 줍니다.
plugins {
...
id 'com.google.dagger.hilt.android' version '2.51' apply false
}
앱 단위 build.gradle.kts 에 적용
plugins {
id 'com.google.dagger.hilt.android'
id 'kotlin-android'
id 'kotlin-kapt'
}
dependencies {
implementation 'com.google.dagger:hilt-android:2.51'
kapt 'com.google.dagger:hilt-compiler:2.51.1'
}
Hilt는 주석을 기반으로 작동하므로, 관련된 KAPT(Kotlin Annotation Processing Tool)를 추가해야 합니다.
Hilt를 사용하려면 Application 클래스에 @HiltAndroidApp 주석을 추가해야 합니다. 이 주석은 Hilt가 애플리케이션의 시작 지점을 알아차리게 해줍니다.
// Application 클래스
@HiltAndroidApp
class NowSopt : Application() {
// 추가 설정
}
//Manifest
<application
android:name=".NowSopt"
기존의 의존성 주입을 ViewModelFactory를 통해 수동으로 설정하던 방식에서, Hilt를 사용하여 의존성을 주입할 수 있습니다. 예를 들어, ViewModel을 주입하려면 다음과 같이 설정할 수 있습니다:
//viewmodel 에
@HiltViewModel
class LoginViewModel @Inject constructor(
private val logInUseCase: LogInUseCase
) : ViewModel() {
// ViewModel 구현
}
//usecase 사용시
//inject로 repository 받아야함
class LogInUseCase @Inject constructor(
private val authRepository: AuthRepository
) {
suspend operator fun invoke(request: UserEntity): Result<Int?> =
authRepository.login(request)
}
그리고 Activity나 Fragment에서 ViewModel을 주입받습니다:
@AndroidEntryPoint
class LoginActivity : BindingActivity<ActivityLoginBinding>(R.layout.activity_login) {
...
// 기존에 뷰모델 팩토리로 수동성 주입을 해주었다면
//private val viewmodel : LoginViewModel by viewModles{ ViewmodelFactory()}
private val viewModel: LoginViewModel by viewModels()
...
}
package com.sopt.now.data.repositoryimpl
...
class AuthRepositoryImpl @Inject constructor(
private val authService: AuthService
) :
AuthRepository {
override suspend fun login(authData: UserEntity): Result<Int?> =
...
}
@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
private const val CONTENT_TYPE = "application/json"
@Singleton
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.AUTH_BASE_URL)
.addConverterFactory(Json.asConverterFactory(CONTENT_TYPE.toMediaType()))
.build()
}
@Singleton
@Provides
fun provideAuthService(retrofit: Retrofit): AuthService {
return retrofit.create(AuthService::class.java)
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
@Binds
@Singleton
abstract fun bindAuthRepository(
authRepositoryImpl: AuthRepositoryImpl
): AuthRepository
}
https://naemamdaelo.tistory.com/entry/Android-Dagger-Hilt-제대로-알고-쓰자
Android 앱에서 Hilt 사용 | Android Developers
종속 항목 수동 삽입 | Android Developers
https://developer88.tistory.com/349
https://medium.com/@jms8732/module-vs-scope-c3324b991a5e
https://velog.io/@dear_jjwim/안드로이드-Hilt-공부를-위한-첫-걸음
https://fornewid.medium.com/dagger-hilt-내부코드-분석-e0af29c63367
https://medium.com/@seungbae2/understanding-the-difference-between-provides-and-binds-in-android-hilt-88d210e358d9
https://velog.io/@likppi100/DIHilt-Binds-vs-Provides