개발/안드로이드

[Hilt] Hilt 의존성 주입 기초 예제로 살펴보기

스몰스테핑 2024. 8. 1. 16:11
import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class App: Application() {

}

 

Hilt의 출발점은 @HiltAndroidApp 애노테이션으로부터 시작된다.

이 애노테이션은 애플리케이션 클래스에 마킹하고 빌드하면 기본적인 설정이 끝난다.

빌드 이후, 싱글톤 컴포넌트(Container)가 자동적으로 생성된다.

 

빌드 후, 자동 생성된 싱글톤 컴포넌트

 

이 싱글톤 컴포넌트에 의존성을 추가할 수 있다.

생성자 바인드, 모듈의 프로바이드 어노테이션을 통한 바인딩 기법 두 가지가 존재한다.

컴포넌트에 의존성을 추가하는 것을 바인딩, 바인딩을 한다고 표현한다.

클라이언트는 컴포넌트에 바인딩된 의존성들을 요청할 수 있다.

 

@AndroidEntryPoint는 반드시 @HiltAndroidApp 선언 이후, 액티비티, 프래그먼트 등 안드로이드 컴포넌트에 마킹할 수 있다. 액티비티에 마킹을 했다면, ActivityRetainedComponent가 생성될 것이다.

 

이전에 다른 프로젝트나 수동으로 의존성 주입을 해볼 때, AppContainer와 목적에 맞는 Container를 분리해 구현하였던 적이 있다. 이때 수동으로 Container 클래스를 생성하고 관리했으나, Hilt의 경우 HiltAndroidApp과 AndroidEntryPoint 단 두 가지로 분류하여 생성할 수 있다.

 

1. @Inject, 생성자를 통한 바인딩

import javax.inject.Inject

class MyName @Inject constructor() {
    override fun toString(): String {
        return "작은발걸음"
    }
}

 

예시로 내 이름을 반환하는 클래스가 있다고 가정하자.

이 클래스는 단순하게도 "작은발걸음"이라는 내 블로그 이름을 반환한다.

이 클래스에 의존성을 컴포넌트에 바인딩하는 방법은 생성자를 만드는 것이다.

@Inject constructor()를 생성자에 추가한다.

 

import android.app.Application
import android.util.Log
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

@HiltAndroidApp
class App: Application() {
    val TAG: String = App::class.java.simpleName

    @Inject lateinit var myName: MyName

    override fun onCreate() {
        super.onCreate()
        Log.e(TAG, "My name is $myName")
    }
}

 

이후, App에서 Log.e를 통해 MyName을 출력해본다고 하자.

이 애플리케이션이 실행될 때, "My name is 작은발걸음"이 error 레벨로 logcat에 출력될 것이다.

그러기 위해선 MyName 클래스를 호출하든 참조를하든 뭘하든 해야 출력이 될 것이다.

 

@Inject lateinit var 를 통해 의존성 주입을 요청하자.

 

 

이 myName 변수를 사용하면 logcat에서 정상적으로 출력되는 것을 볼 수 있다.

 

이렇게 간단하게 몇가지 애노테이션을 추가했을 뿐인데 의존성 주입이 잘 되는 모습을 볼 수 있다.

만약 onCreate()의 super.onCreate() 이전에 Log.e를 넣으면 어떻게 될까?

 

init 오류가 발생한다.

Hilt는 onCreate() 이전에 코드를 배치하면 의존성 주입이 되지 않는다.

onCreate()에서 의존성 주입이 이루어지고, 그 이후에서야 참조가 가능해지기 때문이다.

 

 

2. Module을 통한 바인딩

이번에는 Module을 통한 바인딩이다.

class MyName {
    override fun toString(): String {
        return "작은발걸음"
    }
}

 

우선 MyName의 생성자에 추가했던 @Inject와 생성자를 제거한다.

의존성 바인딩이 사라지기 때문에 이대로 실행하면 오류가 발생할 것이다.

 

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    fun provideMyName(): MyName {
        return MyName()
    }
}

 

AppModule object를 만든다.

@Module을 붙이며, Hilt를 사용하기 때문에 @InstallIn()또한 붙인다.

싱글톤 컴포넌트로 명시하며, 내부에 @Provides를 추가해 MyName을 리턴시키자.

 

다양한 어노테이션에 대해서는 다음 글에 첨부된 사진을 참고하자.

https://small-stepping.tistory.com/1096

 

Android에서 Hilt란? (24-07-31 내용 보충)

