[개념] 의존성 주입 : DI (Dependency Injection) (1)

쓰리원·2022년 5월 10일
0

Depedency Injection

목록 보기
1/4
post-thumbnail

1. DI = Dependency Injection(의존성 주입)란?

선수 지식

1. 객체-지향의-강한-결합과-약한-결합이란
2. 오버로딩-오버라이딩Overloading-Overriding

어떤 클래스는 다른 클래스에 대한 참조가 필요한 경우가 있습니다. 예를 들어, A클래스가 B 클래스의 참조가 필요합니다. 이 때 A가 B에 의존하고 있다고 말하고, B를 A의 종속 항목(디펜던시) 이라고 합니다.

"A가 B를 의존한다"라는 것은 의존 대상인 B가 변하면, 그것이 A에 영향을 미치는 것 입니다. 즉, B의 기능이 추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에 미칩니다.

특정 클래스가 자신이 의존하고 있는 객체를 얻는 방법은 3가지가 있습니다.

  • Toy클래스의 안에서 battery클래스 인스턴스를 생성하여 초기화.
  • 다른 곳에서 객체를 얻음. Android 로 치면 Context, getSystemService() 등에 해당합니다.
  • 객체를 파라미터로 얻음. Toy 의 생성자가 battery를 파라미터로 받는 것 입니다.

세 번째 방법이 바로 Dependency Injection 기법 중 하나 입니다.

2. DI 를 사용하지 않을 때

DI없이 Toy클래스 내부에서 자체적으로 NomalBattary클래스의 인스턴스를 생성을 나타낸 코드입니다.

class Toy {
    private val battery : NomalBattary = NomalBattary()
    fun start() {
        battery.Use()
    }
}
class NomalBattary {
    fun Use() {
        println("일반배터리를 사용합니다.");
    }
}
fun main(args: Array) {
    val toy = Toy()
    toy.start()
}

Toy 클래스의 멤버 변수 타입으로 NomalBattary 클래스가 존재하는 것을 확인할 수 있습니다. 이 코드는 다음과 같은 문제를 갖고 있습니다. 만약 NomalBattary 클래스가 다른 종류의 배터리 클래스(ex 전기배터리 수소배터리 등)로 변경되는 상황이라면 Toy클래스의 코드가 같이 변경됩니다.

즉, Toy 클래스가 NomalBattary 클래스에 의존하게 됩니다. 따라서 Toy 클래스가 NomalBattary 클래스에 의존성이 존재한다고 하게 됩니다. 이러한 의존관계는 Unit Test를 어렵게 만듭니다. 아래의 예시로 더 자세히 설명해 보겠습니다.

class Toy {
    private val battery : NomalBattary = NomalBattary() -> private val battery : ElectricBattary = ElectricBattary()   
    fun start() {
        battery.Use()
    }
}
class ElectricBattary {
    fun Use() {
        println("전기배터리를 사용합니다.");
    }
}
class NomalBattary {
    fun Use() {
        println("일반배터리를 사용합니다.");
    }
}
fun main(args: Array) {
    val toy = Toy()
    toy.start()
}

위의 코드를 보면 전기배터리를 사용하게 만들기 위해서 클래스 내부를 새롭게 수정을 해야하는 것을 확인 할 수 있습니다. 이로 인해 test를 위해 배터리를 바꿔줄 때 마다 지속적으로 Toy클래스 내부의 내용을 수정해야하는 번거로움이 있습니다.

private val battery : NomalBattary = NomalBattary() -> private val battery : ElectricBattary = ElectricBattary()

3. Dependency Inversion Principle(DIP)

Dependency Inversion Principle는 의존 관계를 맺을 때 자주 변화하는 것보다는 거의 변화가 없는 것에 의존하라는 원칙입니다. 그래서 class들 간의 의존성 부패를 제거하기 위한 일반적인 디자인 방법입니다. 만약 Toy와 Battary클래스에 DIP를 적용한다면 아래와 같은 순서로 적용됩니다.

  1. 고차원 모듈은 저차원 모듈에 의존하면 안됩니다. 따라서 Toy가 Battary를 바라보는 Depedency를 제거합니다.

  2. 이 모듈 모두 추상화된 것에 의존해야 합니다. 그리고 추상화 된것은 구체적인 것에 의존하면 안됩니다. Toy는 Abstract를 Reference하지만 Abstract는 Battary를 Depedency하면 안됩니다.

  3. 구체적인 것이 추상화된 것에 의존해야 합니다. Battary가 Abstract를 상속하도록 하여 Dependency를 Inversion합니다. 아래에서 예제 코드를 통해 알아보겠습니다.

class Toy {
	// Toy는 Abstract를 의존
    val battery: BattaryInterface = NomalBattary()
    fun start() {
        battery.Use()
    }
}

// Abstract에 해당
interface BattaryInterface {
	fun Use()
}

// Abstract를 상속
class NomalBattary: BattaryInterface {
    override fun Use() {
        println("일반배터리를 사용합니다.");
    }
}

class ElectricBattary: BattaryInterface {
    override fun Use() {
        println("전기배터리를 사용합니다.");
    }
}

