의존성 주입이란?

정지원·2024년 1월 24일
0
post-thumbnail

의존성

  • 어떤 대상이 참조하는 객체.
  • 어떤 대상 → A
    • 어떤 대상이 A를 참조한다. → 어떤 대상이 A를 가지고 있다.
    • 자동차가 엔진을 참조하고 있다. → 자동차가 엔진을 가지고 있다.
// Car는 engine을 생성하는 책임을 가지고 있음.

class Car {
	val engine = Engine() 
}
  • Car는 Engine에 의존한다. or Car는 Engine에 의존적이다.
  • Engine은 Car의 의존성임.

의존성 주입

class Car(val engine: Engine) {

}
  • engine을 생성자 매개변수로 옮김. → engine생성의 책임을 제거함.
    • 외부에서 engine을 생성하게 됨.
  • 위 설계 패턴을 IoC(제어의 역전)라고 부르고, 객체 생성의 책임을 내부에서 외부로 뒤집으며 engine에 대한 제어를 역전시킴.
    • engine 의존성을 외부에서 주입 받을 수 있게 해줌.
class Car(val engine: Engine) {
	// 매우 긴 소스코드
}
  • 자동차는 엔진외 많은 부품으로 이뤄져 있음.
  • Car 클래스에서 이러한 부품들을 전부 가지고 있는다면, 아주 거대한 소스코드를 갖게 될 것임.
  • IoC를 통해서 필요한 의존성들을 외부에서 주입 받으므로 긴 소스코드와 책임이 줄어들게 됨.
class Car(
		val engine: Engine,
		val wheels: Wheels,
		val wiper: Wiper,
		val battery: Battery
) {
	// 적은 소스코드
}
  • 하나의 기능만 책임질 수 있도록 캡슐화하는 원칙을 단일 책임 원칙이라 부름.
    • Engine, Wheels, Wiper, Battery가 단일 책임 원칙을 적용한 클래스임.
  • IoC를 적용하여 주입을 하게 되면, Car 클래스가 가져야 할 비즈니스 로직에 더욱 집중이 가능함.
    • 이는 개발과 유지보수의 간편성으로 직결됨.

Injector

  • 의존성을 클라이언트(객체)에게 제공하는 역할임.
  • 어떤 의존성을 객체에 제공할 땐 보통 Inject개념을 통해서 이뤄짐.
// 인젝터가 없는 코드
fun main(args: Array<String>) {
	val engine = Engine()
	val car = Car(engine)
}

// 인젝터가 있는 코드
class Injector {
	fun getEngine(): Engine = Engine() // 엔진을 생성하고 반환함.
}

fun main(args: Array<String>) {
	val engine = Injector().getEngine()
	val car = Car(engine)
}
  • 메인 함수에서 엔진을 생성하는 코드를 작성하지 않고, 인젝터에게 위임함.
    • 여러가지의 엔진을 공유하고 싶다면, 인젝터 클래스에 엔진을 생성하는 코드를 작성하여 자원공유를 할 수 있음.
  • Injector의 또다른 명칭은 다음과 같음.
    • Container
    • Assembler
    • Component
    • Provider
    • Factory

의존성 주입의 장점

  • Car 클래스의 코드를 변경하지 않고, 단순히 어떤 엔진(객체)를 상속하여 확장한 여러 타입의 엔진을 할당하는 것이 가능함.
    • GasolineEngine(), DieselEngine()은 Engine을 상속받고 있음.
val gasolineCar = Car(GasolineEngine())
val dieselCar = Car(DieselEngine())
  1. Car 클래스의 소스코드를 변경하지 않음. → 재사용성
  2. 클래스간 결합도를 느슨하게 만들어 줌. → 디커플링
class Car {
	val gasolineEngine = GasolineEngine()
}

의존성 주입을 사용하지 않는다면, 기본 엔진에서 가솔린 엔진으로 교체하는 경우 Car 클래스의 수정이 일어나게됨.

  • 테스트를 쉽게 만들어줌.
class CarTest {
    @Test
    fun `Car 성공 케이스 테스트`() {
        val car = Car(FakeEngine()) // 엔진을 확장하는 페이크 엔진임.
    }

    @Test
    fun `Car 실패 케이스 테스트`() {
        val car = Car(FakeBrokenEngine()) 
    }
}

open class Engine() {

}

class Car(val engine: Engine) {
}

class FakeEngine(): Engine() {

}

class FakeBrokenEngine(): Engine() {

}
  1. Mock 객체를 주입(사용)하여 실 데이터의 동작들을 완전히 제어가 가능함.
    1. 이는 특정 동작들을 설정하거나 결과 확인이 가능함.
  2. 실제 사용되는 객체보다 더욱 무거운 객체를 만들어 테스트할 수 있음. → FakeBrokenEngine
  3. DI를 사용 안하고, Car 클래스 내에서 직접 엔진을 생성하는 경우 실제 엔진을 사용하게 됨.
    1. 동작을 테스트 시나리오에 맞게 제어하기 어려움.
    2. 코드를 반복적으로 쓰고 지우기 번거로움.
    3. 개발자의 실수로 중요 로직을 삭제하거나 변경될 수 있음.
  4. 새로운 엔진을 테스트 하려면 Car 클래스의 코드를 변경해야함.
  • 보일러 플레이트 코드가 감소됨.
    • IoC를 통해서 필요한 의존성들을 외부에서 주입 받으므로 긴 소스코드와 책임이 줄어들게 됨.
  • 의존성 관리가 용이함(자원공유)
    • Injector
profile
안드로이드 개발자 정지원입니다

0개의 댓글