[SOLID 원칙] 의존 역전 원칙 (Dependency Inversion Principle, DIP)
의존 역전 원칙은 다음 두 가지의 내용을 담고 있습니다.
- 고수준 모듈(High-level Module)은 저수준 모듈(Low-level Module)에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항은 추상화에 의존해야 한다.
조금 어렵게 느껴질 수 있지만, 쉽게 말하자면, 두 클래스가 서로의 동작을 직접적으로 사용하는 관계가 되어서는 안된다는 것입니다.
만약 두 클래스가 종속(dependency)되거나 결합(coupling)된다면, 이후 수정 사항이 발생할 때, 상당히 많은 부분을 수정하게 되어 유지보수성을 해칠 수 있습니다.
말로만 하면 어렵기 때문에 간단한 코드로 보겠습니다.
의존 역전 원칙 이해하기
DIP를 적용하지 않은 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Light
{
public:
void TurnOn()
{
cout << "Light is on" << endl;
}
void TurnOff()
{
cout << "Light is off" << endl;
}
};
class Switch
{
public:
// Light에 의존
void Operate(Light& light, const string& action)
{
if (action == "on")
{
light.TurnOn();
}
else if (action == "off")
{
light.TurnOff();
}
}
};
int main()
{
Light light;
Switch sw;
sw.Operate(light, "on");
sw.Operate(light, "off");
}
위 예제에서 Switch
클래스는 Light
클래스에 직접 의존하고 있습니다.
이 경우, Switch
클래스가 현재 의존하고 있던(Light
) 클래스 대신 다른 클래스로 변경하려면 기존 코드를 변경해야 하며, 심지어는 Light
외의 기능을 추가(확장)하려고 할 때도 기존 코드를 수정해야 합니다.
DIP를 적용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 추상화된 인터페이스
class Switchable
{
public:
virtual void TurnOn() = 0;
virtual void TurnOff() = 0;
};
// 저수준 모듈
class Light : public Switchable
{
public:
void TurnOn() override
{
cout << "Light is on" << endl;
}
void TurnOff() override
{
cout << "Light is off" << endl;
}
};
// 고수준 모듈
class Switch {
public:
void Operate(Switchable& device, const string& action)
{
if (action == "on")
{
device.TurnOn();
}
else if (action == "off")
{
device.TurnOff();
}
}
};
int main()
{
Light light;
Switch sw;
sw.Operate(light, "on");
sw.Operate(light, "off");
}
이 코드에서는 Switchable
이라는 추상화 된 인터페이스를 이용했습니다.
따라서, Switch
클래스가 Light
클래스 대신 다른 클래스를 사용한다고 하더라도, Switchable
인터페이스를 구현한 다른 클래스를 받기만 하면 되기 때문에, 코드 수정 없이 동작을 변경할 수 있습니다.
또한, Switch
클래스는 Switchable
인터페이스에 의존하여, 다른 Switchable
구현체가 추가되더라도 Switch
클래스는 변경할 필요가 없게 되었습니다.
고수준 모듈 (High-level Module)
- 고수준 모듈은 시스템의 주요 로직이나 규칙을 정의하는 모듈입니다. 이 모듈은 시스템의 중요한 기능과 동작을 처리하며, 일반적으로 추상적이고 포괄적인 역할을 합니다.
- 고수준 모듈은 시스템의 전체적인 동작과 흐름을 제어하며, 저수준 모듈에 의존하여 구체적인 작업을 수행합니다.
- 따라서, 고수준 모듈은 가능한 한 구체적인 구현에 대한 의존성을 피하고 추상화된 인터페이스에 의존해야 합니다.
저수준 모듈 (Low-level Module)
- 저수준 모듈은 구체적이고 세부적인 작업을 수행하며, 고수준 모듈의 요청을 처리합니다.
목적
- 유연성 증가: 고수준 모듈과 저수준 모듈 간의 결합도가 낮아져서 변경에 유연하게 대응할 수 있습니다.
- 재사용성 향상: 추상화를 통해 모듈 간의 의존성을 줄임으로써, 모듈을 다른 프로젝트나 컨텍스트에서 쉽게 재사용할 수 있습니다.
- 테스트 용이성: DIP를 적용하면 모듈을 독립적으로 테스트하기 쉬워집니다. 구체적인 구현 대신 인터페이스나 추상 클래스에 의존하므로, 테스트 시 모의 객체(mock object)를 쉽게 사용할 수 있습니다.
의존 역전이란?
기존에는 고수준 모듈이 저수준 모듈의 구체적인 구현에 의존적이었습니다.
즉, [고수준 모듈->저수준 모듈]로 의존하고 있었습니다.
그러나, DIP를 적용하면, 오히려 저수준 모듈이 추상화 타입에 의존하게 됩니다.
즉, [저수준 모듈->추상화 타입<-고수준 모듈]로 의존 관계가 바뀌게 됩니다.
이때, 이 추상화 타입은 고수준 모듈의 클래스와 함께 고수준 모듈이라고 볼 수 있습니다.
고수준 모듈의 정의가, 시스템의 주요 로직이나 규칙을 정의하는 모듈이고, 이 추상화 타입도 여기에 포함되어야 하기 때문입니다.
이는 중간에 추상화 타입을 어느 입장에서 추상화하느냐로 이어집니다.
예를 들어, 고객에게 어떤 메시지를 보내는 문자 서비스를 만든다고 가정해 봅시다.
그 수단의 하나로 Kakao 서비스를 이용하려고 하고, 중간에 추상화 단계를 하나 더 넣는다고 해서 세부적인 작업에 초점을 맞추게 되어버리면, 이 추상화는 잘못된 것입니다.
즉, 추상화를 할 때, 고수준 모듈의 입장에서 추상화하게 되고, 이는 [저수준 모듈->고수준 모듈]의 의존성 역전으로 이어집니다.
마무리
의존 역전 원칙과 개방-폐쇄 원칙은 둘 다 모두 추상화를 활용하기 때문에 얼핏 보면 비슷한 원칙처럼 보일 수 있습니다. 그러나 DIP는 모듈 간의 결합도를 낮추는 것에 초점을 맞추며, 유연성과 재사용성을 높이는 데 중점을 둡니다.