더이상 피할 수 없다 의존성 주입....!!
꼭 이해하고 싶었던 개념이었고 새로운 프로젝트의 규모도 점점 커지고 있어서 꼭 필요하겠다는 생각에 드디어 공부하고 정리!!
의존성 주입이란 무엇이고 왜 쓰는가?
- 클래스 내부에서 직접 다른 클래스를 생성해 사용하는 것이 아닌 외부에서 생성 후 생성된 객체를 주입만 하는 것
- 공통적으로 사용하는 클래스에 대해서 의존성 주입을 통해 한 번에 관리가 가능하고 변화에 유연하게 대응할 수 있다
- 나름대로 MVVM 패턴이나 클린아키텍쳐를 구현하려고 노력하면서 독립된 계층이 존재하고 각 계층간의 결합도는 낮아지고 응집도는 높아져야 하는 특성을 지니는데 이때 의존성 주입을 사용
- 예를 들어 domain 계층에서 data 계층에 속하는 repository를 사용하기 위해 domain 계층에서는 repository interface 를 사용하고 구현체는 data 계층에 작성한다. 그리고 필요한 필요한 구현체 주입은 DI 컨테이너에서 한다. 만약 domain 계층에서 사용해야 하는 repository 구현체가 바뀌어도 domain 계층은 전혀 건들일 필요 없이 여전히 repository interface를 갖고 있으면 된다. 어떤 구현체를 생성해 주입할 지는 외부에서 진행하는 것이다. 즉, domain 계층이 data 계층의 변황에 대해 알 필요가 없다. (물론 아예 다른 인터페이스를 필요로 하게 되면 domain 계층에서의 인터페이스 변경이 필요할 것 같다. 하지만 이런 수정이 자주 발생하지 않도록 )
Car 객체는 OilEngine 객체를 포함하고 있다. 그런데 OilEngine 객체가 ElectronicEngine 객체로 바뀐다면? 그 이후에도 다른 형태의 Engine 클래스를 사용하게 된다면?
OilEngine 와 ElectronicEngine 객체의 각기 다른 이름의 메서드를 사용하기 위해서 모든 코드를 수정해야 한다.
fun main(args: Array) {
val car = Car()
car.start()
}
class Car {
val engine: Engine = ElectronicEngine()
OR
val engine: Engine = OilEngine()
OR
val engine: Engine = GasolineEngine()
fun start() {
engine.startEngine() //어떤 Engine 클래스가 들어오던 같은 메서드 호출 가능
}
}
interface Engine {
fun startEngine()
}
class ElectronicEngine: Engine {
override fun startEngine() {
print("startElectronicEngine!!")
}
}
class OilEngine: Engine {
override fun startEngine() {
print("startOilEngine!!")
}
}
class GasolineEngine: Engine {
override fun startEngine() {
print("startGasolineEngine!!")
}
}
우리는 이 문제를 인터페이스와 상속으로 해결할 수 있다. (다형성과 재사용성 보장)
이렇게 Engine 인터페이스를 생성하고 impelement 하여 다양한 Engine 클래스로 확장시킬 수 있으며 메서드 또한 어떤 Engine 클래스가 들어오던 같은 메서드로 호출이 가능해진다. 이로서 Engine클래스가 바뀔때마다 engine.startEngine() 를 매번 바꿔야 한다는 문제를 해결할 수 있어진다.
하지만 지금은 단순히 Car 클래스 하나에서만 Engine 객체를 생성해 사용한다. 그런데 만약 다양한 종류의 car 클래스가 존재한다면?
class SonataCar {
val engine: Engine = OilEngine() // 이넘을 ElectronicEngine()으로 바꿔달라는 요구사항!
fun start() {
engine.startEngine()
}
}
class AvanteCar {
val engine: Engine = OilEngine() // 이넘을 ElectronicEngine()으로 바꿔달라는 요구사항!
fun start() {
engine.startEngine()
}
}
class JenesissCar {
val engine: Engine = OilEngine() // 이넘을 ElectronicEngine()으로 바꿔달라는 요구사항!
fun start() {
engine.startEngine()
}
}
class PalisadeCar {
val engine: Engine = OilEngine() // 이넘을 ElectronicEngine()으로 바꿔달라는 요구사항!
fun start() {
engine.startEngine()
}
}
그리고 이 모든 자동차 클래스에 대해서 OilEngine을 썼다가 ElectronicEngine로 바꿔야 한다면? 모든 클래스의 엔지 클래스 생성 부분을 OilEngine() -> ElectronicEngine() 으로 수정해줘야 한다....
귀찮기도 하지만 코드가 복잡하고 커지면서 까먹고 수정을 해주지 않을 경우 문제가 발생할 수도 있다.
이는 자동차 클래스 내부에서 직접 엔진 클래스를 생성하기 때문에 발생하는 문제이다.
따라서 이렇게 클래스 내부에서 다른 클래스의 객체를 생성하는 것이 아니라 외부에서 객체를 생성하여 주입해 주는 것이다. 그러면 객체를 생성하는 부분은 외부에서 공통적으로 한 번만 수정하면 되는 것이다!!
DI Container
- 객체를 생성하는 곳
class SonataCar {
@Inject
lateinit var engine: Engine
fun start() {
engine.startEngine()
}
}
class AvanteCar {
@Inject
lateinit var engine: Engine
fun start() {
engine.startEngine()
}
}
class JenesissCar {
@Inject
lateinit var engine: Engine
fun start() {
engine.startEngine()
}
}
class PalisadeCar {
@Inject
lateinit var engine: Engine
fun start() {
engine.startEngine()
}
}
@Module
@InstallIn(ApplicationComponent::class)
object EngineModule {
@Provides
fun provideElectronicEngine(): Engine {
return ElctronicEngine()
}
}
이렇게 EngineModule 라는 DI Container를 만들어 ElctronicEngine 클래스를 생성해 한방에 필요한 곳들에 주입하게 된다.
즉, Engine 클래스 생성에 대한 권한은 Car클래스가 아니라, Cotainer클래스가 담당하게 되면서 생성과 사용을 분리하게 된다.
@Inject 어노테이션이 사용된 필드나 생성자를 감지하고, Engine 타입에 대해 객체를 주입할 수 있는 DI 컨테이너 모듈을 찾아가다가 발견하게 되면 @Provides 어노테이션이 작성된 함수는 따로 호출하지 않아도 자동으로 런타임에 객체를 생성하게 되는 것 같다.
- 필드 주입 → 클래스 필드변수로 주입
- 생성자 주입 → 해당 안드로이드 클래스 인스턴스를 생성할 때 생성자단에서 어떤 의존성 주입이 필요한지 확인하고 주입해줌 (생성자 주입시에는 인터페이스나 외부 라이브러리 클래스는 바로 주입이 불가능하기 때문에 DI 컨테이너에서 따로 어노테이션을 달아줘야 함)
💡@Module / @Provides 와 같은 어노테이션은 무엇인지?
각 컴포넌트별로 (액티비티, 프래그먼트, 뷰모델, 애플리케이션 등) 안드로이드 클래스에서 HIlt 의존성 주입을 활성화 하겠다는 어노테이션이다. 이걸 작성해주지 않으면 의존성주입을 사용할 수 없다.
@Module → 의존성을 생성하고 제공하는 방법을 정의한다는 의미 (DI Container 만들때 주로 사용)
@Component → 해당 클래스에서 어떤 모듈을 사용해 의존성을 주입할 것인지 구성하는 어노테이션
// EngineModule 과 ThirdPartyLibraryModule 모듈을 사용해 의존성 주입할 것
@Component(modules = [EngineModule::class, ThirdPartyLibraryModule::class])
interface CarComponent {
// 컴포넌트 메서드들...
}
(그런데 Application에서 @HiltAndroidApp 를 작성해놓으면 어플리케이션 수준에서 컴포넌트를 설정하므로 애플리케이션 전반에 걸쳐 의존성 주입이 가능해진다. 하지만 CarComponent는 interface지만 Activity 와 같은 고유한 라이프사이클이 존재하는 컴포넌트의 경우에는 @AndroidEntryPoint 같은 어노테이션을 추가해줘야 한다. )
@Provides / @Binds → 인스턴스를 생성하여 직접 생성하거나 주입받아 연결한다는 의미, 일반적으로 @Inject 만을 사용해서 인스턴스를 생성하고 반환해서 주입할 수 있지만 특정한 작업을 더 수행해야 하는 경우에 사용 (인터페이스나 외부라이브러리를 생성자 주입 하는 경우)
@Provides → 외부 라이브러리를 주입 하는 경우 (Retrofit, Room)
@Binds → 인터페이스의 인스턴스를 제공하는 경우 사용
@InstallIn → 모듈이 언제 결합되는지를 설정, 인스턴스마다 한 번만 범위가 지정된 결합을 생성하며 이 결합에 관한 모든 요청은 동일한 인스턴스를 공유한다.
- SingletonComponent::class → 해당 모듈이 Application 단에서 사용 가능 (Application 종료시까지 계속 공유)
- ActivityComponent::class → 해당 모듈이 Activity 단에서 사용 가능
💡 @InstallIn(SingletonComponent::class) 로 어노테이션이 설정되어 있는데 굳이 bindsAuthDataSource 추상메서드에 @Singleton를 또 추가해준 이유는?
- **@InstallIn**은 모듈이 어떤 Dagger Hilt 컴포넌트에 속하는지를 나타내고, **@Singleton**은 해당 의존성이 싱글톤임을 나타내며, 두 어노테이션을 함께 사용함으로써 해당 모듈 내의 의존성이 싱글톤 스코프에 맞춰지게 됩니다.
💡DI Container 를 Object / 추상 클래스 / 일반 클래스 등으로 구분하여 만드는 방법
- Object → 싱글톤 객체를 정의
- abstract class → 해당 모듈을 상속하여 실제 구현을 제공
- **object**는 간단한 싱글톤 모듈이나 유틸리티성 메서드를 담을 때 편리하고, **abstract class**는 모듈 일부를 공유하면서도 다른 구현체를 제공할 때 사용할 수 있습니다. 일반 **class**는 특별한 상황이나 모듈이 상태를 가지고 있어야 할 때 사용될 수 있습니다.
[추후 공부할 내용]
💡의존성 주입시 코루틴과 어떤 연관이 있는지?
💡 interface 에 대해 다양한 impelement가 존재할 수 있는데 100개의 클래스에서 50개는 A 구현체, 나머지 50개는 B구현체를 사용
하는 경우 어떻게 의존성 주입을 할 수 있는지? 혹은 다 다른 엔진을 사용하려고 하면 어떻게 해결하는지?
[레퍼런스]
https://everyday-develop-myself.tistory.com/293
https://it-of-fortune.tistory.com/26
https://developer.android.com/training/dependency-injection/hilt-android?hl=ko#kts
https://velog.io/@dlwpdlf147/Android-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85%EA%B3%BC-Hilt
'Android' 카테고리의 다른 글
[안드로이드/RecyclerView] 리사이클러뷰 + 데이터바인딩 + MVVM 적용하기 (0) | 2024.02.03 |
---|---|
[안드로이드/소셜로그인&Oauth] 안드로이드에서 애플(Apple Sign-In) 로그인 구현하기 (1) | 2024.01.29 |
[안드로이드/AI] 앱에서 Segmentation 모델을 사용하기 위한 다양한 시도 (0) | 2023.08.17 |
[안드로이드/아키텍쳐] 안드로이드 공식 권장 아키텍쳐 (구vs신버전) (0) | 2023.07.19 |
[안드로이드/아키텍쳐] AAC ViewModel 사용하기 (0) | 2023.07.13 |