안드로이드 dependency(Dagger 2)

이영준·2022년 11월 4일
0

어떠한 클래스(차)를 만들때 생성자에서 다른 클래스 인스턴스(엔진)를 불러와 생성을 하게 된다면, 차의 dependency는 engine이게 된다. 우리는 retrofit을 사용하기 위해 dependency를 주입하고, room을 사용하기 위해서도 주입한다. 중요한 것은 이 의존성들이 결합도를 낮게 가져가야 예기치 못한 문제 상황에서 해결하기가 수월하다는 것이다. dagger는 의존성 주입을 통해 서로 의존적인 클래스들의 외부 선언을 일일히 하지 않도록 도와준다.

초기 설정

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
implementation 'com.google.dagger:dagger:2.40.5'
    kapt 'com.google.dagger:dagger-compiler:2.27'

https://github.com/google/dagger 최근 버전을 여기서 확인한다.

@Inject, @Component

간단한 예로 스마트폰 클래스를 만들어보았다.

class SmartPhone (val battery: Battery, val simCard: SIMCard, val memoryCard: MemoryCard) {

    init {
        battery.getPower()
        simCard.getConnection()
        memoryCard.getSpaceAvailablity()
        Log.i("MYTAG", "SmartPhone Constructed")
    }

    fun makeACallWithRecording() {
        Log.i("MYTAG", "Calling.....")
    }
}

스마트폰을 만들기 위해서는 배터리, 심카드, 메모리카드가 있어야 하므로 battery,simCard, memoryCard에 의존적이다. 또한 simCard역시 serviceProvider 클래스를 인자로 받아 만들 수 있기 때문에 이에 의존적이다.

class SIMCard (private  val serviceProvider: ServiceProvider) {


    init {
        Log.i("MYTAG","SIM Card Constructed")
    }

    fun getConnection(){
        serviceProvider.getServiceProvider()
    }
}

각각의 클래스에 @Inject constructor로 생성자를 명시해준다.
심카드 클래스의 경우,

class SIMCard @Inject constructor(private  val serviceProvider: ServiceProvider) {


    init {
        Log.i("MYTAG","SIM Card Constructed")
    }

    fun getConnection(){
        serviceProvider.getServiceProvider()
    }
}

그리고 SmartPhoneComponent 인터페이스 클래스를 만들어 가장 상위 클래스인 SmartPhone을 부를 수 있는 클래스를 만들고 Component 애너테이션을 해준다.

@Component
interface SmartPhoneComponent {
    fun getSmartPhone() : SmartPhone
}

re-build를 해주면 dagger에서 자동으로 injection에 해당하는 클래스들의 의존성을 적어주며, 이제 main을 수정해주면 된다.

smartPhone 객체를 부르는 메인 액티비티를 수정해준다.

class MainActivity : AppCompatActivity() {
    private lateinit var smartPhone: SmartPhone

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        DaggerSmartPhoneComponent.create()
            .getSmartPhone()
            .makeACallWithRecording()


//        val smartPhone = SmartPhone(
//            Battery(),
//            SIMCard(ServiceProvider()),
//            MemoryCard()
//        )
//            .makeACallWithRecording()

    }
}

일일히 Battery() 등의 클래스 생성을 하지 않고 getSmartPhone메서드로 SmartPhone을 만들고 이의 makeACall을 실행시키는 같은 방법으로 동작하는 함수가 완성되었다.

클래스 변경을 할 수 없을 때

많은 경우에서는 우리가 클래스를 직접 수정하는 것이 불가능할 때가 있다. 예를 들어 Retrofit을 사용할 때 등이 있는데, 이때는 새로 인스턴스를 만들어주기 위한 module을 만들어 @Provides 애너테이션을 넣음으로서 대신할 수 있다.

class MemoryCard{
    init {
        Log.i("MYTAG","Memory Card Constructed")
    }

    fun getSpaceAvailablity(){
        Log.i("MYTAG","Memory space available")
    }
}

MemoryCard 클래스가 위처럼 inject를 하지 않는 다고 가정해보자.

MemoryCardModule

@Module
class MemoryCardModule {
    @Provides
    fun providesMemoryCard(): MemoryCard{
        return MemoryCard()
    }
}

위가 자신에게 의존적인 클래스에 자신을 전달한다는 의미의 @inject constructor 와 같은 역할을 하는 클래스이다.

