DI (Dependency Injection)
DI는 의존성 주입이라 부르며, 하나의 객체에 다른 객체의 의존성을 제공하는 기술입니다.
여기서 의존성은 서비스를 사용할 때 필요한 객체를 뜻합니다.
DI 기법 예시
DI를 예시로 Car 클래스와 Engine 클래스로 설명하면 다음과 같습니다.
Car 클래스가 실행되기 위해서는 Engine 클래스의 인스턴스가 있어야 합니다.
이 때 필요한 클래스인 Engine을 종속 항목, 다른 말로 의존성이라고 합니다.
클래스가 필요한 객체를 얻는 방법에는 세 가지가 있습니다.
- Car 클래스 안에서 Engine 인스턴스를 생성하여 초기화합니다.
- 다른 곳에서 객체를 가져옵니다. Android에서는 Context나 getSystemService() 등을 사용합니다.
- 객체를 매개변수로 제공받습니다. Car 클래스의 생성자에서 Engine을 매개변수로 받게 합니다.
여기서 세 번째 방법이 바로 의존성 주입 기법 중 하나입니다.
// DI를 사용하지 않은 경우
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
- Car와 Engine은 결합(의존성)이 강합니다.
- Car는 Engine의 자식 클래스를 사용하기 어렵습니다.
- Engine을 직접 생성하면 Car를 재사용할 때, Engine을 상속한 다른 클래스를 재사용하기 어렵습니다.
- Engine 유형이 Gas와 Electric 두 가지라면 하나의 Car 인스턴스를 재사용하는 대신 두 가지 유형의 Car를 생성해야 합니다.
- Car는 Engine의 실제 인스턴스를 사용하기 때문에 다양한 시나리오를 고려하지 못하기 때문에 테스트를 수행하기 어렵습니다.
- Engine의 생성자가 변경된 경우 Car 클래스에서도 수정이 이루어져야 합니다.
// DI를 사용한 경우
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
- Car 클래스에게 다른 Engine의 구현들을 전달할 수 있기 때문에, Car를 재사용할 수 있습니다.
- Engine의 생성자 등 내부의 코드가 변경되어도, Car 클래스를 수정하지 않아도 됩니다.
- Car를 테스트하기 쉽습니다.
- 모의 객체를 사용하여 Engine 타입을 여러 방면으로 테스트할 수 있습니다.
Android에서의 DI
- Constructor Injection : 생성자를 통한 주입
위의 방법대로, 생성자 파라미터를 통해 의존성을 주입해주는 방법입니다. - Field Injection : setter를 통한 주입
Android에서는 Activity나 Fragment는 시스템에서 인스턴스화하므로 생성자 삽입이 불가능합니다.
참조가 필요한 클래스를 먼저 생성한 다음에 의존성을 주입하는 방법을 사용해야 합니다.
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
Dagger와 Hilt
이처럼 수동으로 의존성을 주입하면 다음의 문제점이 생깁니다.
- 크기가 큰 앱은 의존성이 필요한 클래스들을 연결하려면 불필요한 코드가 많이 생성되게 됩니다.
- 여러 레이어를 가질 경우, 최상위 객체를 가지기 위해서는 그 아래 모든 계층의 객체가 필요하게 됩니다.
- 전달하기 전에 종속성을 생성할 수 없는 경우, lazy init과 같은 코드를 작성하고 수명을 직접 관리해야 합니다.
Android에서는 이러한 의존성 주입을 자동으로 해주는 라이브러리로 `Dagger`와 `Hilt`가 있습니다.
Dagger
- Dagger는 Android 뿐만 아니라 Java 앱에서도 동작하도록 디자인되어 있습니다.
- 의존성을 직접 관리하여 앱에서 쉽게 사용할 수 있도록 합니다.
- 컴파일 타임에 의존성을 연결해서 리플렉션 기반으로 의존성을 주입하는 것에 대한 성능 이슈를 해결합니다.
Hilt
- Hilt는 Dagger를 기반으로 빌드되어 Dagger를 Android 애플리케이션에 통합하는 표준 방법을 제공합니다.
- SingletonComponent, ActivityComponent, FragmentComponent 등 안드로이드 컴포넌트 생명주기에 맞게 인스턴스를 관리하는 방법을 제공합니다.
- Android에서 종속성 주입에는 Hilt를 사용하도록 권고하고 있습니다.
Hilt는 자동으로 다음을 생성하고 제공합니다.
- 수동으로 생성해야 하는 Dagger와 Android 프레임워크 클래스를 통합하기 위한 구성요소
- Hilt가 자동으로 생성하는 구성요소와 함께 사용할 범위 주석
- Application 또는 Activity와 같은 Android 클래스를 나타내는 사전 정의된 결합
- @ApplicationContext 및 @ActivityContext를 나타내는 사전 정의된 한정자
Hilt 사용하기
1. build.gradle에 의존성 추가
Hilt를 사용하기 위해서 build.gradle에 Hilt 라이브러리를 추가합니다.
// build.gradle (Project)
plugins {
...
id 'com.google.dagger.hilt.android' version '2.44.2' apply false
}
// build.gradle (Module)
plugins {
...
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
...
implementation "com.google.dagger:hilt-android:2.44.2"
kapt "com.google.dagger:hilt-compiler:2.44.2"
}
2. Application 클래스 작성
Hilt를 사용하는 모든 앱은 @HiltAndroidApp 주석이 지정된 Application 클래스를 포함해야 합니다.
Application 클래스에 @HiltAndroidApp 어노테이션을 포함하면 Hilt에게 종속성 주입에 필요한 코드를 생성하도록 지시합니다,
생성된 코드에는 애플리케이션 수준 종속성에 대한 컨테이너 역할을 하는 Dagger 구성 요소가 포함되어 있어 이러한 종속성을 다양한 부분에 더 쉽게 주입할 수 있습니다.
@HiltAndroidApp
class HiltApplication : Application()
3. Android 클래스에 종속성 주입
Hilt는 @AndroidEntryPoint 주석이 있는 다른 클래스에 종속 항목을 제공할 수 있습니다.
Hilt는 이를 사용해서 다음 클래스들에 종속성을 제공해 줄 수 있습니다.
- Application (@HiltAndroidApp 사용)
- ViewModel (@HiltViewModel 사용)
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
Hilt는 @AndroidEntryPoint 주석이 달린 구성요소에 종속성을 삽입하는 데 필요한 Dagger 코드를 자동으로 생성합니다.
구성 요소에서 종속성을 가져오려면 다음과 같이 @Inject 주석을 사용하여 필드 주입을 실행합니다.
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
// ExampleActivity에서 analytics 필드를 사용하여
// 주입된 AnalyticsAdapter 인스턴스에 액세스 가능
@Inject lateinit var analytics: AnalyticsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// @Inject 주석이 없었다면 다음과 같이 직접 인스턴스화 해야함
// analytics = AnalyticsAdapter()
// 바로 analytics 필드를 사용 가능
analytics.trackEvent("Example Event")
}
}
Hilt에 결합 정보를 제공하는 방법 중 하나는 생성자 주입입니다.
다음과 같이 @Inject 주석을 사용하면 해당 클래스의 인스턴스를 만들 때 필요한 종속성을 Hilt에 알려줍니다.
주석이 지정된 클래스 생성자의 매개변수는 그 클래스의 종속 항목입니다.
// AnalyticsAdapter 인스턴스를 만들 때 AnalyticsService가 필요하다는 것을 나타냄
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
Hilt 모듈
@Inject 주석 뿐만 아니라 Module을 이용해서도 Hilt에게 원하는 종속 항목을 생성하게 할 수 있습니다.
아래와 같이 생성자 주입을 할 수 없는 상황에 Hilt 모듈을 사용하여 Hilt에 결합 정보를 제공할 수 있습니다.
- 인터페이스
- 외부 라이브러리의 클래스
먼저 @Module 주석을 붙여주어 Hilt가 여기가 Module이 있는 곳임을 알 수 있게 합니다.
다음으로 @InstallIn 주석을 붙여줍니다.
@InstallIn(ActivityComponent::class)는 해당 모듈이 액티비티에서 사용 가능하다고 선언합니다.
@Module
@InstallIn(ActivityComponent::class)
class ExampleModule {
...
}
@Binds 사용
@Binds 주석은 인터페이스의 인스턴스를 제공해야 할 때 사용할 구현을 Hilt에 알려줍니다.
@Binds 주석이 지정된 함수는 Hilt에 다음 정보를 제공합니다.
- 함수 반환 유형은 함수가 어떤 인터페이스의 인스턴스를 제공하는지 Hilt에 알려줍니다.
- 함수 매개변수는 제공할 구현을 Hilt에 알려줍니다.
interface AnalyticsService {
fun analyticsMethods()
}
class AnalyticsServiceImpl @Inject constructor(
...
) : AnalyticsService { ... }
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
@Provides 사용
@Provides 주석은 외부 라이브러리인 클래스(Retrofit, OkHttpClient, Room 등) 또는 빌더 패턴으로 인스턴스를 생성해야 하는 경우에 사용합니다.
@Provides 주석이 지정된 함수는 Hilt에 다음 정보를 제공합니다.
- 함수 반환 유형은 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt에 알려줍니다.
- 함수 매개변수는 해당 유형의 종속 항목을 Hilt에 알려줍니다.
- 함수 본문은 해당 유형의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다. Hilt는 해당 유형의 인스턴스를 제공해야 할 때마다 함수 본문을 실행합니다.
@Module
@InstallIn(SingletonComponent::class)
object AnalyticsModule {
@Singleton
@Provides
fun provideAnalyticsService(): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}
동일한 타입의 객체에 대한 종속성
같은 타입을 반환해야 할 경우 각각 annotation class를 붙여 이름을 지정합니다.
@Qualifier @Retention(AnnotationRetention.BINARY) 주석을 붙여서 구분할 식별자라고 Hilt에 알려줍니다.
@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()
}
}
사전 정의된 한정자
애플리케이션 또는 액티비티의 Context 클래스가 필요할 수 있으므로 Hilt는 @ApplicationContext 및 @ActivityContext 한정자를 제공합니다.
class AnalyticsAdapter @Inject constructor(
@ApplicationContext private val applicationContext: Context,
@ActivityContext private val activityContext: Context,
private val service: AnalyticsService
) { ... }
Hilt 구성요소
Hilt는 컴포넌트(구성요소)들을 이용해서 필요한 Dependency를 클래스에 제공할 수 있게 해 줍니다.
Hilt는 다음 구성요소를 제공합니다.
구성요소 범위
기본적으로 Hilt의 모든 결합은 범위가 지정되지 않는다.
앱이 결합을 요청할 때마다 Hilt는 필요한 유형의 새 인스턴스를 생성합니다.
그러나 Hilt는 결합을 특정 구성요소로 범위 지정할 수도 있습니다.
Hilt는 결합의 범위가 지정된 구성요소의 인스턴스마다 한 번만 범위가 지정된 결합을 생성하며, 이 결합에 관한 모든 요청은 동일한 인스턴스를 공유합니다.
구성요소 전체 기간
Hilt는 해당 Android 클래스의 수명 주기에 따라 생성된 구성요소 클래스의 인스턴스를 자동으로 만들고 제거합니다.
구성요소 계층 구조
구성요소에 모듈을 설치하면 이 구성요소의 다른 결합 또는 구성요소 계층 구조에서 그 아래에 있는 하위 구성요소의 다른 결합의 종속 항목으로 설치된 모듈의 결합에 액세스할 수 있습니다.
Hilt가 지원하지 않는 클래스에 종속 항목 삽입
Hilt가 지원하지 않는 클래스에 필드 삽입을 실행해야 할 수도 있습니다.
이러한 경우, @AndroidEntryPoint 대신 @EntryPoint 주석을 사용하여 진입점을 만들 수 있습니다.
진입점은 Hilt가 관리하는 코드와 그렇지 않은 코드 사이의 경계입니다.
진입점을 통해 Hilt는 Hilt가 관리하지 않는 코드를 사용하여 종속 항목 그래프 내에서 종속 항목을 제공할 수 있습니다.
콘텐츠 제공자가 Hilt를 사용하여 일부 종속 항목을 가져오도록 하려면 원하는 결합 유형마다 @EntryPoint로 주석이 지정된 인터페이스를 정의하고 한정자를 포함해야 합니다.
그리고 다음과 같이 @InstallIn을 추가하여 진입점을 설치할 구성요소를 지정합니다.
class ExampleContentProvider : ContentProvider() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ExampleContentProviderEntryPoint {
fun analyticsService(): AnalyticsService
}
...
}
진입점에 액세스하기 위해 EntryPointAccessors의 정적 메서드인 fromApplication()을 사용하여 객체를 가져옵니다.
매개변수는 구성요소 인스턴스이거나 구성요소 소유자 역할을 하는 @AndroidEntryPoint 객체여야 합니다.
- EntryPointAccessors.fromApplication(Application Context, 인터페이스::class.java)
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()
...
}
}
'안드로이드 > 활용' 카테고리의 다른 글
[Android] Paging3 사용하기 (0) | 2023.12.02 |
---|---|
[Android] Coil 사용하기 (0) | 2023.10.24 |
[Android] CountDownTimer 사용하기 (0) | 2023.10.11 |
[Android] SMS Retriever API를 통해 SMS 자동으로 읽어오기 (0) | 2023.10.11 |
[Android] BindingAdapter 사용하기 (0) | 2023.10.09 |