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를 활용하면 고수준 모듈을 건드리지 않고, 저수준 모듈만 구현하여 변경 없이 확장할 수 있다는 점을 배웠습니다.
해당 원칙을 잘 고려하며 설계를 한다면 최소한의 비용으로 이해관계자의 변경사항에 따라 유연하게 개발하는 데 도움이 될 것 같습니다.
'Swift' 카테고리의 다른 글
[Swift] @escaping 클로저 (escaping closure) (3) | 2024.12.18 |
---|---|
백준 14501번 퇴사 Swift (2) | 2024.11.12 |
[Swift] Combination(조합) (1) | 2024.06.16 |
[Swift] Optional Unwrapping (4) - 옵셔널 체이닝 (Optional Chaining) (2) | 2024.04.19 |
[Swift] Optional Unwrapping (3) - 닐 코얼레싱 (Nil-Coalescing) (0) | 2024.01.18 |