Who should read?

이제 막 안드로이드를 배우고 있는 개발자에게는 추천하지 않습니다.
하지만 안드로이드를 개발을 어느정도 해봤고, 내가 중급개발자 이상이 되고 싶다 하시는분은 잘 오셨습니다.
자바와 객채지향개념에 대해서 친숙하다면 큰 어려움이 없을 겁니다.
Welcome to Dagger World 

Dependency Injection이란 무엇일까?

한국말로 하자면 Dependency는 의존성이고, Injection은 주입이다.

의존성이란?

  • 코드에서 두 모듈간의 연결
  • 두 클래스 간의 관계
  • 의존성이 크다는것은 Coupling(결합도)이 높다는것

의존성이 미치는 영향은?

  • 하나의 모듈이 변경됨에 따라 결합된 다른 모듈이 영향을 받게 된다.
  • 두개의 모듈일때는 괜찮지만 최악의경우 모듈이 100개,1000개…n 개 일때 하나의 모듈변경으로 인해 n-1개의 모듈이 영향을 받는다고 생각해보자
  • 나머지 모듈이 제대로 동작하는지에 대한 검증이 필요할 수도 있다. 그럼 시간과 비용도 n만큼??
  • 결합도가 높으면 독립성이 떨어진다. 반대로 결합도가 낮으면 독립성이 높아진다.

의존성 주입의 목적 ( 테스트, 유지보수, 재사용성)

  • 가장 큰 목적은 모듈을 Testable하게 만들수 있다는 점이다. 독립된 모듈에 대한 테스트 코드 작성
  • 하나의 모듈이 변경되어도 다른 모듈들이 영향을 받지 않으므로 유지보수가 용이
  • new를 이용한 생성자를 없애자. 모듈내에서 다른 모듈을 초기화하면 결합도가 높아지므로 객체생성은 다른곳에서 하고 생성된 객체를 참조하자.
  • 객체생성을 외부에서 하면 클래스의 독립성이 증가되며 이에따라 클래스를 테스트 가능하며, 재사용을 할 가능성도 높아진다.

결론:의존성주입은 두개이상의 모듈(클래스)간의 결합도를 낮추기 위해 외부에서 객체를 생성(new)하고 주입하는 것이다.

Dagger?

Dagger는 자바와 안드로이드를 위해 만들어진 컴파일타임 의존성 주입 프래임워크 입니다.
이전버전은 Square에서 주도적으로 개발하였으나 지금은 구글이 관리하고 있는중입니다.
대거는 추적가능한 보일러플레이트 자바 코드를 컴파일타임에 자동으로 생성하고 리플렉션 사용이 없기 때문에 많은 개발자들이 애용하고 있습니다.

Annotation Processor 이해하기

Annotation

애노테이션은 메타데이터 클래스로 다른 클래스나 메소드, 필드, 심지어 또다른 애노테이션과 결합되어 사용되어진다. 자바 애노테이션은 XML이나 Java마커 인터페이스와 처럼 추가적인 정보를 제공하는 대안이 된다.

Annotation Processor

애노테이션 프로세서는 코드를 자동으로 생성해주는 역할을 해준다. 그래서 보일러플레이트 코드를 컴파일 타임에 싹 들어 낼 수 있다.
Dagger2는 애노테이션 기반으로 동작한다. 모든 코드는 컴파일시간에 생성되며 그런이유로 퍼포먼스에 대한 오버헤드가 없고 에러에 대해 추적가능한 코드를 만들어 낼 수 있다.
자바나 안드로이드 개발하면서 제일 많이 보는 애노테이션이 @Override일것이고 ButterKnife 라이브러리를 써보았다면 @BindView또한 보았을것이다. 이런것들이 애노테이션이며, 애노테이션 프로세서에 의해 코드를 자동으로 생성하고 메타데이터를 제공하는 등의 역할을 한다.

애노테이션에 대해 좀 더 알고 싶다면 애노테이션 프로세서 만들기편을 참고 해주세요

안드로이드 스튜디오 프로젝트에 Dagger 설정하기 for kotlin

모듈레벨의 build.gradle

