'리스코프 교체 원칙'에 해당되는 글 1건

  1. 2011.03.23 리스코프 치환 원칙 - Liskov Substitution Principle for Primer (1)
리스코프 치환 원칙 LSP Liskov Substitution Principle 은 객체-디자인의 S.O.L.I.D 5원칙 중의 가장 이해하기 어려운 원칙이라고 할 수 있습니다. 다른 원칙들은 이름에서 힌트를 얻을 수 있는데 LSP만은 이름으로 부터 힌트를 얻을 수가 없습니다.

이 글을 읽고 있다면 이미 S.O.L.I.D 를 공부해 본적이 있을 겁니다. 독일어나 러시아어 이름 같은  리스코프 라는 어감은 이 원칙을 이해하면 뭔가 대단한 것을 알고 있다는 느낌이 들것도 같습니다. 이해하기 위해 정의를 수십번 읽어 봐도 대부분의 원칙이란걸 대할때 그러하듯 "그래서 이거 가지고 뭘 하라는 건데" 라는 자조섞인 답변이 들리기도 하죠.  

수 많은 문서와 책,  인터넷의 블로그 글들은 리스코프 치환 (교체) 원칙에 대해 다음과 같이 기술하고 있습니다.

서브타입은 언제나 자신이 기반타입 (base type)으로 교체할 수 있어야 한다.

유도된 클래스의 메소드를 퇴화시키거나 불법으로 만드는 일을 피하라. 기반 클래스의 사용자는 그 기반 클래스에서 유도된 클래스에 대해 아무것도 알 필요가 없어야 한다.
java 프로그래머를 위한 UML 실전에서는 이것만 쓴다!. 로버트 C.마틴 지음 / 이용원, 정지호 옮김, 인사이트 출판사, p137,p144  에 있는 정의 입니다. 


"서브타입을 기반타입으로 교체할 수 있어야 한다고?  음... 다음 말은 무슨 뜻인지 잘 모르겠고, 상속 받고, 메소드를 빠짐없이 구현하면 되잖아, 기반 타입에 파생 타입을 대입할 수도 있고, 봐~~ 컴파일도 되고, 작동하잖아, LSP 다 됐어!" 라고 많은 개발자들이 넘어 갔지만, 화장실 갔다가 마무리를 짖지 않은 것 처럼 뭔가 찜찜한 기분이 들었다면, 아래의 내용을 한줄도 빠짐 없이 읽어주시길 바랍니다. 



리스코프 치환 원칙이란?

1998년 Barbara Liskov (MIT 컴퓨터 사이언스 교수, 2008 튜링상 수상) 가 제안한 원칙으로 :

타입 S의 객체 o1 과 타입 T의 인스턴스 o2가 있을 때, 어떤 프로그램에서 타입 T의 객체로 P가 사용된다고 하자. S가 T의 서브타입이라면 P에 대입된 o1이 o2로 치환 된다고 해도 P의 행위는 바뀌지 않는다. 

Barbara Liskov wrote LSP in 1988:
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T." - BarbaraLiskov, Data Abstraction and Hierarchy, SIGPLAN Notices, 23,5 (May, 1988).

로버트 C. 마틴의 LSP 정의  :

참조되는 기반클래스의 함수는 파생클래스 객체의 상세를 알지 않고서도 사용될 수 있어야 한다. 

FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE
CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES
WITHOUT KNOWING IT.


리스코프 치환 원칙이 왜 중요한가?

1. 이 원칙을 지키지 않으면 클래스 계층이 지저분하게 될 꺼에요. 서브클래스 인스턴스를 파라미터로 전달 했을 때 메소드가 이상하게 작동 할 수 있습니다.


2. 슈퍼클래스에 대해 작성된 단위테스트가 서브클래스에 대해서는 작동되지 않을 것입니다.




로버트 C. 마틴 Liskov Substitution Principle

