포스트

[SOLID 원칙] 의존 역전 원칙 (Dependency Inversion Principle, DIP)

의존 역전 원칙은 다음 두 가지의 내용을 담고 있습니다.

  1. 고수준 모듈(High-level Module)은 저수준 모듈(Low-level Module)에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.
  2. 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항은 추상화에 의존해야 한다.

조금 어렵게 느껴질 수 있지만, 쉽게 말하자면, 두 클래스가 서로의 동작을 직접적으로 사용하는 관계가 되어서는 안된다는 것입니다.

만약 두 클래스가 종속(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는 모듈 간의 결합도를 낮추는 것에 초점을 맞추며, 유연성재사용성을 높이는 데 중점을 둡니다.


참고

[SOLID 원칙] 객체지향 설계 5가지 원칙

[SOLID 원칙] 개방-폐쇄 원칙 (Open-Closed Principle, OCP)

[SOLID 원칙] 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.