인터페이스에 의존성 넣을 때

interface Battery {
    fun getPower()
}

기존의 배터리 클래스를 인터페이스로 바꾸고 로직을 따로 구현할 때는 Inject가 있더라도 따로 모듈을 만들어주어야 한다.

NickelCadmiumBattery.kt

class NickelCadmiumBattery @Inject constructor():Battery {
    override fun getPower(){
        Log.i("MYTAG" , "Power from NickelCadmiumBattery")
    }
}

NCBatteryModule

@Module
class NCBatteryModule {
    @Provides
    fun providesNCBattery(nickelCadmiumBattery: NickelCadmiumBattery):Battery{
        return nickelCadmiumBattery
    }
}

심지어 어차피 Battery 형이 nickelCadmiumBattery 하나만 있으므로 추상 클래스로 만들어

@Module
abstract class NCBatteryModule {
    @Binds
    abstract fun bindsNCBattery(nickelCadmiumBattery: NickelCadmiumBattery):Battery
}

위처럼 만들어줘도 된다.

Field Injection

SmartPhoneComponent의 getter 메서드로 필요한 액티비티마다 이 메서드를 콜하는 것을 막기 위해 field injection이라고 하여 클래스 안에 의존성 주입을 해줄 수 있다.

@Component(modules = [MemoryCardModule::class,NCBatteryModule::class])
interface SmartPhoneComponent {
   fun inject(mainActivity: MainActivity)
}

먼저 컴포넌트 클래스를 위처럼 수정하여 액티비티를 인자로 받는 inject 함수를 만들고,

@Inject
lateinit var smartPhone: SmartPhone

main에서 smartPhone 클래스를 inject해준다음 component의 inject함수를 불러 smartPhone의 메서드를 가져올 수 있도록 한다.

class MainActivity : AppCompatActivity() {
@Inject
lateinit var smartPhone: SmartPhone
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        DaggerSmartPhoneComponent.create()
            .inject(this)
        smartPhone.makeACallWithRecording()
}

의존적인 클래스에 파라미터가 있을 때

@Module
class MemoryCardModule(val memorySize:Int) {

    @Provides
    fun providesMemoryCard():MemoryCard{
        Log.i("MYTAG" , "Size of Memory is $memorySize")
        return MemoryCard()
    }
}

메모리카드 모듈에 위와 같이 Size라는 인자를 받는다면 기존처럼 create() 함수만으로 smartPhone을 만들 수 없다. 이럴 때에는 create() 대신 builder() 를 사용해준다.

class MainActivity : AppCompatActivity() {
@Inject
lateinit var smartPhone: SmartPhone
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        DaggerSmartPhoneComponent.builder()
            .memoryCardModule(MemoryCardModule(1000))
            .build()
            .inject(this)

    }
}

Application class

main activity에서 smartphone을 부르기 위한 build() 등등의 일련의 코드를 여러 액티비티에서 사용하고 싶다면 application class를 만들어 사용해주어야 한다.

SmartPhoneApplication.kt

class SmartPhoneApplication : Application() {
    lateinit var smartPhoneComponent: SmartPhoneComponent
    override fun onCreate() {
        super.onCreate()
        smartPhoneComponent = initDagger()
    }

    private fun initDagger() : SmartPhoneComponent = DaggerSmartPhoneComponent.builder()
        .memoryCardModule(MemoryCardModule(1000))
        .build()
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
@Inject
lateinit var smartPhone: SmartPhone
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        (application as SmartPhoneApplication).smartPhoneComponent
            .inject(this)
	}
}

manifest.xml

<application
        android:name=".SmartPhoneApplication" ...

클래스 한번만 생성 @Singleton

rotateapp과 같은 상황에서 oncreate안의 inject 함수를 통하여 smartPhone이 다시 재생성되는 불상사를 막기 위해 singleton 애너테이션으로 클래스를 구성할 수 있다

smartPhone.kt

@Singleton
class SmartPhone  @Inject constructor(val battery: Battery, val simCard:
.
.
.

smartPhoneComponent

@Singleton
@Component(modules = [MemoryCardModule::class,NCBatteryModule::class])
interface SmartPhoneComponent {
   fun inject(mainActivity: MainActivity)
}
profile
컴퓨터와 교육 그사이 어딘가

0개의 댓글