
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 |