[DI/Android] 의존성 주입 (Dependency Injection)
1. Dependency
Dependency는 '의존성'을 의미한다.
의존성이란, 하나의 객체가 다른 객체에 의존하는 것 (= 하나의 객체가 어떤 용도로 다른 객체에 필요한 것)
class Worker {
private car = Car()
fun Commute() {
car.drive()
}
}
Worker가 있고 출퇴근을 Car로 한다고 한다. 이때, Worker 객체가 Car 객체에 의존한다고 할 수 있다.
→ Worker 객체 상에 Car의 객체가 존재하므로 Worker 객체가 생성되면 Car 객체는 계속 존재해야 하는 것이다.
→ Worker 객체가 Commute()하기 위해 Car 객체가 필요하다 == Worker가 Car에 의존
여기서 생길 수 있는 문제는 Car가 아니라 이제부터 Bus를 탄다고 가정했을 때이다.
class Worker {
private bus = Bus()
fun Commute() {
bus.take()
}
}
위의 코드 처럼 Car가 Bus로 바뀌고 Commute() 안의 메서드도 변경된 것을 볼 수 있다. 따라서 의존하는 객체에 변경이 있는 경우 의존 객체를 사용하고 있는 모든 곳의 코드를 변경해주어야 한다. 지금은 매우매우 간단한 코드여서 쉽게 변경이 가능했지만, 그 범위가 프로젝트로 확장된다면 엄청난 수정이 필요할 것이다.
즉, '의존'은 코드의 재사용성을 어렵게 하며 유지보수를 어렵게 한다.
2. Dependency Injection
Dependency Injection은 '의존성 주입'으로, 흔히 DI라고 부른다.
DI는 객체 간의 의존 관계를 객체-객체가 아닌 외부에서 객체를 생성하고 전달하여 의존성을 제거하고 결합도를 낮추는 것을 말한다.
즉, 의존하는 클래스를 직접 생성하는 것이 아니라 생성자와 메서드를 통해 외부에서 주입하는 방식이다.
interface Transportation {
fun ride()
}
class Worker(trans: Transportation) {
private var trans: Transportation = trans
fun changeTransportation(new: Transportation) {
this.trans = new
}
fun commute() {
trans.ride()
}
}
위의 방식대로 한다면 changeTransportation()에 의해 Worker은 보통의 통근 수단을 Car, Bus 등 다양한 교통수단으로 특별한 코드 수정 없이 바꿀 수 있을 것이다. 또한 각 Car과 Bus 등의 교통수단의 종류를 나타낸 클래스에서는 Transportation 인터페이스의 ride()를 오버라이딩하여 각 클래스에 맞게 구현하여 사용하면 된다.
3. DI in Android
- 생성자 주입 (Constructor Injection) : 생성자를 통해 의존하는 객체를 전달하는 방식
- 필드 / Setter 주입 (Field / Setter Injection) : 객체가 초기화된 후, 메서드를 통해 의존하는 객체를 전달하는 방식
Android는 context의 영향을 많이 받는다. 가장 큰 예로는 Activity의 LifeCycle에 따라 자원을 생성하고 사용하는 것이다. 즉, Activity, Fragment 내에 선언되고 사용되는(의존하는) Instance들은 Activity, Fragment의 영향을 받는다. 따라서 다른 곳에서 Instance를 생성하고 Activity나 Fragment에서는 생성된 Instance를 받아서 사용하면, 내부 환경과는 상관없이 같은 동작을 하는 범용적이고 재사용 가능한 Instance 사용을 할 수 있다.
하지만 이러한 DI도 구조가 복잡해지면 boilerplate code가 많아지고, lazy initialization 등에 의해 의존성을 전달하기 전에는 의존성을 구성할 수 없는 경우가 생기는 등의 문제점이 발생한다. 이러한 문제점을 해결하기 위해 DI를 자동화하는 라이브러리들이 있다. Android 공식 문서에서는 Dagger2를 사용하는 것을 권장하고 있지만, 러닝커브가 낮은 라이브러리로는 Koin 또한 많이 사용되고 있다.
Dagger2
- 환경 세팅 과정과 원활한 적용에 필요한 러닝커브가 큼
- compile time에 Annotation을 통해 의존성을 주입함 → 컴파일 시점에서 에러
- 컴파일에 필요한 시간이 늘어나므로 UX를 해치기 보다는 개발자의 시간이...
- 빌드가 완료된 파일은 DI 면에서 안정성이 보장됨
Koin
- Kotlin DSL로 만들어짐
- Dagger2에 비해 러닝커브가 낮아 빠르게 적용 가능
- run time에 의존성을 주입함 → 런타임 에러 발생, 컴파일 시 오류 확인 어려울 수 있음(테스트로 보강)
- 런타임에 의존성을 주입하다보니 앱 성능이 저하되기 때문에 큰 규모의 프로젝트에서는 적합하지 않음
Hilt
- Dagger를 기반으로 만들어졌으며 비교적 러닝커브가 낮음
- Java, Kotlin을 모두 지원함
- compile time에 에러를 검출함하므로 안정성이 있음
Dependency Injection in Android
위에서 비교한 것 처럼 Dagger의 경우, 가 Annotation에 대한 역할, module & component 간의 관계 등 라이브러리에 대한 많은 이해가 필요하므로 러닝커브가 높다. 이에 대해 상대적으로 러닝커브가 낮고, 사용이 용이한 Koin이 많은 인기를 얻고 있지만, DI의 개념보다는 Kotlin의 DSL을 활용한 패턴에 가깝기 때문에 프로젝트 규모가 커질수록 Dagger에 비해 런타임 퍼포먼스가 떨어진다. 이에 많은 안드로이드 개발자분들이 DI 라이브러리에 대해 피드백을 제시했고, 초기 DI 환경 구축 비용을 크게 절감시키는 것이 큰 목적인 Hilt가 나오게 되었다.
따라서 Hilt에 대한 개념을 이해하고, 어떻게 사용하는 것인지에 대해 포스팅을 할 예정이다.
언젠가 프로젝트에서도 사용할 수 있기를✨
reference >
- https://developer.android.com/training/dependency-injection/manual
- https://salix97.tistory.com/264
- https://velog.io/@jojo_devstory/DIDependency-Injection%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90
- https://medium.com/@lazysoul/dependency-injection%EC%9D%B4%EB%9E%80-7ff65bdf624