[DI,Android] Dagger2, Hilt 없이 의존성 주입 사용해보기

 

스터디 목적

DI의 원리를 이해하기 위해서 Dagger2와 Hilt 같은 의존성 라이브러리 없이 로그인 플로우를 구현하는 방식을 배운다.

의존성 라이브러리를 사용하지 않고 구현했을 경우, 차이점과 필요성을 알 수 있다.

우선 프로젝트 구조는 아래와 같다.

 

[ Login Activity ]
   - > [ Login ViewModel ]
              - > [ UserRepository ]
                              - > [ UserLocalDataSource ]

                              - > [ UserRemoteDataSource ]
                                                - > [ Retrofit ]

 

Login Activity에서 사용자가 데이터에 접근하면, Login ViewModel에 접근할 수 있다.

Login Viewmodel은 userRepository를 생성해서 기능을 사용하게 되는데,

userRepository는 localDataSource와 remoteDataSource 클래스를 생성한다.

userRemoteDataSource는 loginService 인터페이스를 통해서 레트로핏으로 서버에 접근하게 된다.

 

각 기능 별 내부 기능은 작성하지 않았지만, 기능의 구현 방식을 이해하기 위해서 빈 클래스로 제작하였다.

 

Login ViewModel

class LoginViewModel(
    private val userRepository : UserRepository
) {
}

 

UserRepository

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

}

 

UserLocalDataSource

class UserLocalDataSource {

}

 

UserRemoteDataSource

class UserRemoteDataSource(
    private val loginService: LoginService
) {

}

 

UserService( Retrofit)

// 로그인 통신을 위해서 구현하는 인터페이스
interface LoginService {
}

 

기존의 LoginActivity

 

기존의 로그인 액티비티에서 아래와 같이 코드를 작성했다.

여러가지 문제가 발생했는데, 가장 큰 문제는 재사용성이 떨어지는 것이다.

다른 곳에서 remoteDataSource나 localDataSource를 사용할 경우, 매번 코드를 작성해야 한다.

또 UserRemoteDataSource의 경우 retorfit을 인자로 받는데, 으를 또 LoginService로 전달해 주어야 한다.

이러한 코드를 작성한 경우 재사용 성이 떨어질 뿐아니라, 보일러 플레이트가 발생하게 되는데,

이는 다른 곳에서 값을 넣어주거나 받아오는 경우가 반복되는 비효율적인 코드가 생성된다.

마찬가지로 userRepository와 loginViewModel의 경우에서도 이런 문제가 발생한다.

 

// 로그인 액티비티 1 -> 보일러 플레이트 발생 + 재사용성 떨어짐
class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

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

        // In order to satisfy the dependencies of LoginViewModel, you have to also
        // satisfy the dependencies of all of its dependencies recursively.
        // First, create retrofit which is the dependency of UserRemoteDataSource
        val retrofit = Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService::class.java)

        // Then, satisfy the dependencies of UserRepository
        val remoteDataSource = UserRemoteDataSource(retrofit)
        val localDataSource = UserLocalDataSource()

        // Now you can create an instance of UserRepository that LoginViewModel needs
        val userRepository = UserRepository(localDataSource, remoteDataSource)

        // Lastly, create an instance of LoginViewModel with userRepository
        loginViewModel = LoginViewModel(userRepository)
    }
}

 

 

AppContainer와 MyApplication 생성

 

좀 더 나은 코드를 구성하기 위해서 앱 전체에 걸쳐서 사용할 수 있는 코드를 작성해야 한다.

우선 AppContainer를 생성하고, 기존 LoginActivity에서의 기능을 구현했다.

 

class AppContainer {


    // Since you want to expose userRepository out of the container, you need to satisfy
    // its dependencies as you did before
    private val retrofit = Retrofit.Builder()
        .baseUrl("https://example.com")
        .build()
        .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    // userRepository is not private; it'll be exposed
    val userRepository = UserRepository(localDataSource, remoteDataSource)