로버트 마틴은 LSP를 다음과 같이 소개 하고 있습니다.
개방 폐쇄 원칙 OCP 은 관리가능하고 maintainable, 재사용 reusable 가능한 코드를 만드는 기반이다. 잘 설계된 코드는 기존 코드의 변경 없이 확장 가능하다. OCP 를 가능케 하는 중요 메커니즘은 추상화와 다형성이다. 추상화와 다형성을 가능케 하는 키 메커니즘이 상속이다. 추상 기반 클래스의 순수 가상 함수로 부터 클래스를 상속 파생 시킴으로써 추상화된 다형 인터페이스를 만들어 낼 수 있다.

상속을 적용함에 있어서 디자인 룰은 무엇일까? 최고의 상속 구조의 특성은 무엇일까? 계층 구조를 만듦에 있어 OCP를 위반하게 하는 덧은 무엇일까?


LSP가 바로 상속의 룰 이며, 최고의 상속 구조 특성을 갖추게 하며, OCP를 위반하지 않도록 인도하는 원칙 이라고 말하고 있습니다.  



정사각형과 사각형, 그리고 심각한 위반, Square and Rectangle, a More Subtle Violation.

LSP를 위반하는 사례를 살펴봅시다. 로버트 C. 마틴의 LSP 문서의 사례 입니다. C++ 로 작성된 예제를 C#으로 바꾸었습니다.

아래와 같이 정의된 Rectangle 클래스를 사용하는 어플리케이션이 있습니다.
 public class Rectangle
    {
        protected double itsWidth;
        protected double itsHeight;
    
        public void SetWidth(double w)
        {
            this.itsWidth = w;
        }

        public void SetHeight(double h)
        {
            this.itsHeight = h;
        }

        public double GetWidth()
        {
            return this.itsWidth;
        }
        
        public double GetHeight()
        {
            return this.itsHeight;
        }
    }


이 어플리케이션은 많은 사이트에 설치 되었고, 매우 잘 작동하고 있다고 상상해 봅시다. 매우 성공한 소프트웨어라고 할 수 있습니다. 어느날 사용자가 정사각형 square 요소를 추가해 달라고 요구해 왔습니다. 상속은 ISA 관계이고, 새로 추가할 객체가 기존 객체의 ISA 관계를 만족 시킨다면, 새로운 객체는 기존 객체로 부터 파생 시킬수 있습니다.


명확하게 정사각형은 사각형의 일반적인 의도를 만족 시켰습니다. ISA 관계가 성립되기 때문에, Square 클래스를 Rectangle 클래스로 부터 파생시키는 것은 논리적으로 완벽합니다.

ISA 관계를 사용하는 것은 객체-지향의 기반 기법을 사용하는 것으로 사료됩니다. Square 클래스를 Rectangle로 부터 파생시켜면 문제가 있을 수 있지만, 이런 문제는 실제로 코드에 적용해 보지 않고서는 알 수 없습니다.
 

Rectangle, Square 상속 다이어 그램



우리가  눈치 챈 첫번째 문제는 Square는 itsHeight, itsWidth 의 두개의 멤버 변수가 필요하지 않지만 상속된다는 것 입니다. 이건 명확하게 쓸모 없는 것이고, CAD와 같은 프로그램에서 수백 추천개의 정사각형을 그리게 된다면 문제가 발생할 건 확실합니다.


메모리 효율을 문제 삼지 않는다고 가정합시다. 다른 문제가 있을 까요?  이런! Square 는 SetWidth 와 SetHeight 함수도 상속 받습니다. Square는 가로와 세로가 같아야 하기 때문에 설계의 문제가 발생한 겁니다. 이 문제를 해결하기 위해 아래와 같이 오버라이드 override 할 수 있습니다.
 


  public class Square : Rectangle
    {
        /// 
        /// 정사각형이기 때문에 너비를 할당하더라도 높이도 같이 할당해 줌.
        /// 
        /// The w.
        /// 기반 타입의 SetWidth를 호출 하면 
        /// 기반 타입의 메소드가 호출 됨.
        /// 
        public new void SetWidth(double w)
        {
            base.itsWidth = w;
            base.itsHeight = w;
        }

        /// 
        /// 정사각형이기 때문에 높이를 할당하더라도 너비도 같이 할당해 줌.
        /// 
        /// The h.
        /// 
        /// 
        public new void SetHeight(double h)
        {
            base.itsWidth = h;
            base.itsHeight = h;
        }
    }



