의존성 주입이란?
💬 Dependency Injection (DI)
💬 여러 컴포넌트 간 의존성이 강한 안드로이드에서 클래스 간 의존성을 낮춰주기 위한 장치
💬 객체의 생성을 클래스 내부에서 하는 것이 아니라, 클래스 외부에서 객체를 생성하여 주입시켜주는 디자인 패턴
코드 예시
의존 관계를 형성하고 있는 코드
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
생성자를 통한 의존성 주입을 실행한 코드
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
멤버 변수 선언을 통한 의존성 주입을 실행한 코드
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
의존성 주입을 왜 사용하는가?
💬 규모가 큰 앱은 클래스끼리 연결하는데 불필요한 코드가 많이 생성되며, lazy init과 같은 코드를 통해 수명 관리까지 직접 해 줘야 한다.
💬 외부에서 객체를 생성해서 주입하기 때문에 코드의 재사용성이 높으므로, 여러 클래스를 주입시켜가면서 테스트를 하기에 용이하다.
Hilt 라이브러리
💬 인스턴스를 클래스 외부에서 주입시켜주며, 인스턴스에 대한 생명주기의 관리까지 해주는 라이브러리
💬 Dagger2보다 러닝 커브가 낮고, 초기 DI 환경 구축 비용을 크게 절감할 수 있다.
Settings
build.grale (rootProject)
buildscript {
ext {
hiltVersion = '2.38.1'
}
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hiltVersion"
}
}
💬 최신 버전 확인
https://developer.android.com/training/dependency-injection/hilt-android
Hilt를 사용한 종속 항목 삽입 | Android 개발자 | Android Developers
Hilt를 사용한 종속 항목 삽입 Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. 수동 종속 항목 삽입을 실행하려면 모든 클래스
developer.android.com
build.grale (:app)
plugins {
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
kapt "com.google.dagger:hilt-android-compiler:$rootProject.hiltVersion"
implementation "com.google.dagger:hilt-android:$rootProject.hiltVersion"
}
AndroidManifest.xml
<application
android:name=".MainApplication"
</application>
Hilt Application Class
MainApplication
@HiltAndroidApp
class MainApplication : Application() { }
💬 앱에 의존성 주입이 가능하도록 컴파일 단계 시 의존성 주입에 필요한 구성 요소들을 초기화하는 과정
💬 @HiltAndroidApp이 붙은 MainApplication을 앱 컨테이너라고 하며, 앱의 수명 주기와 밀접하게 연결된다.
💬 이어질 의존성 주입은 크게 의존성 바인딩(생성) -> 의존성 주입(사용)의 과정을 거친다.
의존성 주입 (사용)
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapter
}
💬 의존성 주입을 사용할 클래스에 @AndroidEntryPoint를 지정한다.
💬 @AndroidEntryPoint 지정이 가능한 클래스는 아래와 같다.
▫ Application (by using @HiltAndroidApp)
▫ ViewModel (by using @HiltViewModel)
▫ Activity
▫ Fragment
▫ View
▫ Service
▫ BroadcastReceiver
💬 @AndroidEntryPoint를 지정한 클래스에 종속되어 있는 모든 클래스에도 @AndroidEntryPoint를 지정해야 한다. 예를 들어, Fragment에 지정했다면 Fragment가 속한 Activity에도 지정해야 한다.
💬 Hilt에 install 되어 있는 인스턴스화된 변수를 사용하기 위해서는 @Inject를 사용한다.
💬 private으로 선언한 멤버 변수에는 @Inject를 사용할 수 없다.
생성자를 통한 의존성 바인딩 (생성)
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { }
💬 클래스의 생성자에서 @Inject를 지정하여 해당 클래스의 인스턴스를 만드는 방법을 Hilt에 알려준다.
💬 이로 인해, 외부 클래스에서 @Inject를 지정하면 클래스의 인스턴스를 바로 사용할 수 있다.
💬 또한, AnalyticsAdapter의 생성자 파라미터에는 AnalyticsService가 종속 항목으로 있기 때문에 Hilt는 AnalyticsService의 인스턴스를 제공받는 방법도 알아야 한다.
❓ 하지만, AnalyticsService는 인터페이스이기 때문에 생성자를 통한 의존성 바인딩이 가능하지 않다.
Hilt 모듈
💬 인터페이스나 외부 라이브러리 클래스와 같이 소유할 수 없는 경우에는 생성자를 통한 의존성 바인딩이 불가능하기 때문에 Hilt Module을 통해 Hilt에 바인딩 정보를 제공해야 한다.
💬 Hilt 모듈을 통해 Hilt에게 바인딩 정보를 제공하기 위한 방법으로는 @Binds와 @Provides가 있다.
@Binds
interface AnalyticsService {
fun analyticsMethods()
}
class AnalyticsServiceImpl @Inject constructor(
...
) : AnalyticsService { ... }
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
💬 @Module을 지정함으로써 특정 유형의 인스턴스를 제공하는 방법을 Hilt에게 알려준다.
💬 기본적으로 Hilt는 바인딩 시 범위를 지정하지 않는다. 따라서 바인딩을 요청할 때마다 새로운 인스턴스를 생성해서 준다. 하지만, @InstallIn을 지정함으로써 모듈을 사용하거나 설치할 Android 클래스 범위를 Hilt에게 알려주게 되면, 동일한 인스턴스를 공유할 수 있다.
💬 위 예제에서 AnalyticsModule은 ExampleActivity에 의존성 주입을 위해서 ActivityComponent 범위의 @InstallIn을 지정했다. 이에 따라, AnalyticsModule은 앱 전체의 액티비티 범위 내에서 사용할 수 있게 된다.
💬 @Binds는 주로 인터페이스의 인스턴스를 제공해야 할 때 사용한다.
💬 구성 요소
▫ 함수 파라미터 : 인터페이스를 구현한 클래스
▫ 리턴 타입 : 인스턴스를 제공할 인터페이스
💬 이제부터 액티비티 범위 내에서 AnalyticsService에 대한 의존성 주입이 가능해진다.
💬 bindAnalyticsServcice()는 직접 사용되지 않고, 외부에서 AnalyticsService에 대한 의존성 주입을 사용할 때 호출되어 인스턴스를 전달한다.
@Provides
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
@Provides
fun provideAnalyticsService(
...
) : AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
💬 외부 라이브러리(Retrofit, OkHttpClient, Room Database)나 Builder 패턴으로 생성되는 경우처럼 클래스를 소유할 수 없는 경우에는 @Provides를 통해 인스턴스를 제공하는 방법을 Hilt에게 알려줘야 한다.
💬 구성 요소
▫ 함수 파라미터 : 리턴 타입의 종속 항목
▫ 리턴 타입 : 인스턴스를 제공할 클래스 타입
▫ 함수 스코프 : 리턴 타입의 인스턴스를 제공하는 방법을 구현. Hilt는 해당 유형의 인스턴스를 제공해야 할 때마다 함수 스코프를 실행한다.
💬 @Binds와 마찬가지로 provideAnalyticsService()가 직접 사용되는 것이 아닌, 외부에서 AnalyticsService에 대한 의존성 주입을 사용할 때 호출되어 인스턴스를 전달한다.
동일한 리턴 타입에 대한 여러 바인딩 제공
💬 동일한 리턴 타입에 대한 서로 다른 구현을 제공할 경우 Hilt에게 한정자(@Qualifier)를 사용하여 이를 구분하는 방법을 알려줘야 한다.
💬 우선, 구분할 리턴 타입에 따른 한정자를 지정해준다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
💬 동일한 리턴 타입을 갖지만, 서로 다른 구현을 포함한 인스턴스를 한정자로 구분하여 제공한다.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@AuthInterceptorOkHttpClient
@Provides
fun provideAuthInterceptorOkHttpClient(
authInterceptor: AuthInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
}
@OtherInterceptorOkHttpClient
@Provides
fun provideOtherInterceptorOkHttpClient(
otherInterceptor: OtherInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(otherInterceptor)
.build()
}
}
💬 구분한 한정자에 해당하는 annotation class 명이 포함된 주석을 지정하여 서로 다른 구현 인스턴스를 사용할 수 있다.
// As a dependency of another class.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
@Provides
fun provideAnalyticsService(
@AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.client(okHttpClient)
.build()
.create(AnalyticsService::class.java)
}
}
// As a dependency of a constructor-injected class.
class ExampleServiceImpl @Inject constructor(
@AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...
// At field injection.
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {
@AuthInterceptorOkHttpClient
@Inject lateinit var okHttpClient: OkHttpClient
}
Hilt의 사전 정의된 한정자
💬 의존성 바인딩을 실행하는 클래스 내부에서 context가 필요할 경우, hilt는 @ApplicationContext와 @ActivityContext를 통해 context를 제공한다.
class AnalyticsAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: AnalyticsService
) { ... }
ViewModel 의존성 바인딩과 주입
💬 @HiltViewModel을 붙여주면 ViewModel 또한 의존성 바인딩이 가능하다.
@HiltViewModel
class LoginViewModel @Inject constructor(
private val analyticsAdapter: AnalyticsAdapter
): ViewModel { ... }
@AndroidEntryPoint
class LoginActivity: AppCompatActivity() {
private val loginViewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// loginViewModel is ready to be used
}
}
범위 지정을 위한 Hilt 구성 요소
💬 의존성 주입을 실행할 수 있는 안드로이드 클래스(상단의 @AndroidEntryPoint 지정이 가능한 클래스)마다 @InstallIn 주석에 참조할 수 있는 Hilt 구성 요소가 있다. 이는 위에서 @Module을 설치한 위치(구성 요소)를 의미하며, 안드로이드 수명 주기를 따르는 바인딩 컨테이너라고 이해하면 된다.
❗ BroadCast Receiver의 구성요소는 SingletonComponent에서 직접 주입하므로 생성하지 않는다.
구성요소 수명 주기
💬 Hilt는 해당 안드로이드 클래스의 수명 주기에 따라 생성된 구성요소 클래스의 인스턴스를 자동으로 만들고 제거한다.
구성요소 범위
💬 구성요소와는 별개로 설치된 구성요소 내부에 구성요소 범위 주석을 설정할 수 있다.
💬 구성요소 범위 주석을 설정하지 않으면 기본적으로 구현한 바인딩에 대한 주입을 요청할 때마다 새로운 인스턴스를 반환하게 된다.
💬 하지만 구성요소 범위 주석을 설정할 경우, 구현한 바인딩에 대한 주입 요청이 들어올 때마다 항상 같은 인스턴스를 반환하게 된다.
💬 구성요소 범위는 설치된 구성요소와 같은 범위만 설정할 수 있다. 예를 들면, ActivityComponent에 설치된 바인딩은 @ActivityScoped만 설정할 수 있다.
@ActivityScoped
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
💬 @ActivityScoped를 사용하여 AnalyticsAdapter의 범위를 ActivityComponent로 지정하면 Hilt는 해당 액티비티의 수명 주기동안 동일한 AnalyticsAdapter 인스턴스를 제공한다.
// If AnalyticsService is an interface.
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {
@Singleton
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
// If you don't own AnalyticsService.
@Module
@InstallIn(SingletonComponent::class)
object AnalyticsModule {
@Singleton
@Provides
fun provideAnalyticsService(): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}
💬 앱의 모든 위치에서 매번 동일한 인스턴스를 사용해야 하는 AnalyticsService가 있다고 가정하면, 위의 코드처럼SingletonComponent에 @Singleton 주석을 설정해주면 된다.
❗ 범위 주석을 설정한 객체는 구성요소가 제거될 때까지 메모리에 계속 남아있기 때문에 가급적 @Singleton 범위 주석을 지정한 바인딩의 사용을 최소화 시켜야 한다. 하지만, 특정 범위 내에서 동일한 인스턴스를 사용해야 하거나, 바인딩하는데 비용이 많이 들어가는 경우에는 범위 지정 바인딩을 사용하는 편이 좋다.
구성요소 계층 구조
💬 화살표는 하위 구성요소를 가리키고 있으며, 하위 구성요소에 설치하는 바인딩들은 상위 구성요소에서 설치된 바인딩들을 주입할 수 있다.
구성요소에 대한 종속 바인딩 제공
💬 Hilt는 구성요소에 종속되는 안드로이드 클래스 바인딩을 기본으로 제공한다.
💬 Application Context 바인딩은 @ApplicationContext를 통해 사용할 수도 있다.
class AnalyticsServiceImpl @Inject constructor(
@ApplicationContext context: Context
) : AnalyticsService { ... }
// The Application binding is available without qualifiers.
class AnalyticsServiceImpl @Inject constructor(
application: Application
) : AnalyticsService { ... }
💬 Activity Context 바인딩은 @ActivityContext를 통해 사용할 수도 있다.
class AnalyticsAdapter @Inject constructor(
@ActivityContext context: Context
) { ... }
// The Activity binding is available without qualifiers.
class AnalyticsAdapter @Inject constructor(
activity: FragmentActivity
) { ... }
Hilt가 지원하지 않는 클래스에 종속 항목 주입
💬 Hilt가 지원하지 않는 클래스에 대한 의존성 주입을 실행하려면 추가적인 조치가 필요하다.
💬 @EntryPoint 주석을 통해 진입점을 만들 수 있으며, 진입점이란 Hilt가 관리하는 객체의 그래프에 코드가 처음으로 들어가는 지점이다.
💬 Hilt는 ContentProvider를 직접 지원하지 않는다. 따라서 ContentProvider가 Hilt를 사용하여 종속 항목을 가져오도록 하려면 원하는 바인딩 유형마다 @EntryPoint를 지정하고 @InstallIn을 추가해야 한다.
class ExampleContentProvider : ContentProvider() {
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface ExampleContentProviderEntryPoint {
fun analyticsService(): AnalyticsService
}
...
}
💬 생성한 진입점에 접근하여 의존성 주입을 하기 위해서는 EntryPointAccessors와 메서드를 사용해야 한다.
💬 매개변수는 구성요소의 인스턴스이거나 구성요소 소유자 역할을 하는 @AndroidEntryPoint 객체여야 한다.
💬 매개변수로 전달하는 구성요소와 EntryPointAccessors 정적 메서드가 모두 @EntryPoint 인터페이스의 @InstallIn 주석에 있는 Android 클래스와 일치하는지 확인한다.
class ExampleContentProvider: ContentProvider() {
...
override fun query(...): Cursor {
val appContext = context?.applicationContext ?: throw IllegalStateException()
val hiltEntryPoint =
EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java)
val analyticsService = hiltEntryPoint.analyticsService()
...
}
}
참고 문서
https://medium.com/androiddevelopers/dependency-injection-on-android-with-hilt-67b6031e62d
Dependency injection on Android with Hilt
Jetpack’s recommended library for DI
medium.com
'Android > Sunflower' 카테고리의 다른 글
[ Android ] [ Kotlin ] Navigation (0) | 2021.12.13 |
---|---|
[ Android ] MVVM 패턴 (0) | 2021.09.27 |