    // 뷰 모델을 인스턴스화
    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

 

앱컨테이너에서는 레트로핏을 생성하고 remoteDataSource, localDataSource, userRepository를 작성한다.

이전과의 차이점은 LoginViewModelFactory 클래스를 사용한 것이다.

해당 클래스는 뷰 모델을 인스턴스화하며, 이를 위해서 객체를 생성하는 인터페이스를 구현해야 한다.

 

// 팩토리 정의 ( 뷰 모델을 인스턴스화 하기 위함 )
// 객체를 생성하기 위한 인터페이스를 만들고, 인터페이스를 구현하는 클래스에서 어떤 객체를 만들지 결정하는 패턴
// 객체를 만들기 위한 인터페이스를 선언해서 유연하게 구현해 사용할 수 있도록 하는 패턴
interface Factory<T> {
    fun create(): T
}



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

 

이는 구조를 유연하게 해주며, 팩토리를 통해서 뷰 모델을 반환한다.

이를 앱 컨테이너에서 인스턴스화 해주어서, 뷰 모델 역시 앱 전체에서 사용할 수 있도록 선언한다.

 

AppContainer는 Application에서만 사용할 수 있으므로, 이를 위한 MyApplication 역시 생성한다.

 

class MyApplication : Application() {

    // 애플리케이션 내부에서 앱 컨테이너 인스턴스화
    val appContainer = AppContainer()
}

 

 

의존성을 주입한 LoginActivity

 

이전과 다른 로그인 액티비티가 작성되었다.

이는 재사용성을 증가시키는 앱 컨테이너를 활용하였다.

싱글톤으로 구현하지 않고, 모든 액티비티에서 공유되는 AppContainer를 통해서

UserRepository를 필요로 하는 모든 액티비티에 인스턴스를 제공할 수 있다.

 

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

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

        // Gets userRepository from the instance of AppContainer in Application
        val appContainer = (application as MyApplication).appContainer
        // 미리 생성해둔 로그인 팩토리를 통해서 뷰 모델에 접근
        loginViewModel = appContainer.loginViewModelFactory.create()
    }
}

 

 

  두 코드의 차이점


[첫번째 코드]
보일러플레이트가 많다 (변화없이 여러 군데에서 반복되는 코드)
- 다른 부분에서 LoginViewModel의 다른 인스턴스를 만들려면 중복된 코드 발생
객체를 재사용하기 어려움
- 여러 부분에서 UserRepository를 재사용하려면 싱글톤 패턴을 따르게 해야 함
- 하지만 싱글톤으로 구현해도 모든 테스트가 동일한 인스턴스를 공유 -> 다양한 시나리오 테스트 어려움

[두번째 코드]
Container로 Dependency 관리

- 객체 재사용 문제 해결
- 자처젝인 Dependency Container 클래스 생성
- 이 컨테이너에서 제공하는 인스턴스는 외부로 공개할 수 있음
- UserRepository 인스턴스를 public 상태로 둠

 

 

다른 문제 발생

Container를 활용해서 재사용성이 높아졌다고 생각했지만, 아래와 같은 문제가 발생한다.

 

1. AppContainer를 직접 관리하기 때문에 모든 인스턴스를 수동으로 생성해야 한다.

2. 여전히 많은 보일러 플레이트 코드를 가지고 있다.

3. 객체를 재사용할 지에 따라 팩토리, 파라미터도 생성해야 한다.

4. 프로젝트에 기능이 많이 포함 될 경우 복잡해지는 문제가 발생한다.

 

이 경우 로그인 플로우를 위한 전용 컨테이너를 제작해서 문제를 해결할 수 있다.

하지만 새로운 경우를 적용한다 해도, 앱이 커질 수록 오류 발생 가능성이 커지며, 자잘한 버그와 메모리 낭비가 발생할 것이다.

 

이 모든 문제들을 해결하기 위해서 Dagger, Hilt와 같은 DI 라이브러리를 사용하는 것이다!/