본문 바로가기

dev/android

Dagger2 (feat. Android Developer, Codelab)

via GIPHY

https://medium.com/@sangcomz/dagger2%EB%9E%80-f6aed5948023

 

Dagger 2란?

Dagger 2

medium.com

2016년 3월에 이런 글을 썼었습니다. 쓴 이유는 회사에서 Dagger2를 사용하고 있었기 때문에 학습 및 발표를 위해서 작성했었습니다.

한 동안 Dagger2를 사용하지 않고 전 회사에선 Koin을 사용하고, 요즈음엔 어떤 DI 쓰고 있지 않아서 기억이 가물가물해졌습니다. (애초에.... 제대로 이해하지 못했습니다.....)

 

이번에 Dagger2를 사용할 일이 있어서 다시 학습을 했습니다.

학습을 하면서 제가 저 글에서 알았던 지식이 잘 못 됐다는 것을 깨달았습니다.

https://developer.android.com/training/dependency-injection

 

Dependency injection in Android  |  Android Developers

Dependency injection (DI) is a technique widely used in programming and well suited to Android development. By following the principles of DI, you lay the groundwork for good app architecture. Implementing dependency injection provides you with the followi

developer.android.com

https://codelabs.developers.google.com/codelabs/android-dagger/#13

 

Using Dagger in your Android app

In Android, you usually create a Dagger graph that lives in your Application class because you want an instance of the graph to be in memory as long as the app is running. In this way, the graph is attached to the app's lifecycle. In our case, we also want

codelabs.developers.google.com

위에 두 링크를 통해서 학습을 했고, 학습을 하면서 정리한 내용을 간단히 공유 혹은 메모를 하려고 합니다.

 

의존성 주입이란?

의존성 주입은 말 그대로 외부에서 의존성을 주입받는 것입니다. 예를 들어서 Car라는 Class는 Engine 클래스에 대한 참조가 필요합니다. 이때 Car 클래스는 Engine 클래스에 대한 의존성을 갖게 됩니다. 이때 Engine 클래스 인스턴스를 주입받는 것을 말합니다. 

의존성 주입 장점

a. 코드 재사용성

b. 리팩토링을 더 쉽게

c. 테스트를 더 쉽게

의존성 주입 방법

a. 생성자 주입 (Constructor Injection)

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

b. 필드 주입 (Field Injection or Setter Injection)

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

 

의존성 주입 자동화의 필요성

For big apps, taking all the dependencies and connecting them correctly can require a large amount of boilerplate code. In a multi-layered architecture, in order to create an object for a top layer, you have to provide all the dependencies of the layers below it. As a concrete example, to build a real car you might need an engine, a transmission, a chassis, and other parts; and an engine in turn needs cylinders and spark plugs.

앱이 커지면 많은 수의 의존성 주입이 생기는데, 이는 많은 보일러 플레이트 코드를 만듭니다. 그리고 상위 레이어에서 하위 레이어까지 주입을 하게 되면 오류를 낳게 됩니다.

When you're not able to construct dependencies before passing them in — for example when using lazy initializations or scoping objects to flows of your app — you need to write and maintain a custom container (or graph of dependencies) that manages the lifetimes of your dependencies in memory.

메모리 관리가 어렵습니다.

 

자동화하는 라이브러리엔 두 가지 방식이 있습니다.

- 리플렉션을 이용해서 런타임에 의존성을 주입해주는 라이브러리 (ex:Guice)

- 코드를 생성해서 컴파일 타임에 의존성을 연결해주는 라이브러리 (ex:Dagger)

 

의존성 주입 대안

서비스 로케이터 패턴을 사용해서 의존성 주입

 

서비스 로케이터 패턴의 단점

- 테스트하기 더 어려움

- 런타임에 오류를 발견

- 객체의 수명 관리 어려움

 

https://en.wikipedia.org/wiki/Service_locator_pattern

 

Service locator pattern - Wikipedia

The service locator pattern is a design pattern or anti-pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer. This pattern uses a central registry known as the "service locator",

en.wikipedia.org

 

Dagger 기초

Dagger 장점

- 의존성 컨테이너 생성해서 의존성 관리

- 필요한 클래스의 팩토리 생성

- Scope를 이용해서 의존성 재사용 및 재생성을 제어

- SubComponent를 이용해서 더 이상 필요 없는 의존성 혹은 재사용할 의존성을 관리

 

Dagger는 annotation processing을 사용해서 컴파일 타임에 수동 의존성 주입과 비슷한 코드를 생성해줍니다.

그리고 컴파일 타임에 종속성 그래프를 빌드하고 검증해줍니다. 

- 런타임 오류가 발생하지 않습니다.

- 의존성 싸이클이 발생하는 걸 방지합니다.

 

 @Inject 사용

// @Inject Dagger에게 이 오브젝트 인스턴스를 어떻게 생성하는지 알려줍니다.
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Component 사용

// @Component Dagger에게 의존성 컨테이너를 생성하도록 말해줍니다.
@Component
interface ApplicationGraph {
    // 필요한 클래스의 인스턴스를 반환하는 함수를 작성
    fun repository(): UserRepository
}

Scoping with Dagger

 

You can use scope annotations to limit the lifetime of an object to the lifetime of its component. This means that the same instance of a dependency is used every time that type needs to be provided.

Scope Annotation을 이용해서 생성된 인스턴스의 생명주기를 설정할 수 있습니다.

 

Android App에서 Dagger 사용

Best practices summary

- 대부분의 경우에 가능하면 생성자 @Inject를 사용합니다. 예외인 경우에 @Binds 및 @Provides를 사용합니다.

 

