DI (Dependency Injection)
DI는 의존성 주입이라 부르며, 하나의 객체에 다른 객체의 의존성을 제공하는 기술입니다.
여기서 의존성은 서비스를 사용할 때 필요한 객체를 뜻합니다.
Android의 종속 항목 삽입 | Android 개발자 | Android Developers
Android의 종속 항목 삽입 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니
developer.android.com
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 사용하기
Hilt를 사용한 종속 항목 삽입 | Android 개발자 | Android Developers
Hilt를 사용한 종속 항목 삽입 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Hilt는 프로젝트에서 종속 항목 수동 삽입을 실행하는 상용구를 줄이는 Android용
developer.android.com
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 |