의존성 주입에 대한 기본 이해

 

 

 

위의 그림은 안드로이드 아키텍쳐를 공부하다보면 자연스럽게 접할수 있는 그림이다

 

클린 아키텍쳐를 공부하면 자연스럽게 또 접하게 되는 것이 의존성주입(DI)이라는건데

 

의존성 주입을 통해서 코드의 재사용을 높이고, 테스트가능한코드가 생성되며, 보일러플레이트 코드가 제거된다는 장점이 있다.

 

의존성 주입에 대해서 공부하다보면 android에서 가장 인기가 많은 dagger 가 빠질수 없다.

 

kotlin 기반의 프로젝트라면 koin이라는 의존성주입 프레임워크도 많이 쓰이고 있지만

 

java 및 kotlin 프로젝트를 모두 지원하는 dagger가 인기가 가장 많다

 

이러한 라이브러리의 종류에는

런타임에서 의존성을 주입하는 방법과 

컴파일에서 의존성을 주입하는 방법이있다 

구글이 유지보수하는 dagger는 java, kotlin, android에서 사용가능한 의존성 주입 라이브러리이고

컴파일단에서 의존성을 주입해준다 

그리고 koin이라는 라이브러리는 런타임에서 의존성을 주입해준다

 

dagger는 컴파일단에서 동작하기 때문에

 

일단 코드를 제대로 완성하고 빌드에 성공했다면 적어도 런타임에서는 오류를 만드는 경우가 거의 없다

 

앞으로 여기서 말하는 dagger는 dagger 2 버전을 말하는데 dagger2 버전은 구글이 유지보수하고 있기도하다

 

그런데 dagger는 공부하기가 만만치가 않다. 러닝커브가 진짜 높다.

 

생각보다 쉬운 이해를 돕는 자료를 찾기가 어렵고, 

 

인터넷에서 주울수 있는 샘플들도 거의 비슷한 모양을 하고 있어서 다양하게 사용되는 예제를 찾기가 어렵다

 

원래 나와 비슷한 프로젝트의 비슷한 사례들을 탐구하면서 best practice를 쫓아야되는데

 

당장 내가 하고 있는 프로젝트에 맞게 어떻게하면 적용할수 있을지가 막막하다

 

그리고 적용을 하면서도 이렇게 쓰는게 맞게 쓰는건가 라는 의심을 지울수가 없다

 

나도 이제서야 dagger를 조금 이해한것 같은데

 

나의 이해를 바탕으로 앞으로 여러개의 포스팅에 걸쳐서 dagger 사용에 대한 글을 좀 더 쉽게 풀어볼까한다

 

 

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

실제로 구글은 안드로이드에서 dagger와 협력하고 있다고 공식문서에도 언급할정도로

 

dagger와 android는 밀접하게 연결되어있다

 

그리고 이 구글의 공식 문서에는

 

의존성주입을 왜 쓰는지에 대해서부터

 

다양한 의존성주입방법에 대한 설명은 물론

 

코드가 점진적으로 나아가는 과정이 아주 잘 설명되어있다

 

하지만 다시한번 말하지만 dagger는 확실히 러닝커브가 높다

 

특히 제대로 이해가 되지 않은상태에서 Subcomponent, Binds, MultiBinding으로 가면 멘붕에 빠지게 된다

 

그러니 꼼꼼하게 이해를 하면서 진행하는것이 좋다

 

그리고 이 dagger를 학습 하기 전에

 

안드로이드 클린아키텍쳐와 android viewmodel에 대해서 어느정도의 사전 이해가 필요하다

 

이에대한 사전 지식이 없으면 앞으로 진행할 예제에 대해서

 

왜 굳이 이렇게 구현을 해야되는거지 라는 의문을 가질수밖에 없다

 

 

 

자 이제부터 의존성 주입에 대해서 알아보자

 

아래의 예제와 설명은 대부분 위의 구글문서에서 가져왔으며, 이해를 돕기위해 내용을 빼거나 더했다

 

 

 

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

