Android

Dagger란? -dagger 시작

park_juyoung 2020. 4. 21. 23:30

Dagger2 란 무엇일까?

Dagger는 자바와 안드로이드에서 의존성 주입을 구현하기 위해 사용하는 프레임 워크 입니다.

의존성 주입(Dependency Injection)이란?

DI는 Dependency Injection의 약자로 의존성 주입을 의미합니다. 구성요소간의 의존 관계가 내부가 아닌 외부를 통해 정의되게 하는 디자인 패턴 중의 하나입니다. 의존성 주입의 목적은 객체를 생성하고 사용하는 관심사를 분리하는 것입니다.

위에 그림과 같이 내부가 아닌 외부에서 객체를 생성해서 주입하는 것을 의미합니다.

의존성 주입은 다음과 같은 장점이 있습니다.

  • 코드의 재사용
  • 리팩토링 쉬움
  • 테스트 쉬움
  • 보일러플레이트 코드 감소

의존성을 주입할수 있는 방법은 여러가지가 있습니다.

* 직접 의존성 주입하기 : 작은 프로젝트에서 사용가능하다, 프로젝트가 커질수록 많은 보일러플레이트 코드가 필요하다

* 서비스로케이터 : 상대적으로 적은 보일러플레이트코드가 필요하지만 여전히 확장가능하지는 않다. 싱글톤오브젝트에 의존해 있기 때문에 테스트가 어렵다

*의존성주입 라이브러리 사용(dagger) : 좋은 방법이다

class Car {

    private val engine = Engine()

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

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

위와 같은 Car 클래스는 맴버 변수로 Engine()의 인스턴스가 있습니다.

이렇게 코드를 작성해도 이 클래스는 매우 잘 동작합니다.

하지만 이러한 형태의 코드를 보고 강하게 커플링 되어있다라고 말합니다.

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

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

이렇게 코드를 변경하면 Engine()을 Car 클래스 내부에서 초기화하던것과 달리

생성자에 파라메터로 주입을 해주게 되면 Car 클래스는 Engine클래스와의 커플링이 사라집니다.

다른 종류의 Engine을 달고 있는 Car 인스턴스를 생성하기 위해서 Car 클래스를

직접 건드릴 필요 없이 Engine의 서브클래스들만 넣어주면 Car 클래스는 변경없이 잘 작동합니다.

위와 같은 예제는 생성자 형태로 의존성을 주입하는 경우입니다.

"Constructor injection 생성자 주입"

이라고 표현합니다.

 

"Field Injection 필드 주입"
에 대해서 살펴 보겠습니다.

class Car {
    lateinit var engine: Engine

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

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

Engine()의 인스턴스를 Car 클래스에서 직접 생성하지 않았고

생성자의 파라메터로 넘겨주지도 않았으며 밖에서 해당 멤버변수에 직접 값을 넣는 형태를 갖는 것을 볼 수 있습니다.

이것이 필드 인젝션 입니다.

위에 예제에서 보듯이 생성자 주입, 필드 주입을 사용하기 위해서는 직접 원하는 종류의 engine을 생성하고 주입해주어

야합니다.

작은 앱에서는 직접 객체를 생성해서 넣어주는 작업들을 할 수 있지만 앱이 커질수록

클래스가 필요로 하는 의존성들이 많아지고

의존성이 또다른 의존성을 필요로 하기 때문에 의존성주입을 위한 코드들이 늘어남에 따라 보일러플레이트 코드들이

점점 거대해집니다.

 

서비스로케이터

 

다음은 서비스로케이터를 사용하는 방식을 알아보겠습니다.

 

object ServiceLocator {
	fun getEngine(): Engine = Engine()
}


class Car {
	private val engine = ServiceLocator.getEngine()
    
    fun start() {
    	engine.start()
    }
}

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

의존성에 대해 알고 있는 ServiceLocator을 이용하여 의존성을 주입하였습니다.

 

서비스로케이터 패턴은 클래스 구현할때 코딩되어야 하기떄문에 결과적으로 

바깥에서 볼떄 무슨 클래스를 필요로 하는건지 알기 어렵습니다.

 

직접 의존성을 주입하면서 Dagger가 어떻게 자동적으로 코드를 생성해서 의존성을

주입하는지 알아 보겠습니다.

 

MVVM 아키텍쳐를 기반으로 로그인 기능을 만들어보겠습니다.

 

LoginActivity는 LoginViewModel에 의존적이고,

LoginViewModel은 UserRepository에 의존적입니다.

 

그리고 UserRepository는 UserLocalDataSource와 UserRemoteDataSource 에 의존 적일것입니다.

 

class UserRepository(
	private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
){...}

class UserLocalDataSource{...}
class UserRemoteDataSource(
	private val loginService: LoginRetrofitService
} {...}

class LoginActivity: Activity() {
	