@Binds : 주입하는 대상이 interface일 경우에 사용합니다

interface NeedBind {
    fun print()
}

class NeedBindImpl @Inject constructor() : NeedBind {
    override fun print() {
        println("Hello NeedBindImpl")
    }
}

@Module
abstract class NeedBindModule {
    @Binds //NeedBind 인스턴스할 방법을 Dagger에게 알려줍니다.
    abstract fun provideNeedBind(needBindImpl: NeedBindImpl): NeedBind
}

@Provides : 프로젝트가 소유하지 않은 클래스를 제공하는 방법을 Dagger에게 알려줘야 할 때 사용합니다. (ex:Retrofit)

@Module
class NetworkModule {

    // @Provides 대거에게 인스턴스를 어떻게 생성하는지 알려줍니다.
    @Provides
    fun provideMainApiService(): MainApiService {
        return Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(MainApiService::class.java)
    }
}

 

- 컴포넌트는 한 번만 모듈에 선언

 

- Custom Scope Animation은 생명주기에 맞는 이름으로 만들면 재사용하기 좋습니다.

 

Dagger subcomponents

로그인을 구현할 때, 로그인 플로우상 같은 인스턴스를 사용해야 하는 인스턴스가 존재할 수 있습니다. 이럴 때 @Singleton을 사용하면 동일한 인스턴스를 받을 수 있습니다. 하지만 여기선 두 가지 문제점이 있습니다.

- 로그인 플로우가 끝난 뒤에도 메모리에 남아있게 됩니다.

- 새로운 로그인 플로우가 시작됐을땐 새로운 인스턴스가 필요한데, 기존에 생성한 인스턴스를 사용하게 됩니다.

 

그럴 때 subcomponents를 사용해서 문제를 해결할 수 있습니다. subcomponents는 캡슐화의 좋은 방법입니다.

 

// Subcomponent를 생성해줍니다.
@ActivityScope
@Subcomponent
interface SubComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(): SubComponent
    }

    fun inject(nextActivity: NextActivity)
}

// Subcomponent를 갖고 있는 Module을 만들어줍니다.
@Module(subcomponents = [SubComponent::class])
class SubModule {}

@Singleton
@Component(modules = [NetworkModule::class, NeedBindModule::class])
interface AppComponent {
    fun inject(mainActivity: MainActivity)

    fun loginComponent(): SubComponent.Factory //subcomponent 생성 방법을 Dagger에게 알려줍니다.
    
    
}

// 사용합니다.
class NextActivity : AppCompatActivity() {

    lateinit var subComponent: SubComponent

    @Inject
    lateinit var nextViewModel: NextViewModel

    override fun onCreate(savedInstanceState: Bundle?) {

        subComponent = (application as Dagger2SampleApp).appComponent.loginComponent().create()
        subComponent.inject(this)

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_next)

        nextViewModel.print()
    }
}

 

추가적으로 알면 좋은 Annotation 설명

@BindsInstance

Dagger Graph 외부에서 이미 만들어진 객체를 Graph에 넣어줄 때 사용합니다. (Ex:Context)

@Singleton
@Component(modules = [NetworkModule::class, NeedBindModule::class, CarBindingModule::class, AnimalBindingModule::class])
interface AppComponent {
    fun inject(mainActivity: MainActivity)
    @Component.Factory
    interface Factory {
        // With @BindsInstance, Context를 의존성 그래프에서 사용할 수 있게 됩니다.
        // 이미 instance가 외부에서 생성된 것중에 의존성 그래프에서 필요할 때 사용합니다.
        fun create(@BindsInstance context: Context): AppComponent
    }

    fun loginComponent(): SubComponent.Factory
}

@IntoMap

Map으로 주입 받을 수 있게 만들어줍니다.

@Module
abstract class AnimalBindingModule {

    @Binds
    @IntoMap
    @StringKey("Dog")
    abstract fun provideDog(dog: Dog): Animal


    @Binds
    @IntoMap
    @StringKey("Cat")
    abstract fun provideCat(cat: Cat): Animal
}

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var animalMap: Map<String, @JvmSuppressWildcards Animal>
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	(application as Dagger2SampleApp).appComponent.inject(this)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        .
        .
        .
        animalMap.forEach {
            print("I am ${it.key} ")
            it.value.cry()
        }
    }
}

 

@IntoSet

Set으로 주입 받을 수 있게 만들어줍니다.

@Module
abstract class CarBindingModule {

    @Binds
    @IntoSet
    abstract fun provideSuperCar(car: SuperCar): Car


    @Binds
    @IntoSet
    abstract fun provideMiniCar(car: MiniCar): Car
}



class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var carSet: Set<@JvmSuppressWildcards Car>
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	(application as Dagger2SampleApp).appComponent.inject(this)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        .
        .
        .
        carSet.forEach {
        	it.boooong()
        }
    }
}

 

@IntoMap의 경우에 Android AAC ViewModel을 주입할 수 있습니다.

@Singleton
class ViewModelFactory @Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) :
    ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T =
        viewModels[modelClass]?.get() as T
}

@Target(
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER
)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

@Module
abstract class ViewModelModule {

    @Binds
    internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

    @Binds
    @IntoMap
    @ViewModelKey(HomeViewModel::class)
    internal abstract fun homeViewModel(viewModel: HomeViewModel): ViewModel
}

 

전체 코드는 아래 저장소에서 확인 가능합니다.

https://github.com/sangcomz/Dagger2Sample

 

sangcomz/Dagger2Sample

Contribute to sangcomz/Dagger2Sample development by creating an account on GitHub.

github.com