* 코드의 재사용
* 리팩토링 쉬움
* 테스트 쉬움

 

 

여기 Car 클래스가 있다

 

멤버 변수로 Engine()의 인스턴스가 있다

 

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

 

하지만 우리는 이러한 형태의 코드를 보고 강하게 커플링 되어있다라고 말할수 있다

 

 

이렇게 변경해보자

 

Engine()을 Car 클래스 내부에서 초기화하던것과 달리

 

생성자에 파라메터로 주입을 해주게 되면

 

Car 클래스는 Engine클래스와의 커플링이 사라진다

 

 

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

 

Car 클래스를 직접 건드릴 필요 없이

 

ElectricEngine, GasEngine같은 Engine의 서브클래스들만 넣어주면

Car클래스는 아무런 변경없이 매우 잘 동작한다

게다가 Car 클래스의 테스트를 위해 FakeEngine이라는 클래스를 넣어서 쉽게 테스트를 할수도 있다

 

방금 위의 예제 같은 경우가 생성자 형태로 의존성을 주입하는 경우이다


"Constructor injection 생성자 주입"

 

이라고 표현할수 있다

 

 

그렇다면 이번엔

"Field Injection 필드 주입"

 

이라는것을 알아보자

 

 

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

 

생성자의 파라메터로 넘겨주지도 않았으며

 

밖에서 해당 멤버변수에 직접 값을 넣는 형태를 갖는 것을 볼수 있다

 

바로 이것이 필드 인젝션이다

 

 

 

 

위의 2가지 예제들에서 코딩해보았다시피

 

생성자 주입, 필드 주입을 사용하기 위해서는 직접 원하는 종류의 engine 객체를 생성하고 주입시켜야 한다

 

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

앱이 점점 커질수록 클래스가 필요로 하는 의존성들이 많아지고, 의존성이 또 다른 의존성을 필요로 하기때문에

 

의존성주입을 위한 코드들이 늘어남에 따라 보일러플레이트 코드들이 점점 거대해진다


이러한 번거로움을 없애기 위해 자동적으로 의존성을 주입해주는 dagger와 같은 라이브러리를 사용할수 있다

 

 

 

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



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

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

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

 

 


서비스로케이터를 사용하는 방식을 알아볼까

 

 

 

의존성에 대해 알고 있는 ServiceLocator를 통해 의존성을 주입하는 방법이다


서비스로케이터 패턴은

의존성들이 api 형태가 아니라 클래스 구현할때 코딩되어야 하기때문에

 

결과적으로 클래스 바깥에서 볼때 무슨 클래스를 필요로 하는건지 알기 어렵다

그리고 객체들의 생애주기를 관리하기가 어렵다



서비스 로케이터는 위의 예제와 같은 형태로 사용한다는걸 알았으니 그럼 직접 의존성을 주입해볼까

이 과정을 직접 구현해보면서 dagger가 어떻게 자동적으로 코드를 생성해서 의존성을 주입하는지

기본원리에 대해서 이해할수 있다

 



위와 같은 안드로이드 아키텍쳐를 기반으로 구현한다고 가정하고 로그인 기능을 만들어보자

LoginActivity는 LoginViewModel에 의존적이고,

LoginViewModel은 UserRepository에 의존적이다

그리고 UserRepository는 UserLocalDataSource와 UserRemoteDataSource(retrofit을 많이 사용할것이다)에 의존적일것이다

 

 

LoginActivity는 이렇게 코딩되었을것이다

이 코드에서 문제점을 찾아보자


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

 

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

 

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

 

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


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


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


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

 


3. 객체 재활용이 어렵다


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

 



이러한 문제점을 해결하기 위해 직접 의존성 컨테이너 클래스를 만들어보자

 

 

 

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

 

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

 



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

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

 

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

 

 

 

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

 

 


그리고 다시 LoginActivity 코드를 완성해보자


훨씬 줄어든 코드가 보이는가?

 

 