누군가 Square 객체에 가로값을 적용한다 해도 높이가 자동으로 적용되고, 정사각형이라는 논리를 위반하지 않습니다.



하지만 아래의 함수를 고려해 보면 문제가 있습니다.


Square 객체의 Height가 변경되지 않을 것이기 때문에 지금까지 쌓아 놓은 것은 무너 집니다. Rectangle의 SetWitdth , SetHeight가 virtual 로 선언 되지 않았기 때문에 발생한 문제 입니다.

 


     /// 
        /// Fs the specified rect.
        /// 
        /// The rect.
        /// 
        /// 
        static void f(Rectangle rect)
        {
            rect.SetWidth(32);
        }




진짜 문제 The Real Problem

이제 우리는 Rectangle, Square 두개의 클래스를 가지고 있고, Square 객체로 어떠한 일을 하더라도 문제가 없어 보입니다. Square 는 수학적으로도 일관성이 있고, Rectangle 도 문제가 없습니다. 

아래의 함수 g를 생각해 봅시다.
 


     /// 
        /// Gs the specified rect.
        /// 
        /// The rect.
        /// 
        /// 
        static void g(Rectangle rect)
        {
            rect.SetWidth(5);
            rect.SetHeight(4);

            //파라미터로 Square 타입이 전달되면 Assertion 에러를 발생 시킵니다.
            System.Diagnostics.Debug.Assert(rect.GetWidth() * rect.GetHeight() == 20);
        }



Rectangle의 메소드를 실행시키는 함수 입니다. Rectangle 객체에 대해서 올바르게 동작하고, Square 객체가 전달 될 때는 assertion 에러를 발생하도록 한겁니다.

함수 g를 만든 프로그래머는 매우 합리적인 가정을 했습니다. 다른 프로그래머가 함수 g에 Square 객체를 전달하면 assertion  에러를 보고 받을 테니깐요.

하지만, 부모인 Rectangle 객체에서 작동하는 행위가 Square 객체에 대해서 작동하지 않는다는 것은 LSP를 위반하는 것이고, 파생클래스 행위의 내부 상세를 안다는 것은 OCP open-close principle 또한 위반 하는 것입니다. 


public class Rectangle
    {
        protected double itsWidth;
        protected double itsHeight;
    
        public virtual void SetWidth(double w)
        {
            this.itsWidth = w;
        }

        public virtual void SetHeight(double h)
        {
            this.itsHeight = h;
        }

        public double GetHeight()
        {
            return this.itsHeight;
        }

        public double GetWidth()
        {
            return this.itsWidth;
        }
    }

    public class Square : Rectangle
    {
        /// 
        /// 정사각형이기 때문에 너비를 할당하더라도 높이도 같이 할당해 줌.
        /// 
        /// The w.
        /// 기반 타입의 SetWidth 메소드를 호출하더라도
        ///  파생 타입의 메소드가 호출됨.
        /// 
        public override void SetWidth(double w)
        {
            base.itsWidth = w;
            base.itsHeight = w;
        }

        /// 
        /// 정사각형이기 때문에 높이를 할당하더라도 너비도 같이 할당해 줌.
        /// 
        /// The h.
        /// 
        /// 
        public override void SetHeight(double h)
        {
            base.itsWidth = h;
            base.itsHeight = h;
        }
    }



무엇이 잘 못 되었는가? What Went Wrong (W3)