Hilt란? 프로젝트에서 종속 항목 수동 삽입을 실행하는 상용구를 줄이는 Andorid용 종속 항목 삽입 라이브러리이다. 종속 항목 삽입(DI)를 실행하려면 모든 클래스와 종속 항목을 수동으로 구성하

small-stepping.tistory.com

 

import android.app.Application
import android.util.Log
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

@HiltAndroidApp
class App: Application() {
    val TAG: String = App::class.java.simpleName

    @Inject lateinit var myName: MyName

    override fun onCreate() {
        super.onCreate()
        Log.e(TAG, "My name is $myName")
    }
}

 

App 클래스는 변경사항이 없다.

이대로 빌드후, 실행하게 되면 마찬가지로 잘 뜨는 것을 볼 수 있다.

 

 

 

// Generated by Dagger (https://dagger.dev).
package com.example.snsaplication;

import dagger.MembersInjector;
import dagger.internal.DaggerGenerated;
import dagger.internal.InjectedFieldSignature;
import dagger.internal.QualifierMetadata;
import javax.inject.Provider;

@QualifierMetadata
@DaggerGenerated
@SuppressWarnings({
    "unchecked",
    "rawtypes",
    "KotlinInternal",
    "KotlinInternalInJava",
    "cast"
})
public final class App_MembersInjector implements MembersInjector<App> {
  private final Provider<MyName> myNameProvider;

  public App_MembersInjector(Provider<MyName> myNameProvider) {
    this.myNameProvider = myNameProvider;
  }

  public static MembersInjector<App> create(Provider<MyName> myNameProvider) {
    return new App_MembersInjector(myNameProvider);
  }

  @Override
  public void injectMembers(App instance) {
    injectMyName(instance, myNameProvider.get());
  }

  @InjectedFieldSignature("com.example.snsaplication.App.myName")
  public static void injectMyName(App instance, MyName myName) {
    instance.myName = myName;
  }
}
// Generated by Dagger (https://dagger.dev).
package com.example.snsaplication;

import dagger.internal.DaggerGenerated;
import dagger.internal.Factory;
import dagger.internal.Preconditions;
import dagger.internal.QualifierMetadata;
import dagger.internal.ScopeMetadata;

@ScopeMetadata
@QualifierMetadata
@DaggerGenerated
@SuppressWarnings({
    "unchecked",
    "rawtypes",
    "KotlinInternal",
    "KotlinInternalInJava",
    "cast"
})
public final class AppModule_ProvideMyNameFactory implements Factory<MyName> {
  @Override
  public MyName get() {
    return provideMyName();
  }

  public static AppModule_ProvideMyNameFactory create() {
    return InstanceHolder.INSTANCE;
  }

  public static MyName provideMyName() {
    return Preconditions.checkNotNullFromProvides(AppModule.INSTANCE.provideMyName());
  }

  private static final class InstanceHolder {
    private static final AppModule_ProvideMyNameFactory INSTANCE = new AppModule_ProvideMyNameFactory();
  }
}

 

자동 생성되는 코드를 보면 의존성 주입이 injectMyName에서 이루어지고, 주입을 한다. injectMyName은 주어진 Provider에서 get이라는 함수를 통해 MyName을 얻고 있음을 알 수 있다.

 

애노테이션의 사용법에 따라 생성되는 코드는 달라지고, 생성 코드를 참고해서 의존성 주입이 이뤄지는 방식을 볼 수 있다. 하지만, Dagger에 비해 Hilt가 캡슐화와 은닉이 잘되어있어 전체 코드의 파악이 어렵기 때문에 간단하게 참조하는 정도로만 생각하고 넘어가는게 좋다.

 

 

이제 기본적으로 만들어지는 액티비티에 의존성 주입을 시도해보자.

 

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.snsaplication.ui.theme.SnsAplicationTheme
import javax.inject.Inject

class MainActivity : ComponentActivity() {
    @Inject lateinit var myName: MyName

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SnsAplicationTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting(myName.toString())
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    SnsAplicationTheme {
        Greeting("Android")
    }
}

 

하지만 이렇게 하면 에러가 발생한다.

 

 

이전에 썼던 Hilt 관련 포스팅이나 이 글의 처음 부분을 보았으면 알겠지만, MainActivity에 대한 진입점(AndroidEntryPoint)을 제공하지 않았기 때문이다.

 

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    ...
}

 

MainActivity 위에 @AndroidEntryPoint 어노테이션을 추가하면 정상적으로 작동하는 모습을 확인할 수 있다.