한걸음 더 나아가보자

 


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

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

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


LoginViewModel을 만들기 위해 팩토리 클래스를 작성해보자

 

 

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

 

LoginActivity가 AppContainer의 인스턴스를 통해 LoginViewModel을 가져올수 있도록 하자

 

 



더 완벽해졌다

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

 


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

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

 

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

 


LoginActivity의 구현을 좀 더 세분화 하고 상세화 해보자

 

만약 LoginActivity가 LoginUsernameFragment와 LoginPasswordFragment 두개의 프래그먼트로 구성되어있다고 생각해보자 

 

LoginUsernameFragment에서는 로그인할 유저의 이름을 받고, 

 

LoginPasswordFragment에서는 로그인할 유저의 비밀번호를 받는다

 

그리고 이름과 비밀번호는 LoginUserData에 저장될것이다

 

 

 

이것을 구현할때는 다음과 같은 조건을 고려해야한다

1. 이들은 같은 LoginUserData를 로그인플로우를 마칠때까지 사용해야한다

(LoginUserData의 동일한 인스턴스를 LoginActivity, LoginUsernameFragment, LoginPasswordFragment가 공유해야만 각 프래그먼트에서 저장한 정보가 유지될것이다)

2. 새로운 로그인 플로우가 시작되면 새로운 LoginUserData 인스턴스를 생성해야한다

(LoginAcitivy를 종료했다면, 로그인 플로우를 벗어났다는 의미이므로, 기존에 사용했던 LoginUserData를 버려야하고, 다시 LoginActivity가 실행되었다면 새로운 이름과 비밀번호를 받기 위해 LoginUserData 인스턴스를 생성해야한다)

 

 

이러한 조건을 고려해볼때

로그인플로우만을 위한 새로운 LoginContainer를 만들어서

 

LoginContainer를 통해 LoginViewModel에 접근할수 있도록하고

 

LoginActivity가 생성될때 LoginContainer를 생성하고

 

LoginActivity가 종료될때 LoginContainer를 해지한다면

 

우리는 좀 더 의존성을 체계적으로 관리할수 있고,

 

메모리도 효율적으로 사용할수 있을것이다

 

 

 


AppContainer에 있던 LoginViewModelFactory를 

 

LoginContainer로 옮겨넣었고

 

LoginContainer에서만 쓰여지는 LoginUserData도 마찬가지로 접근할수 있다

 


어떤 특정 기능을 위한 container를 만들때 이 컨테이너를 언제 생성하고 삭제할지를 결정해야한다

LoginActivity가 LoginContainer를 사용할것인데 액티비티는 라이프사이클에 의해서 관리되고 있기때문에

LoginActivity가 생성될때(onCreate()) 컨테이너를 생성하고, onDestroy()될때 삭제하면된다


 

 

 


AppContainer가 모든것을 가지지 않고 각 컨테이너가 컨테이너에게 필요한것들을 갖게끔 변경되었다

 

LoginActivity에서 appContainer에 접근했던것 처럼

 

fragment들에서도 activity에서 생성한 LoginContainer의 인스턴스를 AppContainer 인스턴스로 부터 얻을수 있다

 

 

여기까지의 예를 통해 의존성 주입이 안드로이드앱을 확장가능하게 하고 테스트가능하게 만든다는것을 보았다

 


하지만 이렇게 직접 의존성을 만들고 주입하는 방법을 사용한다면

 

만약 앱이 더 커진다면 팩토리와 같은 많은 보일러플레이트 코드를 매번 작성하는 자신을 발견하게 될것이고

 

이러한 행위는 잠재적인 에러를 발생시킬가능성이 높고

 

각각 만든 container들의 라이프사이클을 직접 관리해야한다라는 문제점들을 안고 있다

 

그렇지 않으면 메모리릭이 생길수 있다


 

이제 다음에는 dagger를 통해서 직접 의존성을 만들고 주입했던것을 효율적으로 하는 방법을 알아보자

| 1 | ··· | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ··· | 1837 |