https://speakerdeck.com/fornewid/dagger-hiltro-yijonseong-juibhagi
@네이버 웹툰 안성용님 발표자료를 글로 옮긴 것입니다.
의존성 주입이란?
의존성 주입은 하나의 객체가 다른 객체의 의존성을 제공하는 기법.
의존성 주입의 의도는 객체의 생성과 사용의 관심을 분리하는 것.
// 의존성 주입 X
class Car {
private val engine: Engine = Engine()
fun start() {
engine.start()
}
}
// 의존성 주입 예시 - 생성자에서 전달
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
// 의존성 주입 예시 - 필드 주입
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
의존성 주입이 항상 좋을까?
- 객체를 생성하는 곳과 사용하는 곳이 분리되어 코드를 추적하기 어렵게 만들기도 한다.
- 코드가 부분 부분 분리되어, 시스템 전체가 수행하는 작업을 파악하기 어렵다.
- 사용하는 특정 DI 프레임워크에 의존하게 된다.
Hilt를 이용하여 Android 앱에 의존성 주입 패턴을 구현하되, 목적을 취할 만큼만 적당히 사용하는 방법을 찾아보는 것이 핵심.
Hilt 사용방법 알아보기
@Module
@InstallIn(SingletomComponent::class)
object ExampleModule {
// unscoped
@Provides
fun providesBar(foo: Foo): Bar = BarImpl(foo)
@Singleton
@Provides
fun providesFoo(): Foo = FooImpl()
}
Scope를 지정하면 해당 Component의 수명이 끝날때까지 유지되지만, Scope가 지정되지 않으면 매번 생성한다.
class BarImpl @Inject constructor(private val foo: Foo): Bar { ... }
class FooImpl @Inject constructor(): Foo { ... }
@Module
@InstallIn(SingletonComponent::class)
interface ExampleModule {
@Binds
fun binderBar(impl: BarImpl): Bar
@Singleton
@Binds
fun bindsFoo(impl: FooImpl): Foo
}
Inject와 Binds를 이용해서 구현체를 인터페이스 형태로 제공할 수도 있다. Provides와는 달리, Binds는 Factory 클래스가 생성되지 않는다.
Thinking about WorkManager
WorkManager에서 의존성 주입하기
WorkManager는 @AndroidEntrypoint
대신 @HiltWorker
를 사용한다. 매개변수를 주입할때는 @AssistedInject / @Assisted
를 사용한다.
WorkManager를 다룰때 발생하는 문제점을 DI를 이용해 해결해보자.
문제1: WorkManager를 사용하려면 항상 Context가 필요하다.
따라서 사용할 수 있는 위치가 다소 제한될 수 있는데, DI를 이용해 Context를 주입하면 Context 의존성을 숨길 수 있다.
@Module
@InstallIn(SingletonComponent::class)
object TasksModule {
@Singleton
@Provides
fun provideWorkManager(
@ApplicationContext context: Context,
): WorkManager {
return WorkManager.getInstance(context)
}
}
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val workManager: WorkManager
): ViewModel
Context를 고려하지 않고 사용하려는 곳에서 WorkManager로 주입받으면 된다.
문제2: 플랫폼 클래스에 의존성을 갖게된다.
Activity/ViewModel에서 WorkManager를 다루다보면 Worker, WorkManager들의 플랫폼 클래스에 의존성을 갖게 된다.
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val workManager: WorkManager
): ViewModel {
...
val request = OneTimeWorkRequestBuilder<ExampleWorker>()
.addTab(ExampleWorker.TAG)
.build()
workManager.enqueue(request)
...
}
DI를 이용하여 OnetimeWorkRequest를 하는 부분을 따로 비즈니스로직으로 빼서,
@Module
@InstallIn(SingletonComponent::class)
interface TasksBindsModule {
@Binds fun bindsExampleTasks(impl: ExampleTasksImpl): ExampleTasks
}
interface ExampleTasks {
fun execute()
}
class ExampleTasksImpl @Inject constructor(
private val workManager: WorkManager,
): ExampleTasks {
override fun execute() {
val request = OneTimeWorkRequestBuilder<ExampleWorker>()
.addTab(ExampleWorker.TAG)
.build()
workManager.enqueue(request)
}
}
플랫폼 클래스에 대한 의존성을 깔끔히 숨길 수 있게된다.
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val exampleTasks: ExampleTasks
): ViewModel {
...
exampleTasks.execute()
...
}
비슷하게, Worker내에서도 Worker의 동작과 비즈니스로직이 뒤섞여있을 경우, 비즈니스 로직들을 UseCase로 뽑아내서 Worker의 역할을 최소화하면서 이해하기 쉬운 코드를 만들 수 있다.
Thinking about Coroutines
DI를 이용해 CoroutineDispatcher를 주입할때, CoroutineDispatcher라는 같은 자료형을 사용하고 있기때문에 특정한 Dispatcher를 사용하고 싶을때 문제가 발생한다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DefaultDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Module
@InstallIn(SingletonComponent::class)
object DispatchersModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
}
Qualifier를 이용하면된다. 이렇게 자료형(Type)만으로 식별하기에 충분하지 않을때 Qualifier가 사용된다.
class Example @Inject constructor(
@DefaultDispatcher private val dispatcher: CoroutineDispatcher
_
이렇게 사용할 수 있다.
Application 범위에서 CoroutineScope를 사용하는 방법
- GlobalScope
- 테스트가 어려움
- ProcessLifecycleOwner
- App Process에 대한 Lifecycle Owner임
- 생명주기가 필요한 경우 사용하면된다 → App이 Foreground/Background인지 체크가 가능하다.
- 테스트가 어려움
- Custom CoroutineScope
- 소개할 방법, Application 범위의 CoroutineScope를 직접 생성한다.
Custom CoroutineScope
@Module
@InstallIn(SingletonComponent::class)
object DispatchersModule {
@ApplicationScope
@Singleton
@Provides
fun prividesApplicationCoroutineScope(
@MainImmediateDispatcher mainImmediateDispatchers: CoroutineDispatcher
): CoroutineScope = CoroutineScope(context = SupervisorJob() + mainImmediateDispatcher)
@MainImmediateDispatcher
@Provides
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}
위 예시에서는 Main.immediate를 주입했으나,
- Application 등 UI 로직에 사용한다면 순서보장이 되는 Main.immediate를,
- Background 작업에 주로 사용한다면 Dispatcher 전환이 적도록 Default를 사용할 수 있다.
서비스에서 Coroutine 사용하기
androidx.lifecycle:lifecycle-service
에서 제공하는 LifecycleService를 이용하면 Lifecycle을 고려한 Coroutine Scope를 사용할 수 있다.
class ExampleService: LifecycleService() {
fun foo() {
lifecycleScope.launch {
// do...
}
}
Thinking about EntryPoint
Hilt가 제공하지 않는 구성요소에도 EntryPoint를 이용하여 의존성 주입이 가능하다.
Compose에서 GrandChildren Composable에서 “Bar”를 사용하고 싶다면, 직접 GrandParents부터 쭉쭉 내려주거나, Composition Local을 이용하여 해결할 수 있다.
@AndroidEntrypoint
class ExampleActivity: Activity() {
@Inject lateinit var bar: Bar
override fun onCreate(savedInstantceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CompositionLocalProvider(LocalBar provides bar) {...}
}
}
}
이런 그림으로 사용할 수 있는데, Entrypoint로도 해결할 수 있다.
@Composable private fun rememberBar(): Bar {
val context = LocalContext.current
return remember {
val entrypoint = EntryPointAccessors.fromApplication(
context.applicationContext,
ExampleEntrypoint::class.java
)
entryPoint.providesBar()
}
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ExampleEntryPoint {
fun providesBar(): Bar
}
LocalContext를 이용해 Application 범위의 의존성을 주입하면
Activity 단에서 Bar를 주입 받을 필요가 없이 GrandChildren에서 바로 참조할 수가 있다.
@Composable fun GrandChildren() {
val bar: Bar = rememberBar()
}
'프로그래밍 > Android' 카테고리의 다른 글
[안드로이드] 순서보장 무한 페이저(Endless Pager) 만들기 with Jetpack Compose (0) | 2023.08.26 |
---|---|
[안드로이드] 부채꼴 카드처럼 돌아가는 Pager 만들기 (with Jetpack Compose Horizontal Pager) (0) | 2023.08.26 |
[안드로이드] 회전목마(Carousel) 애니메이션 구현하기 (0) | 2023.04.27 |
[안드로이드] 특정 시간 내 중복 Request를 막는 OkHTTP Interceptor 구현하기 (0) | 2023.03.28 |
[안드로이드] Push할 때마다 Auto-Formatting적용하기 : GitHook으로 코딩 스타일 맞추기 (0) | 2023.02.04 |