{
...
apply plugin: 'kotlin-kapt'
...
dependencies{
...
//Dagger2
def daggerVer = "2.14.1"
implementation "com.google.dagger:dagger-android:$daggerVer"
implementation "com.google.dagger:dagger-android-support:$daggerVer" // if you use the support libraries
kapt "com.google.dagger:dagger-android-processor:$daggerVer"
kapt "com.google.dagger:dagger-compiler:$daggerVer"
}

안드로이드 프레임워크의 4대 컴포넌트인 Activity, Service, ContentProvider, BroadcastReciever 들이 모여 하나의 애플리케이션을 이룹니다.
각 컴포넌트들은 고유의 생명주기를 가지고 있고, Application은 이런 컴포넌트보다 더 상위 개념이기 때문에 컴포넌트의 생명주기에 맞춰 의존성 주입을 하기가 좋습니다.
그러므로  Application은 싱글톤으로서의 역할을 하고, @Component 애노테이션 이용해 Component단위로 구성하고, 그 하위에 @Subcomponent 애노테이션을 이용해서 객체주입을 하게 합니다.
아래 예제에서는 하나의 액티비티가 프레그먼트를 포함하는경우에 어떤식으로 객체를 주입하는지에 대해서 다룹니다.

안드로이드용 Dagger 예제

우선 AndroidInject를 만들기 위한 인터페이스를 하나 만듭니다.
AppComponent.kt

@Singleton
@Component(modules = [(AndroidSupportInjectionModule::class )])
interface AppComponent : AndroidInjector<App> {
    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<App>()
}

인터페이스 명칭은 원하는대로 지어도 됩니다. 단 ApplicationInject를 만들때 “Dagger”라는 접두사가 붙는다는것만 기억하면 됩니다. 그러므로 인터페이스 명을 AppComponent라고 짓고 @Component를 붙이면 애노테이션 프로세서에의해 자동으로 생성되는 클래스명이 DaggerAppComponent가 될것입니다.이것을 이용하여 아래에서 AndroidInjecter를 생성할 것입니다.

App.kt

import dagger.android.support.DaggerApplication
 
class App : DaggerApplication() {
...
    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerAppComponent.builder().create(this)
    }
...
}

애플리케이션 클래스를 만드는데 Application이 아닌 DaggerApplication으로 만듭니다.
applicationInjector() 메소드를 구현하게 되는데 이때 위에서 만든 컴포넌트를 이용하여 안드로이드 인젝터를 생성해줍니다.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest package="your.package.name"
          xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools">
...
    <application
        ...
        android:name=".App"
        ...>
...
    </application>
</manifest>

애플리케이션 클래스를 만들었으면 메니페스트에 지정을 해줍니다
이것으로 애플리케이션을 싱글톤이라 보고 후에 후에 외부에서 생성되는 객체를 주입하는 하는 인젝터 생성을 끝냈습니다.
그럼 이제 객체의 생성을 담당할 Module을 만들어 보도록 하겠습니다.
MainActivity와 액티비티가 가지고 있는 MainFragment가 있다고 가정해보겠습니다.

MainActivityModule.kt

@Module
abstract class MainActivityModule {
    @Module
    companion object {
        @JvmStatic
        @Provides
        @ActivityScope
        fun provideMainActivityBinding(activity: MainActivity): MainActivityBinding {
            return DataBindingUtil.setContentView(activity, R.layout.main_activity)
        }
    }
   ...
}

@Module 애노테이션과 abstract class를 사용하여 모듈클래스를 만들어야 합니다.
액티비티에 주입될 객체는 static 메소드로 생성해야하며 @Provide 애노테이션을 붙여서 객체가 제공될것임을 명시적으로 알려줍니다. 후에 @Inject 애노테이션을 붙은 곳의 타입을 확인하고 @Provide를 통해 객체가 주입됩니다.
타입은 같은데 다른 객체의 주입을 원하는경우 @Quailifier 애노테이션을 이용하면됩니다.
해당 액티비티에서만 객체를 관리할 것이므로, @ActivityScope를 붙입니다. ActivityScope은 직접 만들어줘야 합니다. 만드는김에 FragmenScope도 미리 만듭시다.

ActivityScope.kt

@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope

FragmentScope.kt

@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class FragmentScope

MainActivity.kt

class MainActivity : DaggerAppCompatActivity() {
    @Inject
    lateinit var binding: MainActivityBinding
...
}

main_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
    </data>
    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
       ...
    </android.support.constraint.ConstraintLayout>
</layout>

MainActivity는 DaggerAppCompatActivity를 상속하도록 합니다. DaggerAppCompatActivity를 상속하기를 원치 않는 경우에는 onCreate에서 직접 AndroidInjection.inject(this@MainActivity)를 호출해야 합니다.
MainActivityModule 에서 만든 MainActivityBinding객체가 inject()를 호출하는 시점에 MainActivity내의 binding변수에 초기화되는것을 확인 할 수 있습니다. 하지만 아직은 아닙니다. 애플리케이션 레벨의 컴포넌트에서 해당액티비티에 대한 정보가 없으므로 인젝션이 실제로는 일어나지 않습니다.
AppComponent를 수정해봅시다.

AppComponent.kt

@Singleton
@Component(modules = [(AndroidSupportInjectionModule::class, ActivityModule::class)])
interface AppComponent : AndroidInjector<App> {
    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<App>()
}

ActivityModule 클래스가 추가되었습니다.

ActivityModule.kt

@Module
abstract class ActivityModule {
    @ActivityScope
    @ContributesAndroidInjector(modules = [(MainActivityModule::class)])
    abstract fun getMainActivity(): MainActivity
}