무슨 일이 일어 난거야? Rectangle과 Square라는 논리적인 모델이 뭐가 잘 못 된거지?  무엇보다도 정사각형은 사각형이다. ISA 관계도 만족 시키잖아!!!.


정사각형은 사각형이지만, Square 객체는 Rectangle 객체가 아닙니다. 왜 일까?  Square 객체의 행위는 Rectangle 객체의 행위와 일관성이 없습니다. 행위적으로 Square 는 Rectangle 이 아니고, 행위는 소프트웨어의 거의 모든 것입니다.

LSP는 OOD의 행위적 ISA 관계를 명확하게 해줍니다. : 본질적인 행위보다 클라이언트가 의존하는 비본질적인 행위를 더욱 중요시 합니다. 함수 g를 작성한 프로그래머는 사각형의 너비와 높이는 독립적으로 설정될 수 있다는 사실에 근거하였습니다. 두 변수의 독립성은 다른 프로그래머들이 의존하는 외적으로 공개적인 행위 입니다. 



계약에 의해 설계하기 Design by Contract

Bertrand Meyer (Eiffel programming language 창시자, Object Oriented Software Construction 저자) 에 따르면  LSP와 Design by Contract 는 매우 깊은 관계가 있습니다. 클래스 메소드의 선행 조건 precondition 과 후행조건 postcondition 에 이런 구조를 적용한다면, 메소드를 실행하기 위해선 선행조건이 만족해야 하고, 그렇다면, 메소드는 후행조건이 참일 것이라는 것을 보장 할 수 있습니다.


Meyer 는 파생클래스의 선행조건과 후행조건에 대한 규칙을 정의 했습니다.: 선행조건이 약하고, 후행조건이 강할 때에만 루틴을 재정의가  가능하다.

...when redefining a routine [in a derivative], you may only replace its
precondition by a weaker one, and its postcondition by a stronger one.

Object Oriented Software Construction, Bertrand Meyer, Prentice Hall, 1988, p256


다시말해서, 기반클래스의 인터페이스를 사용할 때, 사용자는 오로지 기반클래스의 선행조건과 후행조건만을 알고 있기 때문에, 파생 객체는 기반클래스 보다 더 강한 요구사항을 사용자가 지키라고 강요할 수 없다.  또한 파생 클래스는 기반 클래스의 모든 후행 조건을 만족 시켜야만 한다.  이것이 파생클래스의 행위에 대한 결과가 기반 클래스의 제약 사항을 위반하지 않는 것이고, 기반 클래스의 사용자는 파생클래스의 결과물에 혼란스럽지 않게 될 것이다.



맺음말

오리처럼 생겼고, 오리 울음 소리를 내지만 배터리가 필요하다면 - 아마도 잘못된 추상화를 한것이다. LSP



객체-지향 디자인에서 상속에 대한 룰을 어느정도로 엄격하게 지키냐는 가치판단의 문제 입니다. LSP 원칙은 상속에 대한 룰을 가장 엄격함을 적용하는 것이라 할 수 있습니다. 상속을 무분별하게 적용하고 있는 현재의 관행은 상속을 적용함에 있어 더욱 엄격해야 한다는 것이 커뮤니티의 주류 의견되고 있습니다. 객체-지향 디자인이 이루고자 하는 목적이 작동하기만 하는 소프트웨어가 아닌 관리가능한, 재사용가능한, 신뢰할 수 있는, 유연한 소프트웨어의 개발이기 때문입니다.

### 끝.

참고자료 :
로버트 C. 마틴 Liskov Substitution Principle.pdf [Martin96] Robert C. Martin, Engineering Notebook, C++ Report, Nov-Dec, 1996.
 

리스코프 치환 원칙 잘 정리된 wiki 문서


책 : java 프로그래머를 위한 UML 실전에서는 이것만 쓴다!. 로버트 C.마틴 지음 / 이용원, 정지호 옮김, 인사이트 출판사, p137,p144 
저작자 표시
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트


티스토리 툴바