Corgi Dog Bark

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [디자인 패턴] SOLID 의 Liskov Substitution Principle
    알고리즘/디자인 패턴(Design Patterns) 2022. 12. 2. 18:49
    반응형

    디자인 패턴에 대한 정리 내용 중 SOLID 원칙의 세 번째 원칙인 Liskov Substitution Principle 즉, 리스프 치환 원칙에 대하여 적어보겠습니다. SOLID 설계 원칙은 유지 보수가 쉽고 재사용 가능하도록 하고, 확장 가능한 개발을 위해 지켜지고 있습니다. 제 세 번째 디자인 패턴 글에서는 C ++의 리스코프 치환 원칙과 이를 사용하였을 때, 장점 등을 보겠습니다.

     

    이 글 시리즈에서는 총 SOLID의 5가지 원칙에 대해 살펴보겠습니다.

    1. SRP - Single Responsibility Principle
    2. OCP - Open/Closed Principle
    3. LSP - Liskov Substitution Principle
    4. ISP - Interface Segregation Principle
    5. DIP - Dependency Inversion Principle

     


     

     

    리스코프 치환 원칙 정리

    리스코프 치환 원칙이란 상속 관계에서, 자식 객체를 부모 객체로 치환해도 문제없이 작동해야 한다는 것입니다.

    흔히 상속을 할 때, 부모에게서 상속받은 자식객체를 사용하곤 합니다. 이때, 리스 코프 치환 원칙이란 자식 객체를 사용할 때도, 부모 객체를 넣었을 때 올바르게 작동을 해야 한다는 것입니다. 저는 처음 리스 코프 치환 원칙을 접했을 때, 왜 이런 리스 코프 치환 원칙이 생겨났는지 궁금했습니다. 

     

    우선 리스코프 치환 원칙을 지켜지지 않을 때, 생기는 불이익을 생각해보겠습니다. LSP 가 지켜지지 않게 된다면, 자식 객체를 생성할 때마다, 다형성을 지키는 코드에서는 그것이 부모 코드에서 파생된 코드인지, 자식 객체에서 파생된 코드인지에 따라 다른 결과치를 제공해주어야 하는 불편함이 생깁니다. (잘 이해가 되지 않으신다면, 뒤의 예제를 보고 돌아오셔도 됩니다.)

     

    이것은 앞서 살펴봤던, OCP(Open-Closed-Principle) 에도 위반되는 상황이므로, 리스코프 치환 원칙에 대해 왜 이러한 원칙을 지키게 되었는지 알아보았습니다.

     


     

    리스 코프치환 원칙 위반 예시

    현실적으로 생각할 수 있는 예시 그리고 리스 코프 치환 원칙에서 가장 널리 많이 쓰이는 예시를 들어보겠습니다. 직사각형의 클래스가 존재하고, 직사각형을 상속받는 정사각형 클래스가 있다고 가정하겠습니다. 

     

    직사각형은 width, 그리고 height의 값을 가지고, getWidth( ), getHeight( ), area( ) 등의 멤버 함수를 가지게 됩니다. 이때, 정사각형은 이러한 멤버 함수들을 다시 override 해 사용하게 됩니다. 그럼 코드로 설명을 이어나가겠습니다.

     

     

    struct Rectangle {
      Rectangle(const int width, const int height)
          : m_width{width}, m_height{height} {}
      int get_width() const { return m_width; }
      int get_height() const { return m_height; }
      virtual void set_width(const int width) { this->m_width = width; }
      virtual void set_height(const int height) { this->m_height = height; }
      int area() const { return m_width * m_height; }
    
    protected:
      int m_width, m_height;
    };
    
    struct Square : Rectangle {
      Square(int size) : Rectangle(size, size) {}
      void set_width(const int width) override { this->m_width = m_height = width; }
      void set_height(const int height) override {
        this->m_height = m_width = height;
      }
    };

    언뜻 생각하기에, 아무런 문제가 일어나지 않을 것 같지만, 다음 코드에서 엉뚱한 넓이 값이 호출되는 결과를 볼 수 있습니다. 

    void process(Rectangle &r) {
        int w = r.get_width();
        r.set_height(10);
        // sqaure 에 대해서는 성립 x
        // 예를 들어 Square s{5} 에 대해서, 기대한 결과값은 50 이지만, 100가 출력되게 됩니다.
        assert((w * 10) == r.area()); 
    }

     

    반응형

     


     

     

    리스 코프치환 원칙 구현 방법 

    1. 좋지 않은 방법 - Type Checking

    각 Rectangle에 대해서, dynamic_cast를 통한 타입 체킹을 통해, 어떤 클래스인지 확인하는 방식입니다. 하지만 이는 LSP 원칙이 지켜진다면, 굳이 Type-Checking을 하지 않아도 된다는 점이 단점입니다. (속히 시간 낭비라는 이야기입니다.)

    void process(Rectangle &r) {
        int w = r.get_width();
        r.set_height(10);
        // sqaure 클래스 인지 아닌지 확인
        if (dynamic_cast<Square *>(&r) != nullptr)
            assert((r.get_width() * r.get_width()) == r.area());
        else
            assert((w * 10) == r.area());
    }

     

     

    2. Sub Class 인지 확인하는 방법.

    마찬가지로 선호되는 방법은 아니지만, bool 타입 객체를 활용해서, 어떻게 사용되었는지 확인하는 방법이 존재합니다. 하지만 OCP 원칙을 위배합니다. - Is_square( ) 라는 함수를 통해 구현하였습니다.

    void process(Rectangle &r) {
        int w = r.get_width();
        r.set_height(10);
        if (r.is_square())
            assert((r.get_width() * r.get_width()) == r.area());
        else
            assert((w * 10) == r.area());
    }

     

    3. 올바른 상속 사용

    실제 기하학에서는 직사각형이 정사각형을 포함하는 다이어그램 형태 꼴로 나타날 수 있지만, 코드 상의 구현을 달라질 수 있다는 점이 LSP의 가장 큰 난관이지 않을까 싶습니다. 따라서, Shape 형태의 올바른 상속을 사용할 수 있도록 코드를 재작성해줄 수 있도록 합니다.

    struct Shape {
        virtual int area() const = 0;
    };
    
    struct Rectangle : Shape {
        Rectangle(const int width, const int height) : m_width{width}, m_height{height} {}
    
        int get_width() const { return m_width; }
        int get_height() const { return m_height; }
    
        virtual void set_width(const int width) { this->m_width = width; }
        virtual void set_height(const int height) { this->m_height = height; }
    
        int area() const override { return m_width * m_height; }
    
    private:
        int m_width, m_height;
    };
    
    struct Square : Shape {
        Square(int size) : m_size(size) {}
        void set_size(const int size) { this->m_size = size; }
        int area() const override { return m_size * m_size; }
    
    private:
        int m_size;
    };

     

     


     

    리스 코프 치환 원칙의 장점

    1. 유지 보수

    • LSP 원칙을 지키는 코드는 의존성을 약화시키고, 코드 재사용성을 높여줍니다. (부모 객체와 같은 역할을 수행하기 때문)
    • 올바른 상속 역할을 수행할 거라 기대할 수 있습니다.

    2. 타입의 타당성

    • 상속을 할 때, 타입이 올바른지 확인을 하지 않아도 (위의 위반 사항에 기반하여) 올바르게 작동할 것임을 장담할 수 있습니다.

     


     

     

    결론

    SOLID의 리스 코프 치환 원칙의 장단점 및 예를 활용하여 이 원칙이 무엇을 위해 노력하고 있는지, 느슨한 결합과 올바른 상속을 보장하기 위해 어떠한 결과물을 보여주는지에 대해서 이야기해보았습니다. 

    반응형

    댓글

Designed by Tistory.