본문 바로가기
프로그래밍

SOLID 디자인 원칙 - 리스코프 치환 원칙

by bantomak 2023. 11. 20.

SOLID 디자인 원칙

SOLID는 다음과 같은 디자인 원칙들을 아우르는 약어이다.

 

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

 

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

리스코프 치환 원칙은 이 원칙의 제안자인 바바라 리스코프(Barbara Liskov)의 이름에서 유래했다. 이 원칙은 어떤 자식 객체에 접근할 때 그 부모 객체의 인터페이스로 접근하더라도 아무런 문제가 없어야 한다는 것을 의미한다. 즉, 자식 객체를 그 부모 객체와 동등하게 취급할 수 있어야 한다. 먼저, LSP가 준수되지 않는 경우가 어떤 경우인지 알아보자.

 

아래는 직사각형 클래스이다. 이 클래스는 가로/세로 길이에 대한 get/set 및 면적 계산을 위한 멤버 함수를 가진다.

 

class Rectangle
{
   protected:
      int width, height;
   public:
      Rectangle(const int width, const int height)
         :width(width), height(height) {}
         
   int get_width() const { return width; }
   virtual void set_width(const int width) { this->width = width; }
   int get_height() const { return height; }
   virtual void set_height(const int height) { this->height = height; }
   int area() const { return width * height; }
}

 

이제 직사각형의 특별한 경우인 정사각형을 만들어보자. 이 객체는 가로/세로 get/set 멤버 함수를 모두 오버라이딩 한다.

 

class Square : public Rectangle
{
   public:
      Square(int size) : Rectangle(size, size) {}
      void set_width(const int width) override 
      {
         this->width = height = width;
      }
      
      void set_height(const int heigth) override 
      {
         this->height = width = height;
      }
}

 

언뜻 보기에는 해로울 것이 전혀 없어 보인다. 하지만 이러한 접근 방법은 문제를 일으킨다. 어떤 부분에 문제가 있을까? 단지 멤버 함수 set에서 가로세로 값 모두를 설정할 뿐이다. 여기서 잘못될 일이 뭐가 있을까? 이 객체를 그 부모인 Rectangle 객체로서 접근하면 의도치 않은 상황이 발생한다.

 

void Process(Rectangle& r)
{
   int w = r.get_width();
   r.set_height(10);
   
   cout << "expected area = " << (w * 10)
      << ", get " << r.area() << endl;
}

 

가로길이를 가져오고 세로를 10으로 설정하고, 가져온 가로길이에 상수 10을 곱하여 넓이를 구하고 있다. 이 코드만 볼 때는 계산된 넓이가 틀릴 것 같지 않다. 하지만 Square 객체를 인자로 하여 이 함수를 호출하면 엉뚱한 넓이가 계산된다.

 

Square s {5};
process(s); // 기대한 결과 = 50; 구해진 값 = 25

 

비록 작위적인 예제이지만 여기에서 교훈은 LSP를 준수하지 않으면 파생된 서브 클래스 Square를 그 부모 클래스 Rectangle 타입으로 활용할 때 당장은 괜찮을 수 있어도 나중에 문제가 발견될 수 있다는 것이다. 최악의 경우 제품이 고객에게 큰 문제를 일으키고 나서야 문제를 인지할 수도 있다.

 

그럼 해결책은 무엇인가? 여러 가지 방법이 있다. 개인적으로 선호하는 방법은 애당초 서브 클래스를 만들지 않는 것이다. 서브 클래스를 만드는 대신 아래와 같이 Factory 클래스를 두어 직사각형과 정사각형을 따로따로 생성한다.

 

struct RectangleFactory
{
   static Rectangle create_rectangle(int w, int h);
   static Rectangle create_square(int size);
}

 

정사각형인지 여부를 확인해야 할 수 있다. 이를 위해 아래와 같은 멤버 함수를 둔다.

 

bool Rectangle::is_square() const
{
   return width == height;
}

 

더 대중적인 처리 방법도 있다. Square의 멤버 함수 set_width() / set_height()에서 익셉션(exception)을 발생시키고, 그 멤버 함수를 대신 set_size()를 사용하게 하는 것이다. 하지만 이 방법은 놀람 최소화 원칙을 위배한다. set_width()의 사용은 분명 자연스러운 일이고 평온해야 할 작업이다.  정상적인 숫자가 파라미터로 넘겨졌음에도 익셉션이 발생하는 것이고 예상하기는 어렵다.

 

출처

 

모던 C++ 디자인 패턴 - 예스24

새로운 기능으로 풍부해진 C++로 다시 배운다. C++는 C++11/14/17을 거치면서 강력한 언어로 발전했으며, 표현력이 풍부해졌다. GoF의 전통적인 디자인 패턴을 표현력이 풍부해진 모던 C++로 새롭게 학

www.yes24.com

댓글