어떤 클래스는 다른 클래스에 대한 참조가 필요한 경우가 있습니다. 예를 들어 A클래스가 B클래스의 참조가 필요하다는 가정에 이때 A가 B에 의존하고 있다고 말하고 B를 A의 종속 항목(디펜던시)이라고 합니다.
A가 B를 의존한다 라는 것은 의존 대상인 B가 변하면 그것이 A에 영향을 미치는 것입니다. 즉 B의 기능이 추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에 미칩니다.
class Dog{
private val dog : Animal = Animal()
fun start() {
dog.Bark()
}
}
class Animal {
fun Bark(){
println("짖습니다.");
}
}
fun main(args: Array){
val dog = Dog()
dog.start()
}
Dog 클래스의 멤버 변수 타입으로 Animal 클래스가 존재하는 것을 확인할 수 있습니다. 이 코드에서의 문제점은 Animal 클래스에 기능이 추가되거나 변경이된다면 Dog 클래스의 코드가 같이 변경됩니다.
즉 Dog 클래스가 Animal 클래스에 의존하게 됩니다. 따라서 Dog 클래스가 Animal 클래스에 의존성이 존재한다고 하게 됩니다. 이러한 의존관계는 Unit Test를 어렵게 만듭니다.
의존성 역전 원칙은 의존 관계를 맺을 때 자주 변화하는 것보다는 거의 변화가 없는 것에 의존하라는 원칙입니다. 그래서 Class들 간의 의존성 부패를 제거하기 위한 일반적인 디자인 방법입니다. 만약 Animal과 Dog에 DIP를 적용한다면 아래와 같은 순서로 적용됩니다.
고차원 모듈은 저차원 모듈에 의존하면 안됩니다. 따라서 Dog가 Animal을 바라보는 Dependency를 제거합니다.
이 모듈 모두 추상화된 것에 의존해야 합니다. 그리고 추상화 된것은 구체적인 것에 의존하면 안됩니다. Dog는 Abstract를 Reference 하지만 Abstract는 Animal를 Dependecy하면 안됩니다.
구체적인 것이 추상화된 것에 의존해야 합니다. Animal가 Abstract를 상속하도록 하여 Dependency를 Inversion 합니다. 코드로 확인해보겠습니다.
class Zoo {
// Toy는 Abstract를 의존
val zoo: AnimalInterface = Dog()
fun start() {
zoo.Bark()
}
}
// Abstract에 해당
interface AnimalInterface {
fun Bark()
}
// Abstract를 상속
class Dog: AnimalInterface {
override fun Bark() {
println("짖습니다.");
}
}
위의 코드에서는 Constructor Injection을 적용한 Dog클래스입니다. 이런식으로 구현을 하게 되면 AnimalInterface 객체 한 개로 다형성의 조건이 충족되게 여러 클래스의 형질을 반환 할 수 있게 됩니다.
// Abstract에 해당
interface AnimalInterface {
fun Bark()
}
// Abstract를 상속
class Dog: AnimalInterface {
override fun Bark() {
println("개가 짖습니다.");
}
}
class Cat: AnimalInterface {
override fun Bark() {
println("고양이가 짖습니다.");
}
}
class Zoo(private val zoo: AnimalInterface) {
fun start() {
zoo.Bark()
}
}
fun main(args: Array) {
val animal = Dog()
val zoo = Zoo(animal)
zoo.start()
}
Main()에서 zoo 인스턴스를 생성하고 이를 활용하여 Zoo인스턴스를 만들게 됩니다. 이렇게 구현하게 되면 다음과 같은 이점을 챙길 수 있습니다.
위의 코드로 인해 Zoo는 수정없이 재사용이 가능한 Component가 되었기 때문에 animal의 생성자 등 구현이 변경되어도 Zoo 클래스는 수정하지 않아도 됩니다.