액티비티가 추가될때마다 앞으로 ActivityModule에 위와 같이 해당 액티비티를 반환하는 abstract 메소드를 만들면 됩니다.
액티비티 생명주기에 맞추어 객체를 관리 하기 위해 @ActivityScope 애노테이션이 있으며, @ContirubutesAndroidInjector 애노테이션은 반환하는 객체가 생성되는 시점에 injection을 제공할 모듈을 명시합니다. 위에서 MainActivityBinding객체를 주입하기 위해 MainActivityModule을 생성한 것을 기억하실 겁니다. 이런식으로 ContirubutesAndroidInjector 애노테이션을 써서 구성하는 이유는 다른 객체와 달리 안드로이드 프레임워크 컴포넌트(Activity, Service 등)는 개발자가 생성자를 통해  객체를 생성하는것이 아닌 전적으로 안드로이드 시스템이 관리하기 때문입니다. 
이제 Run을 눌러 앱을 실행하면, MainActivity의 binding변수가 onCreate()때 객체가 주입되는것을 확인할 수 있습니다.

NonExistentClass 관련 에러가 발생한다면 build.gradle에 다음 내용을 추가 해보시기 바랍니다.

kapt {
    correctErrorTypes = true
}

프레그먼트도 추가해보도록 하겠습니다.

MainFragment.kt

class MainFragment : DaggerFragment() {
    @Inject
    lateinit var binding: MainFragmentBinding
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = binding.root
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.textView.text = "Hello World!!"
    }
}

main_fragment.xml

<layout xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
    </data>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent">
        <TextView
            android:id="@+id/text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>
    </android.support.constraint.ConstraintLayout>
</layout>

MainFragmentModule.kt

@Module
class MainFragmentModule {
    @Provides
    @FragmentScope
    fun provideMainFragmentBinding(activity: MainActivity): MainFragmentBinding {
        return DataBindingUtil.inflate<MainFragmentBinding>(
                activity.layoutInflater,
                R.layout.main_fragment,
                null,
                false
        )
    }
}

MainActivityModule.kt

@Module
abstract class MainActivityModule {
    @Module
    companion object {
        @JvmStatic
        @Provides
        @ActivityScope
        fun provideMainActivityBinding(activity: MainActivity): MainActivityBinding {
            return DataBindingUtil.setContentView(activity, R.layout.main_activity)
        }
    }
    @FragmentScope
 @ContributesAndroidInjector(modules = [(MainFragmentModule::class)])
 abstract fun getMainFragment(): MainFragment
}

프레그먼트는 액티비티에 속하지만, 독립적인 생명주기를 가지므로 scope를 따로 관리할 필요가 있습니다.
다시 run 을 해보면 Hello World가 출력되는것을 확인할 수 있습니다.
이번엔 @Singleton 으로 관리할 객체들을 위한 모듈을 만들어 보도록 하겠습니다.
객체를 @Provide할때 scope를 @Singleton으로 설정해주면 됩니다.

AppModule.kt

@Module
class AppModule {
    @Provides
    @Singleton
    fun provideHelloWorld() = "Hello World!!"
}

AppComponent.kt

@Singleton
@Component(modules = [(AndroidSupportInjectionModule::class), (ActivityModule::class), (AppModule::class)])
interface AppComponent : AndroidInjector<App> {
    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<App>()
}

MainFragment.kt

class MainFragment : DaggerFragment() {
    @Inject
    lateinit var binding: MainFragmentBinding
   @Inject
 lateinit var txtHelloWorld: String
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = binding.root
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.textView.text = txtHelloWorld
    }
}

run 해보면 Hello World!! 가 출력되는 것을 확인할 수 있습니다.
Dagger에서는 타입을 보고 인젝션이 들어가는데 타입이 String 같은 범용적인 타입일 경우 중첩될 경우가 생길수도 있겠죠?
그럴경우 @Qualifier 애노테이션을 사용하면 됩니다.

Qualifiers.kt

기본적으로 자바에 포함된 Qualifier는 다음과 같습니다.
원한다면 자신만의 Custom Qualifier를 만들 수도 있습니다.

@Qualifier
@MustBeDocumente
@Retention(AnnotationRetention.RUNTIME)
annotation class Named(val value: String = "")

AppModule.kt

@Module
class AppModule {
    @Named("hello")
    @Provides
    fun provideHelloWorld() = "Hello World!!"
}

주입하고자 하는 객체에 수식어구를 붙입니다.

MainFragment.kt

class MainFragment @Inject constructor() : DaggerFragment() {
    @Inject
    lateinit var binding: MainFragmentBinding
    @Inject
    @field:Named("hello")
    lateinit var txtHelloWorld: String
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = binding.root
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.textView.text = txtHelloWorld
    }
}

“hello”라는 키워드를 따라 객체가 주입되는것을 확인 할 수 있습니다.

본 포스트 전체소스는 github에서 확인 가능합니다.

카테고리: 미분류

1개의 댓글

Gomdol · 2021년 7월 7일 3:44 오후

데이터바인딩 객체를 대거로부터 받아오는 과정에서 계속 헤매고 있었는데,
올려주신 글 덕분에 적용할 수 있게 되었습니다.!!
안드로이드 아키텍처 책으로 열공중인 취준생이에요ㅎㅎ
좋은 책, 글들 잘 보고 있습니다 ☺️

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.