Swift

[Swift] Dependency Inversion Principle - DIP

띵지니어 2025. 1. 30. 16:20
반응형

 

 

 

DIP? 하면 가장 많이 듣는 말이 있습니다.

추상화에 의존해야지 구체화에 의존하면 안 된다.
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.

 

이 말만 듣고 저는 쉽게 이해가 되지 않아서 많은 예시와, 공부를 했습니다.

제가 공부한 내용을 포스팅해보려고 합니다.

먼저 꼭 알아야 하는 단어부터 공부하겠습니다.

 

1. 의존성

의존성 역전 원칙에서 의존성이 뭘까요? 많이 들어봤지만 확실히 정리하고 넘어가야 합니다.

A가 B를 의존한다.라는 말을 다른 말로 하면 "A 클래스는 B 클래스를 필요로 한다"라고 합니다.
화살표로는 A B라고 표현합니다.

또 하나 예시를 든다면

스마트폰이 배터리를 의존한다.라는 말은 "스마트폰은 배터리를 필요로 한다."라는 말과 같고
화살표로는 스마트폰 배터리라고 표현합니다.


 

2. 고수준,  저수준 모듈

처음에 정의했던 고수준과 저수준이라는 말이 무슨 말일까요?
방금 말했던 스마트폰과 배터리 중에 뭐가 고수준이고 어떤 게 저수준일까요?

1. 고수준 모듈 (High-Level Module) - 스마트폰

✔ 비즈니스 로직을 담당하는 핵심적인 모듈
✔ 추상적인 개념을 다루며, 기능을 조합하고 실행하는 역할
✔ 직접적인 세부 구현에 의존하지 않는 것이 이상적 (DIP를 적용하면)
2. 저수준 모듈 (Low-Level Module) - 배터리

✔ 구체적인 기능을 담당하는 모듈
✔ 특정한 작업을 수행하는 세부 구현체
✔ 고수준 모듈에서 사용되는 역할을 함

이제 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.라는 말을 우리가 생각할 수 있게 되었어요!

이 말은 스마트폰은 배터리에 의존해서는 안된다!라는 말과 같습니다. 근데...

대체 왜!! 스마트폰이 배터리 구현체에 의존해서는 안된다는 거죠???

 

3. DIP 적용 전

그걸 알기 위해서는 먼저 스마트폰이 배터리의 구현체에 의존하는 코드를 봐야 합니다!
바로 코드로 구현해볼게요!

class Battery {  // 저수준 모듈 (구체적인 구현)
    func power() {
        print("아이폰 배터리 version 1️⃣ 이 탑재 되었습니다.")
    }
}

class Smartphone {  // 고수준 모듈
    let battery = Battery()  // ⚠️ 실제로 배터리를 변경하면 turnOn 함수도 수정 해줘야한다.

    func turnOn() {
        battery.power()
    }
}

let phone = Smartphone()
phone.turnOn() // 아이폰 배터리 version 1️⃣ 이 탑재 되었습니다.

이렇게 고수준 모듈(스마트폰)이 저수준 모듈의 구현체(배터리)에 의존하면 어떻게 될까요? ( 스마트폰 → 배터리 ) 

이 부분이 수정이 돼야 한다면, 다른 부분이 많이 변경이 되어야 할 거예요!

- 문제점 -

1. 만약 성능 좋거나, 버전이 다른 Battery 를 추가하려면 Smartphone(고수준 모듈)을 수정해야 함.
2. 유닛 테스트에서 Battery를 MockBattery로 바꾸는 것도 어렵다..! SmartPhone 클래스에서 battery 변수를 수정해줘야 함
3. 배터리의 코드가 변경된다면, 변경되는 부분이 다른 곳에서도 많아져서 향후 유지보수가 어렵다.

이제 스마트폰 배터리구현체 로 되어 있는 의존 관계를 protocol(추상화)를 통해 수정해 보겠습니다.

 

4. DIP 적용 후

고수준 모듈 → 추상화(protocol) ← 저수준 모듈(구현체)

// ⭐️ 1. 추상화 (프로토콜) 정의
protocol Battery {
    func power()
}

// 2. 저수준 모듈 (구현체)
class BatteryVersion1: Battery {
    func power() {
        print("아이폰 배터리 version 1️⃣ 이 탑재 되었습니다.")
    }
}

class BatteryVersion2: Battery {
    func power() {
        print("아이폰 배터리 version 2️⃣ 가 탑재 되었습니다.")
    }
}

class BatteryVersion3: Battery {
    func power() {
        print("아이폰 배터리 version 3️⃣ 이 탑재 되었습니다.")
    }
}

// 3. 고수준 모듈
class Smartphone {
    let battery: Battery  // ✅ 고수준 모듈이 추상화(프로토콜)에 의존
    
    init(battery: Battery) { // 생성자 주입
        self.battery = battery
    }

    func turnOn() {
        battery.power()
    }
}

let phone1 = Smartphone(battery: BatteryVersion1())
let phone2 = Smartphone(battery: BatteryVersion2())
let phone3 = Smartphone(battery: BatteryVersion3())

phone1.turnOn() // 아이폰 배터리 version 1️⃣ 이 탑재 되었습니다.
phone2.turnOn() // 아이폰 배터리 version 2️⃣ 가 탑재 되었습니다.
phone3.turnOn() // 아이폰 배터리 version 3️⃣ 이 탑재 되었습니다.

 

- 의존성 역전 원칙 적용 -
✔ 고수준 모듈(Smartphone)은 프로토콜(Battery)에 의존
✔ 저수준 모듈(BatteryVersion1, BatteryVersion2, ... )이 프로토콜을 구현(채택)
✔ 따라서, 스마트폰이 직접 배터리에 의존하지 않고, 배터리를 자유롭게 변경 가능!

- 장점 -
✔ 확장성 증가 : 새로운 배터리를 추가할 때 Smartphone을 수정하지 않아도 된다.
✔ 테스트 용이성 : MockBattery를 만들어 테스트할 수 있다.
✔ 유지보수성 향상 : 배터리 코드가 변경되어도 Smartphone을 수정할 필요가 없다.

고수준 모듈이 저수준 모듈을 의존하는 게 아닌 추상화에 의존한 코드이기 때문에,
아무리 서로 다른 배터리가 나와도 Smartphone의 코드는 변경을 하지 않아도 됩니다.

 

5. 결론

 

고수준 모듈이 저수준 모듈을 의존해서는 안된다. 라는 말이 아래 화살표로 이해가 됐습니다.

⭐️ 고수준 모듈 → 추상화(프로토콜) ← 저수준 모듈(구현체)

설계한다면 다음과 같이 설계할 수 있습니다.

전 후 비교

❌ DIP 적용 전 : Smartphone → Battery (직접 의존)
✅ DIP 적용 후 : Smartphone → Battery (추상화 Interface) ← 여러 배터리 구현체

 

- 배운점

DIP를 활용하면 고수준 모듈을 건드리지 않고, 저수준 모듈만 구현하여 변경 없이 확장할 수 있다는 점을 배웠습니다.

해당 원칙을 잘 고려하며 설계를 한다면 최소한의 비용으로 이해관계자의 변경사항에 따라 유연하게 개발하는 데 도움이 될 것 같습니다.

 

반응형
목차(index)