ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 의존성 역전 원칙(Dependency Inversion Principle)
    기록/OOP 2023. 1. 31. 15:45

     

    SOLID의존성 역전 원칙(Dependency inversion principle)은 소프트웨어 모듈을 느슨하게 결합하기 위한 방법론입니다.

     

     

    의존성 역전 원칙에는 이렇게 기술되어 있습니다.

    (1) high-level 모듈은 low-level 모듈로부터 import하지 않아야 하며 abstraction에 의존해야 한다. 쉽게 말해서 high-level 모듈은 low-level 모듈의 존재조차 몰라야 한다.

     

    (2) Abstractiondetail에 의존하지 않아야 한다. Detail(구체 클래스(타입) 등 구현부)이 abstraction에 의존해야 한다.

     

     

    일반적으로 의존성 관계는 high-level의 정책 모듈에서 정책과 비즈니스 로직을 가지고 (종속되어) 구체적으로 구현되는 low-level로 흐릅니다. 여기에서, 의존성 역전 원칙을 적용해서 사이에 인터페이스를 두고 high-level --> interface <-- low-level로 만들어주는 것이 목표입니다. 우선 코드로 작성해보고 의존성을 역전시키면 어떤 장점이 있는지 알아보겠습니다.

     

    일반적인 의존성

    high-level layer --> low-level layer

     

    의존성 역전

    high-level layer --> Interface <-- low-level layer

     

     

    일반적인 의존성을 나타내는 코드입니다. 페트병(high-level)에 물(low-level)을 채우고 비워내기 위해서 페트병이 Water라는 구체 클래스를 가지고 있습니다. 위의 의존성 역전 원칙에 기술되어 있는 내용이 지켜지지 않고 있죠.

    class PlasticBottle {
        var water: Water
        
        init(water: Water) {
            self.water = water
        }
        
        func drink() {
            water.drink()
        }
    }
    
    class Water {
        var volume: Double
        
        init(volume: Double) {
            self.volume = volume
        }
        
        func drink() {
            volume /= 2
        }
    }

     

    위와 같이 작성하게 되면, 페트병에는 구체 클래스에 의존하기 때문에 물밖에 담을 수 없고, 물만으로 로직을 수행할 수 있으며 low-level 모듈의 확장으로부터 자유로울 수 없는 강한 결합이 발생합니다.

     

    의존성 방향

    PlasticBottle --> Water

     

     

    이제 페트병이 (1) abstraction에 의존하고, Water의 존재도 모르게 바꾸고, (2) Abstraction은 detail에 의존하지 않으며 Detail(구체 클래스 등 구현부)이 abstraction에 의존하도록 만들어 보겠습니다.

    class PlasticBottle {
        var beverage: Potable // beverage의 사전적 의미는 (물을 제외한)음료네요.
        
        init(beverage: Potable) {
            self.beverage = beverage
        }
        
        func drink() {
            beverage.drink()
        }
    }
    
    protocol Potable: AnyObject {
        func drink()
    }
    
    class Water: Potable {
        var volume: Double
        
        init(volume: Double) {
            self.volume = volume
        }
        
        func drink() {
            volume /= 2
        }
    }

    Potable(음료로 적합한, 마실 수 있는) 프로토콜을 만들어주면, PlasticBottle은 구체 클래스가 아닌 Potable(abstraction)에 의존하여 Water의 존재를 모릅니다. 또한 의미를 봤을 때, 페트병에는 물뿐만이 아니라 마실 수 있는 다양한 것들이 들어갈 수 있게 되는 것이죠.

     

    의존성 방향

    PlasticBottle --> Potable <-- Water (detail이 abstraction에 의존)

     

    의존성을 역전시켰을 때의 장점

    low-level 모듈의 확장으로부터 high-level 모듈이 자유롭다.

     마실 것을 가지고 수행하는 동작이 추가되는 등, high-level의 정책이 바뀌는 경우에는 당연히 high-level 모듈을 확장해야 합니다. 그와는 별개로 인터페이스 정책이 변경된다면 인터페이스와 그것을 상속받은 구체 클래스를 수정하면 되고, 따라서 PlasticBottle 코드는 건드릴 필요가 없습니다. 또한 소주, 포도 주스 등 마실 것이 추가되더라도 PlasticBottle에서는 확장이 일어날 필요가 없습니다. Potable이라는 청사진을 기반으로 새로운 타입을 정의해주기만 하면 됩니다. 기존 코드의 수정 없이 확장이 일어나는 것이죠. <-- 개방-폐쇄 원칙

     

     하지만 모든 경우에 인터페이스를 두고 의존성을 역전시킬 필요가 있을까요? 어떤 클래스도 구체 클래스에 대한 참조를 가지지 않고 코드를 작성하기는 어렵습니다. 모든 상황에 적용할 수는 없다는 말이죠. 따라서 위의 장점을 누릴 수 없어서 복잡성이 의미 없이 증가하는 경우를 예외로 둘 수 있을 것 같습니다. 애초에 low-level 모듈의 변경이 거의 없을 것이라면 high-level 모듈에 주는 영향이 적을 것이고, 따라서 의존성 역전 원칙을 적용할 필요가 없을 수도 있다는 것을 염두에 두어야 할 것 같습니다.

    댓글

Designed by Tistory.