나는 의존성 주입을 위해 Hilt를 많이 사용해왔는데, Hilt와 함께 많이 사용되는 Koin과 한 번 비교를 해보려고 한다.
🥊 Koin | 🥊 Hilt |
---|---|
러닝 커브 ↓ | 러닝 커브 ↑ |
런타임 안정성 ↓ | 런타임 안정성 ↑ |
퍼포먼스 ↓ | 퍼포먼스 ↑ |
Koin은 Hilt에 비해 러닝커브가 낮다.
하지만 Koin은 Hilt처럼 컴파일타임에 의존성 주입을 검증하지 않고, 런타임에 인스턴스를 동적으로 주입하기 때문에 런타임 안정성이 낮아지고 실사용 중 크래시가 발생할 가능성이 높아 앱 퍼포먼스가 낮아진다.
개발이 계속 진행되면서 프로젝트는 점점 커지고 DI는 점점 더 많은 곳에서 사용하게 된다.
그렇기 때문에 컴파일 타임에 검증을 미리 마치고 문제를 해결할 수 있다는 것은 아주 큰 비용 절감으로 다가온다 !
Hilt는 애노테이션을 기반으로 동작한다.
Annotation Processor를 통해 어노테이션을 읽어 컴파일 타임에 의존성 그래프에 문제가 없는지 확인하고 의존성 주입에 필요한 소스코드를 생성한다.
그렇게 생성된 코드들은 런타임에 실행되면서 의존성 주입이 가능해진다.
이는 바이트 코드 변조를 통해 보일러 플레이트 코드를 줄일 수 있다.
컴파일시 생성되는 힐트 클래스는 주입과 관련된 코드를 포함된다. 그렇다면 실 코드에서 MemoApplication이 의존성 주입을 사용하기 위해 Hilt_MemoApplication을 상속해야 할까?
→ 🙅 아니다. MemoApplication 클래스는 바이트 코드 변조를 통해 자동으로 Hilt_MemoApplication을 상속하는 코드로 변환이 된다.
@AndroidEntryPoint
class MyActivity: AppCompatActivity() { ... }
힐트 컴포넌트는 대응되는 안드로이드 클래스에 바인딩을 제공해주는 역할을 한다.
ex) Activity는 ActivityComponent로부터 바인딩을 제공받는다.
위 사진은 컴포넌트간 계층 구조이며, 하위 컴포넌트는 상위 컴포넌트의 바인딩에는 접근할 수 있지만 그 반대는 불가하다.
바인딩을 제공하는 안드로이드 클래스의 생명주기에 맞게 컴포넌트를 만들고 파괴한다.
// 매 요청시 새로운 인스턴스 생성
class UnScopedFooBinding @Inject constructor() {
...
}
// 스코프 애노테이션이 있음
// 해당하는 Hilt 컴포넌트의 수명동안 매 요청에 동일 인스턴스를 반환
@ActivityScoped
class ScopedFooBinding @Inject constructor() {
...
}
@Inject를 통해 의존성을 주입할 수 있고, 다음과 같은 두 가지 방법이 있다.
class AnalyticsAdapter @Inject constuctor(
private val service: AnalyticsService,
) { ... }
바인딩(binding): 의존성을 컴포넌트에 추가해 객체를 제공하는 방법을 알려주는 것 또는 의존성 그 자체
- 생성자에서 @Inject를 통해 해당 클래스(AnalyticsAdapter)를 만들기 위해 AnalyticsService의 의존성을 힐트 컴포넌트에 요청하고 주입을 받는다.
- 주입 받음과 동시에 주입 받는 대상 클래스(AnalyticsAdapter)도 컴포넌트에 바인딩이 된다. (해당 클래스의 인스턴스를 제공하는 방법을 알려준다.)
- 두 가지를 다 수행하는 것이 특징 !
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var adapter: AnalyticsAdapter,
...
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
fun setBar(bar: Bar) {
...
}
}
@Module
어노테이션을 달아준다.@InstallIn
어노테이션을 사용한다. → 달아주지 않으면 컴파일 타임에 검사될 때 걸린다. @Module
& @InstallIn
은 세트 느낌컴파일 타임에 애노테이션 처리 → @Module 탐색 → @InstallIn에서 설치될 컴포넌트 탐색 → 해당 컴포넌트에 설치
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
// 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)
}
}
Android component | Default bindings |
---|---|
SingletonComponent | Application |
ActivityRetainedComponent | Application |
ViewModelComponent | SavedStateHandle |
ActivityComponent | Application, Activity |
FragmentComponent | Application, Activity, Fragment |
ViewComponent | Application, Activity, View |
ViewWithFragmentComponent | Application, Activity, Fragment, View |
ServiceComponent | Application, Service |
@Module
@InstallIn(SingletonComponent:class)
object FooModule {
@Provides
fun provideBar(app: Application): Bar {
...
}
}
class AnalyticsServiceImpl @Inject constructor(
@ApplicationContext context: Context
) : AnalyticsService { ... }
// The Application binding is available without qualifiers.
class AnalyticsServiceImpl @Inject constructor(
application: Application
) : AnalyticsService { ... }
class AnalyticsAdapter @Inject constructor(
@ActivityContext context: Context
) { ... }
// The Activity binding is available without qualifiers.
class AnalyticsAdapter @Inject constructor(
activity: FragmentActivity
) { ... }
class AnalyticsAdapter @Inject constuctor(
private val service: AnalyticsService,
) { ... }
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
@Provides
fun provideAnalyticsService(
// Potential dependencies of this type
): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}
interface AnalyticsService {
fun analyticsMethods()
}
// Constructor-injected, because Hilt needs to know how to
// provide instances of AnalyticsServiceImpl, too.
class AnalyticsServiceImpl @Inject constructor(
...
) : AnalyticsService { ... }
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
@Retention
은 어노테이션의 Scope을 지정해 어느 시점까지 어노테이션의 메모리를 가져갈 지 설정하는 역할을 한다.@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()
}
}
// 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
}
@InstallIn(SingletonComponent::class)
object FooModule {
@Named("Bar1")
@Provides
fun provideBar1: Bar {
return Bar()
}
}
취준때부터 Hilt는 나에게 굉장히 이해하기 어려운 벽같은게 느껴지는 개념이었다. 근데 이번 기회에 강의도 들어보고 공식문서도 뒤져보면서 어느정도 힐트랑 친해진 것 같아 뿌듯하다 🙂 전엔 Hilt 관련해서 쓰면서도 '아 제발 오류 나지마라 너..'이러면서 운빨코딩한 적이 많았던 것 같은데, 이젠 케이스에 맞게 잘 쓰고 오류가 나더라도 이유를 빠르게 파악하고 해결할 수 있을 것 같은 느낌..! 암튼 이 글 쓰는데 굉장히 오랜 시간이 걸렸고 아직도 엄청 빠삭하게 나는 힐트로 책을 낼 수도 있다 !!! 이런 느낌은 아닌 듯하여 틀린 부분이 있다면 댓글로.. 남겨주세요.. 🤗
[드로이드나이츠 2020] 옥수환 - Hilt와 함께 하는 안드로이드 의존성 주입
Dependency injection with Hilt
뱅크샐러드 안드로이드 앱에서 Koin 걷어내고 Hilt로 마이그레이션하기