예전부터 객체지향 프로그래밍에서는 DI를 잘 사용해야 한다는 말을 들어서인지 이번에 진행하는 새로운 프로젝트에 적용해보고 싶어서 정리를 하게 되었다. 기본에 충실하고자 이번 포스팅은 안드로이드 공식 홈페이지의 내용 + α를 바탕으로 작성하였다.
DI(Dependency Injection)란?
DI는 Dependency Injection의 줄임말이다. 한국말로 하면 의존성 주입이라고 한다.
DI의 정의는 하나의 객체가 다른 객체의 의존성을 제공하는 기술
특정 객체가 다른 객체를 필요로 할 때 외부에서 해당 객체를 생성해서 필요한 객체에게 넘겨주는 것을 의미한다. 이를 통해 프로그램의 결합도를 낮출 수 있다.
DI를 통해 개발자가 얻는 이점은 다음과 같다고 한다.
- 코드 재사용성 - 종속 항목 객체 변경이 쉬움
- Refactoring의 편의성 - 종속 항목이 구현하는 세부정보로 숨겨져 있고 노출되어 있어서 리펙터링이 편함
- Testing의 편의성 - 해당 클래스가 종속 항목을 관리하지 않으므로 다양한 사례 테스트가 가능
홈페이지에서는 하나의 예를 다음과 같이 들고 있다.
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
위와 같이 Car
라는 클래스는 Engine
이라는 클래스 참조가 필요할 때 Car
클래스 내부에서 Engine
인스턴스를 생성하여 초기화를 해준다. 하지만 이렇게 코드를 작성할 경우 다음과 같은 문제가 발생할 수 있다.
Engine
을 상속받아 구현한 여러Engine
클래스가 있다고 할 때 (GasEngine
,EletricEngine
) 다른 엔진으로 대체하기가 힘들다. 즉,Engine
을 수정하기가 힘들다.Engine
을 쉽게 수정할 수 없기 때문에 다양한 테스트를 진행하기가 어렵다.
Android에서 DI를 실행하는 2가지 방법
- Constructor Injection(생성자 삽입) - 클래스의 종속 항목 생성자 전달
- Field Injection(필드 삽입) - Activity나 Fragment 같은 클래스는 시스템에서 인스턴스화를 하기에 생성자 삽입이 불가능하다. 필드 삽입은 종속 항목을 클래스가 생성된 후 인스턴스화 된다.
✔ Constructor Injection
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
을 사용해서 테스트가 가능하다.
✔ Field Injection
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
하지만 문제가 있다.
위의 경우는 종속되는 객체가 하나였지만 만약 여러 개라면 그만큼 더 복잡하고 지루해질 것이다. 또한 종속 항목을 전달하기 전에 구성할 수 없을 때(Activity나 Fragment)는 메모리에서 종속 항목의 전체 기관을 관리하는 맞춤 Container를 작성하고 유지해야 한다.
내가 생각해도 하나 정도는 괜찮은데 앱이 복잡해지고 고도화될수록 DI 작업을 일일이 하기란 매우 벅찰 것 같다는 생각이 들었다. 그래서 Android는 이를 쉽게 해 주기 위한 라이브러리를 제공한다.
Android DI 라이브러리
안드로이드에서는 DI 라이브러리로는 대표적으로 3개가 있다.
- Hilt - Compile Time 에러 검출, Java, Kotlin에서 사용 가능
- Dagger - Compile Time 에러 검출, Java, Kotlin에서 사용 가능
- Koin - RunTime 에러 검출, Kotlin에서 사용 가능
Android 에서 DI 사용 (No 라이브러리)
일단 라이브러리를 사용하기 전에 안드로이드에서는 어떻게 기본적으로 DI를 사용할 수 있는지 알아보자!
예시는 공식 홈페이지에 있는 자료를 활용했다.
그림은 하나의 로그인을 위한 프로세스이다. A -> B는 A는 B에 종속되어 있다는 의미다.
이를 위해 다음과 같이 코드를 작성했다.
class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
class UserLocalDataSource { ... }
class UserRemoteDataSource(
private val loginService: LoginRetrofitService
) { ... }
✔ LoginActivity.kt
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)
val remoteDataSource = UserRemoteDataSource(retrofit)
val localDataSource = UserLocalDataSource()
val userRepository = UserRepository(localDataSource, remoteDataSource)
loginViewModel = LoginViewModel(userRepository)
}
}
기본적인 클래스들은 생성자 삽입으로 의존성을 주입하고 Activity는 필드 삽입으로 의존성을 주입했다. 일단 이 코드의 3가지 문제가 있다.
- 종속성을 순서대로 선언해야 한다.
- 객체를 재사용하기 어렵다.
userRepository
를 다른 곳에서 재사용하기 어렵다. - 보일러 플레이트 코드가 많다. 코드의 다른 부분에서
LoginViewModel
의 다른 인스턴스를 만들려면 코드가 중복될 수 있다.
이러한 문제를 해결하기 위해 Container로 종속성을 관리하라고 한다. Container는 모든 종속 항목을 관리할 수 있다.
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)
}
이렇게 AppContainer
클래스를 통해 종속 항목을 생성해주고 전체 Application에서 사용하기 위해 Application()
을 상속받은 클래스에 배치해준다.
class MyApplication : Application() {
val appContainer = AppContainer()
}
이렇게 하면 보일러 플레이트 코드는 줄일 수 있다. 만약 LoginViewModel
을 다른 곳에서 사용하고 싶다면 마찬가지로 LoginViewModel
을 생성해주는 Factory클래스를 만드는 것이 좋다.
interface Factory {
fun create(): T
}
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
override fun create(): LoginViewModel {
return LoginViewModel(userRepository)
}
}
그리고 AppContainer
에 포함하여 사용한다.
class AppContainer {
...
val userRepository = 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()
}
}
그런데 생각해보면 Login 데이터 같은 경우 로그인이 끝나고 다시 로그인할 때 LoginUserData
같은 경우 새로운 인스턴스를 만들어야 한다. 이 또한 Container로 만들어서 로그인 프로세스가 끝날 때 메모리에서 삭제시킨다.
class LoginContainer(val userRepository: UserRepository) {
val loginData = LoginUserData()
val loginViewModelFactory = LoginViewModelFactory(userRepository)
}
class AppContainer {
...
val userRepository = UserRepository(localDataSource, remoteDataSource)
var loginContainer: LoginContainer? = null
}
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
private lateinit var loginData: LoginUserData
private lateinit var appContainer: AppContainer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appContainer = (application as MyApplication).appContainer
appContainer.loginContainer = LoginContainer(appContainer.userRepository)
loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
loginData = appContainer.loginContainer.loginData
}
override fun onDestroy() {
appContainer.loginContainer = null
super.onDestroy()
}
}
정리해보자면 Container를 사용해 보일러 플레이트 코드를 제거하고 클래스 인스턴스들을 공유한다. 확실히 Container들을 이쁘게 잘 만들면 테스트하기도 편하고 관리하기도 용이할 것 같다.
하지만 생각해보면 앱이 복잡해지고 커지면 관리해야하는 Container들을 많아질 것이고 이는 곧 메모리 누수나 앱의 버그가 생길 수 있는 문제가 있을 것 같다. 그래서 라이브러리를 만들어서 쉽게 관리할 수 있게 하지 않았을까 생각한다.
나는 아직 라이브러리를 자세히 보지 않았지만 사용해보면 이러한 문제들이 많이 해소되지 않을까 생각한다. DI에 대해서는 이해를 했으니 실질적으로 사용해봐야겠다. 다음에는 Hilt를 사용해보고 정리하겠다.
'개발 > Android' 카테고리의 다른 글
[Android] 빌드 시에 No cached version available for offline mode 에러 해결방법 (0) | 2022.03.17 |
---|---|
[Android] ListAdapter에서 submitList()가 동작하지 않는 이유 (0) | 2022.03.10 |
[Android] ViewModel 에서 Activity, Fragment 데이터 공유하기 (0) | 2022.02.16 |
[Android] 실시간 데이터 처리를 위한 STOMP 사용하기 (Kotlin) (0) | 2022.01.26 |
[Android] 앱, 패키지 설치 여부 확인하기 (0) | 2022.01.06 |
댓글