본문으로 바로가기

디자인 패턴과 SOLID 디자인 원칙 - ⑤ 의존성 역전 원칙

디자인 패턴은 소프트웨어 개발과정에서 자주 마주치게 되는 문제들에 적용할 수 있는 몇 가지 패턴들을 의미합니다. 디자인 패턴을 활용하면 읽기 쉽고, 이해하기 쉽고, 수정하기 쉬우며 재사용성이 높은 코드를 작성할 수 있게 됩니다. 그리고 이런 효율적인 코드를 작성하기 위해서 제안된 중요한 다섯 개의 원칙이 있습니다. 이 원칙들의 앞글자를 따서 SOLID 디자인 원칙이라 부르고 그 다섯 개의 원칙은 다음과 같습니다.

  • Single Responsibility Principle(SRP, 단일 책임 원칙)
  • Open-Closed Principle(OCP, 열림-닫힘 원칙)
  • Liskov Substitution Principle(LSP, 리스코프 치환 원칙)
  • Interface Segregation Principle(ISP, 인터페이스 분리 원칙)
  • Dependency Inversion Principle(DIP, 의존성 역전 원칙)

이 다섯개의 원칙들은 디자인 패턴이 존재하는 이유에 모두 녹아들어 있기 때문에 디자인 패턴을 본격적으로 다루기 전에 이 5개의 원칙들을 먼저 살펴볼 필요가 있습니다. 이번 글에서는 네 번째 인터페이스 분리 원칙을 살펴보겠습니다.

Dependency Inversion Principle(DIP, 의존성 역전 원칙)

의존성 역전 원칙이란? "상위 모듈이 하위 모듈에 종속성을 가지면 안 된다. 양쪽 모두 추상화에 의존해야 한다."라는 원칙입니다. 이것을 쉽게 풀어서 보면 "자신보다 변하기 쉬운 것에 의존하지 말라"라는 뜻입니다. 이번에도 예제를 통해서 살펴보도록 하겠습니다.

DIP를 위배하는 설계

먼저 상위 모듈이 하위 모듈에 의존하는 상황입니다. 운전자 클래스 Driver가 있고, 운전자가 탈 수 있는 Avante 클래스와 Sonata 클래스가 있다고 생각해 봅시다. 그리고 Driver 클래스가 직접적으로 Avante 클래스를 사용(종속된)하면 아래와 같습니다.

이를 코드로 살펴보면 아래와 같습니다.

class Avante {
public:
    Avante() {}
    void DriveAvante() { cout << "Drive a avante" << endl; };
};

class Sonata {
public:
    Sonata() {}
    void DriveSonata() { cout << "Drive a sonata" << endl; };
};

class Driver {
public:
    Driver(const shared_ptr<Avante>& car) : mCar(car){}
    void Drive() { mCar->DriveAvante(); }
private:
    shared_ptr<Avante> mCar;
};
int main()
{
    Driver tom(make_shared<Avante>());
    tom.Drive();
    //Driver alice(make_shared<Sonata>()); // 확장 불가능

    return 0;
}

위 코드의 문제는 운전자가 차를 Sonata로 바꿀 경우 발생합니다. Driver가 Avante에 의존하고 있던 모든 코드를 수정해야 합니다. Sonata와 Avante는 동일한 인터페이스를 제공해야 할 의무가 없기 때문에 각자가 제공하는 인터페이스가 다를 수 있기 때문입니다. 이렇게 DIP를 위배하는 설계에서는 하위 모듈이 변경됨에 따라 상위 모듈의 곳곳에 수정사항이 발생합니다. 이는 확장에는 개방, 수정에는 폐쇄라는 열림-닫힘 원칙에도 위배되는 모습입니다.

DIP를 만족하는 설계

의존성 역전 원칙을 만족하기 위해서 다음과 같이 추상 클래스를 사용할 수 있습니다. 상위 모듈인 Driver가 추상 클래스인 Car에 의존하도록 수정합니다. 그리고 하위 모듈인 Avante와 Sonata 또한 추상 클래스인 Car에 의존하도록 수정합니다.

위 그림을 보면 Avante의 의존관계가 역전됨을 볼 수 있습니다. DIP를 위배하는 설계에서는 Avante가 Dirver에 의존 받는 관계였지만 지금은 Avante가 Car에 의존하는 관계가 되었습니다. 그래서 의존관계 역전이라는 이름이 사용되고 있습니다. 이 관계를 코드로 표현하면 다음과 같습니다.

class Car {
public:
    virtual void Drive() = 0;
};
class Avante : public Car{
public:
    Avante() {}
    void Drive() { cout << "Drive a avante" << endl; };
};

class Sonata : public Car{
public:
    Sonata() {}
    void Drive() { cout << "Drive a sonata" << endl; };
};

class Driver {
public:
    Driver(const shared_ptr<Car>& car) : mCar(car){}
    void Drive() { mCar->Drive(); }
private:
    shared_ptr<Car> mCar;
};
int main()
{
    Driver tom(static_pointer_cast<Car>(make_shared<Avante>()));
    tom.Drive();
    Driver alice(static_pointer_cast<Car>(make_shared<Sonata>())); // 확장 가능
    alice.Drive();

    return 0;
}

이제 Driver 클래스의 수정 없이 Sonata를 사용할 수도 있고, Avante를 사용할 수 있습니다. 심지어 다른 기종을 무수히 확장하더라도 Car라는 추상 클래스를 상속받았다면 모두 Driver 모듈에서 활용할 수 있게 되었습니다.

 


<출처>

유튜브 채널 "코드없는 프로그래밍": https://www.youtube.com/channel/UCHcG02L6TSS-StkSbqVy6Fg

길벗 출판사: 모던 C++ 디자인 패턴(Dmitri Nesteruk 저)