본문으로 바로가기

SOLID 디자인 원칙 - ② 열림-닫힘 원칙

 

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

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

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

 

Open Closed principle(OCP, 열림-닫힘 원칙)

열림-닫힘 원칙이란? "클래스의 확장에 대해서는 개방, 수정에 대해서는 폐쇄"(위키피디아)라는 의미입니다. 이 의미를 예시를 통해서 살펴보겠습니다. 먼저 OCP 원칙이 지켜지지 않은 예시를 먼저 살펴보겠습니다.

OCP를 위배하는 설계

아래와 같이 동물 클래스가 있습니다.

enum class AnimalType{ Dog, Cat };
class Animal {
public:
    Animal(AnimalType _type) : type(_type) {}
    void Speak();
private:
    AnimalType type;
};
void Animal::Speak()
{
    if (type == AnimalType::Dog)
        cout << "Bark~!" << endl;
    else if (type == AnimalType::Cat)
        cout << "Meow~" << endl;
}

int main()
{
    Animal bingo(AnimalType::Dog);
    Animal kitty(AnimalType::Cat);

    bingo.Speak();
    kitty.Speak();
    return 0;
}

각 동물들을 생성할 때 동물들의 타입을 입력받습니다. 그리고 각 동물들의 Speak() 멤버함수를 호출하게 되면 if... else 문을 통해서 각 동물의 타입에 맞는 문구를 출력합니다. 그런데 만약 Dog, Cat 말고 Cow나 Sheep을 추가한다면 어떻게 될까요?

enum class AnimalType
{ 
    Dog, Cat , Cow, Sheep // Cow, Sheep 추가
}; 
class Animal {
public:
    Animal(AnimalType _type) : type(_type) {}
    void Speak();
private:
    AnimalType type;
};
void Animal::Speak() //Speak 함수 수정
{
    if (type == AnimalType::Dog)
        cout << "Bark~!" << endl;
    else if (type == AnimalType::Cat)
        cout << "Meow~" << endl;
    else if (type == AnimalType::Cow)  
        cout << "Moo~" << endl;
    else if (type == AnimalType::Sheep)
        cout << "Meh" << endl;
}

int main()
{
    Animal myDog(AnimalType::Dog);
    Animal myCat(AnimalType::Cat);
    Animal myCow(AnimalType::Cow);
    Animal mySheep(AnimalType::Sheep);

    myDog.Speak();
    myCat.Speak();
    myCow.Speak();
    mySheep.Speak();
    return 0;
}

위의 코드 처럼 Speak함수를 수정해야 합니다. 즉, Cow와 Sheep에 대해서 확장을 하기 위해서 Speak 함수를 수정했습니다. 이는 "확장에 대해서는 개방, 수정에 대해서는 폐쇄" 라는 원칙을 위반하는 설계입니다.

OCP를 만족하는 설계

위 코드를 인터페이스를 활용하여 OCP를 만족하도록 다음과 같이 수정할 수 있습니다.

using namespace std;
class Animal {
public:
    Animal() {}
    virtual void Speak() = 0;
    virtual ~Animal() {}
};

class Cat : public Animal {
public:
    Cat() {}
    void Speak() override { cout << "Mewo~" << endl; }
};
class Dog : public Animal {
public:
    Dog() {}
    void Speak() override { cout << "Bark~!" << endl; }
};
class Cow : public Animal {
public:
    Cow() {}
    void Speak() override { cout << "Moo~" << endl; }
}; 
class Sheep : public Animal {
public:
    Sheep() {}
    void Speak() override { cout << "Meh~" << endl; }
}; 

int main()
{
    Animal* myCat = new Cat();
    Animal* myDog = new Dog();
    Animal* myCow = new Cow();
    Animal* mySheep = new Sheep();
    myCat->Speak();
    myDog->Speak();
    myCow->Speak();
    mySheep->Speak();
    // 객체 소멸 생략
    return 0;
}

Animal을 인터페이스로 만들고 Animal을 상속받아 동물 클래스를 구현할 경우 몇 개의 동물 클래스를 새로 추가하더라도 이미 만들어져 있는 Animal 클래스를 수정할 이유는 없습니다. 이렇게 "확장에 대해서는 개방, 수정에 대해서는 폐쇄" 라는 원칙을 만족하는 클래스를 작성하였습니다.

OCP를 위반했을 경우의 문제

OCP를 위한 할 경우 새로운 기능을 추가하기 위해서 기존에 많은 테스트를 거쳐 안정성이 검증된 코드를 수정해야 하는 문제가 생길 수 있고, 이미 클라이언트에 배포된 모듈을 수정해야 하는 상황이 생길 수 있습니다. 그래서 OCP를 만족하는 설계를 지향해야 합니다.


<출처>

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

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

위키피디아: https://en.wikipedia.org/wiki/Single-responsibility_principle