    private lateinit var loginViewModel: LoginViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
        
        val retrofit = Retrofit.Builder()
        			   .baseUrl("http://example.com")
                       .build()
                       .create(LoginService::class.java)
        
        val remoteDataSource = UserRemoteDataSource(retrofit)
        val localDataSource = UserLocalDataSource()
        
        val userRepository = UserRepository(localDataSource, remoteDataSource)
        
        loginViewModel = LoginViewModel(userRepository)
    }
}

이 코드에서 문제점은

 

1. 많은 보일러플레이트 코드가 있습니다.

 

만약에 다른곳에서 LoginViewModel의 인스턴스를 생성해야할일이 있다면

 

LoginViewModel을 만들기 위한 보일러플레이트코드들을 다시 사용해야합니다.

 

이것은 많은 코드가 여러곳에 중복으로 존재할것이라는 것을 의미합니다.

 

2. 의존성이 순서에 맞게 선언되어어야 합니다.


LoginViewModel을 만들기 위해서 필요한 UserRepository를 먼저 생성해야하듯이


더 복잡한 의존성들로 엮여 있다면 A를 만들기 위해 B를 생성해야되고 B를 만들기 위해 C를 생성해야하고 이런식의 패턴에서는 작성하는 코드의 순서마저 너무 중요해집니다.

 


3. 객체 재활용이 어렵습니다.


UserRepository와 같은 클래스는 같은 인스턴스를 재활용하는 것이 메모리 측면에서 더 나을것으로 보입니다.

 

이러한 문제점을 해결하기 위해 직접 의존성클래스를 만들어 보겠습니다.

 

 

class AppContainer { 

	private val retrofit = Retrofit.Builder()
    					   .baseUrl("https://example.com") 
                           .build() 
                           .create(LoginService::class.java)
                           
    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource() 
    
    val userRepository = UserRepository(localDataSource, remoteDataSource)
}

이 컨테이너는 앱 전체에서 공유되는 클래스입니다.

 

userRepository는 다른 곳에서 접근할수 있도록 public하게 되어있습니다

 



이렇게 만든 컨테이너는 어플리케이션 전체에서 사용되어져야 하기 때문에

모든 액티비티들이 접근해서 사용할수 있는 위치에 놓아야합니다

 

바로 안드로이드 앱의 경우 Application 클래스입니다.

 

 

class MyApplication : Application() {
	val appContainer = AppContainer()
}

 

AppContainer 인스턴스를 통해서 UserRepository 인스턴스를 얻을수 있게 되었습니다.

그리고 다시 LoginActivity 코드를 완성해보겠습니다.

 

class LoginActivity : Activity() {
    
    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val appContainer = (application as MyApplication).appContainer
        loginViewModel = LoginViewModel(appContainer.userRepository)
    }
}

코드가 상당히 줄어든 것을 볼수 있습니다.

 

여기서 LoginViewModel을 LoginActivity에서 직접 생성하고 있지만

 

만약 LoginViewModel을 앱 여러곳에서 사용한다면

 

LoginViewModel 클래스를 생성하는것도 한곳에서 관리하는것이 합리적입니다.


LoginViewModel을 만들기 위해 팩토리 클래스를 작성해보겠습니다.

interface Factory {
	fun create(): T
}

class LoginViewModelFactory(private val userReopository: UserRepository) : Factory{
	override fun create(): LoginViewModel {
    	return LoginViewModel(userRepository)
    }
}

그리고 이렇게 만든 팩토리 역시 AppContainer 클래스에 넣어서

 

LoginActivity가 AppContainer의 인스턴스를 통해 LoginViewModel을 가져올수 있도록 하겠습니다.

 

class AppContainer {
	
    val userReopository = UserRepository(localDataSource, remoteDataSource)
    
    val loginViewModelFactory = LoginViewModelFactory(userRepository)
    
}

class LoginActivity: Activity() {
	
    private lateinit var loginViewModel: LoginViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
        
        val appContainer = (application as MyApplication).appContainer
        loginViewModel = appContainer.loginViewModelFactory.create()

하지만 몇가지 남은 고려해볼만한 미션들이 있습니다.

 


1. 여전히 AppContainer 객체는 직접 관리를 해야한다 그리고 모든 의존성을 위한 인스턴스를 직접 만들어야한다

2. 아직 보일러플레이트코드가 남아있다

 

일단 이러한 잠재적인 문제점이 있다고만 알고 있고 지금 단계에서는 일단 넘어가겠습니다.

 

출처: 

https://trend21c.tistory.com/2112?category=218613



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