위의 코드에서는 Toy클래스가 NomalBattary대신 BattaryInterface(Abstract)를 참조하도록 하였고, 각 NomalBattary와 ElectricBattary가 BattaryInterface를 상속하도록 하여 DIP를 하였습니다. 이렇게 되면, BattaryInterface 객체 한개로 다형성의 조건이 충족되게 여러 클래스의 형질을 반환 할 수 있게 됩니다.

다만 DIP가 적용된 코드를 보면 Toy와 Battary의 Dependency만 Inversion한다고 의존성 문제가 해결되지 않습니다.

class Toy {
    val battery: BattaryInterface = NomalBattary()
    ...
}

위의 코드를 보면 Toy클래스안에서 구체 클래스인 NomalBattary()를 직접 생성하고 있습니다. 의존성을 뒤집어 interface를 참조하도록 하였지만, Dependency Injection 클래스 간의 의존을 해결해야 합니다. 아래의 주제에서 어떻게 해결하는지 확인해 보겠습니다.

4. DI 를 사용할 때

Dependency Injection은 Dependency Inversion Principle을 구현하는 기법 중 하나입니다. DI 를 사용한다면 Toy의 각 인스턴스는 초기화할 때 Battery 객체를 생성자 파라미터로 받게 됩니다.

// Abstract에 해당
interface BattaryInterface {
	fun Use()
}

// Abstract를 상속
class NomalBattary: BattaryInterface {
    override fun Use() {
        println("일반배터리를 사용합니다.");
    }
}

class ElectricBattary: BattaryInterface {
    override fun Use() {
        println("전기배터리를 사용합니다.");
    }
}

class Toy(private val battery: BattaryInterface) {
    fun start() {
        battery.Use()
    }
}

fun main(args: Array) {
    val battery = NomalBattary()
    val toy = Toy(battery)
    toy.start()
}

위의 예시는 Constructor Injection(생성자 주입)을 적용한 Toy클래스 입니다. main()에서 Battery 인스턴스를 생성하고, 이를 활용하여 Toy 인스턴스를 만들게 됩니다. 이렇게 구현하게 되면 다음과 같은 이점을 챙길 수 있습니다.

Toy의 재사용성이 높아집니다. 예를 들어 ElectricBattary와 같은 Battery의 서브클래스를 넘겨서 Battery의 다양한 구현을 Toy 에 전달할 수 있습니다. Battery의 생성자 등 구현이 변경되어도, Toy 클래스를 수정하지 않아도 됩니다. 위의 코드로 인해 Toy는 수정 없이 재사용이 가능한 Component가 되었기 때문입니다.

5. Inversion of Control(IoC)

IoC는 객체의 흐름, 생명주기관리 등을 독립적인 제 3자의 시스템에게 역할과 책임을 위임하는 방식에 프로그래밍 모델이며 범용적인 표현입니다. 아래에서 자세히 알아보겠습니다.

fun main(args: Array) {
    val battery = NomalBattary()
    val toy = Toy(battery)
    toy.start()
}

위의 예제에서 Client가 NomalBattary()을 생성해 Toy에 주입하고 있습니다. Toy는 NomalBattary()를 모르게 됐지만, Client가 NomalBattary()를 생성하고 Toy와의 관계를 설정하는 상황이 됐습니다. 그러나 Client는 NomalBattary()를 알 필요가 없습니다. 그저 Toy라는 객체를 활용만 하는 되는 상황이기 때문입니다. 즉, 굳이 내부 상황까지 다 알 필요가 없습니다.

일반적인 프로그램에서 제어 흐름

IoC가 적용되지 않은 일반적인 프로그램의 흐름은 위와 같이 entry Point(main)에서 사용할 오브젝트를 결정하고, 생성하고, 생성된 오브젝트의 메서드를 호출하고, 그 오브젝트 메서드안에서 또 다시 다음에 사용할 것을 결정하고 호출하는 식의 작업이 반복됩니다. 각 오브젝트는 프로그램의 흐름을 결정하거나 사용할 오브젝트를 구성하는 작업에 능동적으로 참여합니다. 즉, Service를 사용하는 Client쪽에서 모든 걸 제어하고 있는 구조입니다. 제어의 역전(IoC)이란 이런 제어의 흐름을 Inversion하는 것을 의미합니다.

역전된 제어(Inversion of Control)의 흐름

위는 역전된 제어의 흐름을 보여줍니다. entry point(main)에서 IoC Container에게 모든 관계 설정에 대한 책임을 위임합니다. 따라서 컴파일 타임의 static한 class dependency가 런타임의 dynamic한 object dependency로 변경된 것을 볼 수 있습니다.

client가 Ioc Container에게 필요한 Object를 요청하면 Ioc Container는 Toy가 필요한 object를 생성하고 관계를 wiring 하여 전달합니다. 각 클래스들이 다른 클래스에 대한 dependency가 모두 사라졌으니 이제 모든 클래스들은 component가 될 수 있습니다. 또한 분리된 모든 클래스들은 전부 mock으로 대체될 수 있어 테스트성도 높아졌습니다. IoC만 바뀌면 dynamic하게 전혀 다른 동작을 하는 프로그램이 될 수도 있습니다.

6. reference

https://develogs.tistory.com/19

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글