리스코프 치환 원칙 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 반더빌트
객체-지향 디자인은 위기에 빠진 소프트웨어를 구할 수 있는 메시아 처럼 받아들여 집니다. 진짜 메시아처럼 그 정체를 정확히 아는 사람도 얼마 없다라는 것도 닮았군요. 소프트웨어 개발의 어려운 점은 개념 자체 이해가 어렵다는 것이 크다고 생각합니다. 이 블로그에서 개념에 집착하는 이유가 거기에 있습니다. 

객체-지향 언어, 객체-지향 디자인, 객체-지향 프로그래밍 이란 용어는 아마도 가장 이해하기 어려운념이며, 구원자를 앞에 두고 알아보지 못하고 있는 것 같은 상황입니다. 객체-지향을 설명하는 수많은 책과 글이 있음에도, 정확히 이해하지 못하는 상황에 빠져 있다는 것은 무엇인가 문제가 있음을 증명하고 있습니다.

주된 이유는 객체-지향이라는 개념이 모호한 용어의 조합으로 설명하기 때문이며, 사람에게 익숙한것은 객체-지향이 아니라 절차적 프로그래밍이라는 생각이 들기도 합니다. 기본적으로 사람의 사고 체계의 기본 단위는 절차적입니다. 한 발짝 뒤로 물러 서서 보면 절차를 감싸고 있는 객체가 있음에도 희안하게 그게 잘 보이지 않습니다.

객체-지향을 좀더 쉽고 정확하게 설명하자, 그리고, 객체-지향이 만들어 내는 현상에 현혹되지 말고 핵심을 설명하자는 것이 객체-지향을 수차례에 걸쳐 포스팅 하는 이유입니다.

이전 포스트 : 객체-지향 프로그래밍 이란 무엇인가 (OOP)? 는 객체-지향의 각론에 대한 이야기입니다.  

간단한 프로그램을 작성한다면 구조적 프로그래밍으로도 충분합니다. 뉴튼의 만유인력의 법칙 만으로도 태양계 행성들과 자연계의 움직임을 근사값에 가깝게 설명할 수 있는 것 처럼 말이죠. 하지만, 복잡한 프로그램을 관리가능하게 작성하려면 아인슈타인의 상대성이론을 적용해야 합니다. 그게 문제 인거죠. 만유인력의 법칙이 구조적 프로그래밍 이라면 상대성 이론이 객체-지향 프로그래밍 입니다.

객체-지향을 설명하는 많은 글들 중에 저의 생각과 일치하는 책이 있어, 객체-지향 프로그래밍 개념에한 단락을 번역 포스트 합니다. 98년에 출판된 책인데 번역서는 없더군요.  바로 Stephen Gilbert 와 Bill McCarty의 저서 Object-Oriented Design in Java, published by Sams, 1998 입니다. 


객체-지향 프로그램이란 상호작용 하는 객체의 집합 입니다.




객체-지향을 정확하기 이해하기 위한 5개의 개념 정의와 중요한 관점에 대해 정리해 보자면, 

1. 객체란 무엇인가?
데이터, 행위, 아이덴티티를 가지고 있는 것.

2. 클래스란 무엇인가? 그리고, 객체와 클래스의 차이는 무엇인가?
클래스란 객체를 생성하는 청사진 이다.

3. 캡슐화란 무엇인가?
행위와 상태를 포장하고 외부에 노출할 것과 감출 것을 결정하는 것. 외부에 노출되는 모든 것을 인터페이스 interface 라고 할 수 있다.

4. 상속이란 무엇인가?
가족 관계를 나타내는 것이다. 자식은 부모의 자산을 이용할 수 있으며, 코드 재사용이 첫번째 달성되는 것이며,  상속은 클래스의 계층 구조를 표현 할 수 있게 되는데, 계층 구조는 그 구조 자체만으로 수 많은 정보를 포함 할 수 있다. 계층 구조를 표현하는 것이 두번째 핵심이다.

많은 문서에서 상속을 코드 재사용의 관점에서 기술하고 있지만, 상속의 핵심은 계층구조를 표현 하는 것입니다. 재사용만을 목적으로 상속을 이용한다면 가장 강력하게 커플링된 재사용방법을 쓰고 있는 것입니다.  공통 라이브러리를 사용하는 것이 훨씬 좋은 방법이죠. 계층구조를 포함할 목적이 아니라면 상속을 사용해서는 안되며, 코드 재사용 보다 계층구조의 표현이 상속에서 더욱 중요한 개념이다 라는 것입니다. 

 
5. 다형성이란 무엇인가? 
캡슐화, 상속과 함께 동작 함으로써 객체-지향 프로그램의 흐름 제어를 단순화 하는 것 입니다.

다형성을 설명하는 많은 문서는  
서브클래스의 오버라이드된 메소드가  다른 작업을 할 수 있어서 다양한 형태라는 다형성이라고 설명하는데 포커스가 맞춰 있지만 다형성의 핵심은 흐름 제어 Flow of Control 를 객체로 처리 하도록 단순화 하는 것 입니다.



아래는 
Stephen Gilbert 와 Bill McCarty의 저서 Object-Oriented Design in Java, 1998 의 객체-지향 프로그래밍 설명 부분의 번역 입니다.


번역 : 

객체-지향 프로그램 Object-Oriented Programs

객체-지향 프로그래밍은 오늘날 프로그램의 복잡성을 극복하고 있는 듯 합니다. 프로시저를 object라고 하는 유닛으로 그룹화 함으로써 프로그램은 더 적은 블록을 요구하고 결과로 단순해 졌습니다.


OOP 새로운 것이 아니다? : Is Nothing New?

객체-지향 프로그래밍을 공부하다 보면, OOP에 대한 매우 다른 관점의 말들이 오간다는 것을 쉽게 알아 차릴 수 있습니다. OOP의 다양한 책과 문서들을 읽어보면 사람들이 전혀 다른 것에 대해 이야기 하는 것처럼 보인다. 모든 잡설들을 제고하고 나면 '혁명적 revolutionary' 과 '혁신적 evolutionary' 이라는 두가지 관점이 있다는 것을 알 수 있다. 혁명적 관점을 변론하자면 OOP는 맨손으로 프로그램을 만들던 전통적인 방법과 전혀 다르다는 것이다. 반면에 혁신가의 입장에서 보면, OOP는 전통적인 개념의 재포장 일 뿐이라고 말합니다. 아마도 각각의 견해는 맞는 것도 있고 틀린 것도 있습니다.

절차적 언어로 명확하고, 이해하기 쉬운 프로그램을 작성하는 것이 가능하다고 가정하면 혁신가의 말이 옳습니다. 그리고 객체지향 언어로 작성하더라도 이해할 수도, 관리할 수도 없는 코드가 작성될 가능성이 있습니다. 그러나 혁신가들은 OOP 프로그램이 절차적인 프로그램과는 기반적으로 다른 방법으로 구성된다는 것을 인지하지 못했습니다.


혁명가들은 OOP 디자인 프로세스가 다른 도구와 다른 추상 타입을 사용하며, OOP 프로그램은 함수적 분해를 하지 않는다는 것을 지적합니다. 혁명가들은 OOP 디자인의 명확성과 이해용이성을 과대평가 하는 듯합니다. 잘 설계되고 구현된 절차적 프로그램은 개판으로 짜여진 프로그램 보다 절대적으로 낫습니다. OOP 와 객체지향 언어는 아이디어를 명확하게 표현할 수 있는 도구를 제공하는 것이지, 즉각적으로 효과가 나타나는 만병통치약이 아닙니다. 


주: 
혁명가 : 패러다임이 완전히 다르다는 견해를 가지고 있는 사람. 
혁신가 : 이전의 기술이 개선 되었을 뿐이라는 견해를 가지고 있는 사람.



객체지향 프로그램의 다섯개의 기반 개념.

1. Objects
2. Classes
3. Encapsulation
4. Inheritance
5. Polymorphism

"객체지향 프로그램은 어떠한 목적을 이루기 위해 협업하도록 구성되어지는 객체들의 집합이다."

객체란 무엇인가? What Are Objects?

프로시저를 구조적 프로그램을 작성하는데 사용하다고 단정하면, 객체란 객체지향 프로그램을 작성하는데 사용 되는 것 입니다. 객체지향 프로그램은 어떠한 목적을 이루기 위해 협업하도록 구성되어지는 객체들의 집합입니다. 

모든 객체는 : 

데이터를 가지고 있습니다. - 데이터는 객체의 상태를 기술하는 정보를 저장합니다.
행위의 집합을 가지고 있습니다. - 이 행위들은 메세지를 받았을 때 객체가 어떻개 해야하는지 알고 있는 것 입니다.
개체를 구분하는 아이덴티티를 가지고 있습니다. - 어떠한 객체를 다른 객체와 구분하는 것을 가능케 합니다. 

구조적 프로그램에서 사용되는 레코드,구조체 처럼 객체도 데이터를 담고 있습니다. 이처럼 생각하면 객체는 이전 섹션에서 보았던 급여관리 프로그램에서 보았던 하나의 직원 레코드 처럼 보입니다. 객체의 데이터는 객체의 상태를 표현하기 위해 사용됩니다. 예를 들어, 객체의 데이터를 가지고 그 직원이 정규직원인지 파트타임인지 감지할 수 있습니다.

직원 객체는 작업 행위를 가지고 있다는 데서, 절차적 프로그램의 직원 레코드와는 다릅니다. 이 작업들은 아마도 객체의 데이터를 읽거나 변경하는데 사용될 겁니다. 객체는 자신의 등에 데이터를 메고 있는 작은 프로그램 처럼 동작합니다.


여러분이 어떤 것을 알고 싶거나, 어떤 일을 하기를 원한다면 객체에게 작업을 수행하도록 요청하면 됩니다. 이것을 객체지향 세계의 말로 "메세지" 보낸다 라고 합니다. 응답은 객체의 두번째 특성입니다.  직원 객체는 내장된 행위로 급여에 대해 어떻게 말해야 하는지, 우편물 주소 라벨을 어떻게 인쇄해야 하는지 알고 있을 것입니다.

객체의 세번째 특성은 유일한 아이덴티티를 가지고 있다는 것 입니다. 이것이 모든 객체가 관계형 데이터베이스에서 말하는 ID 값이나 Primary Key를 가져야 함을 의미하는 것은 아닙니다. 객체는 구조적 언어의 '변수'와 매우 닮았다고 할 수 있습니다. 정수 변수 i 와 j는 같은 값을 가질 수 있지만 여전히 구분 될 수 있습니다. 


클래스란 무었인가? What Are Classes?

자바 프로그램이 객체의 집합으로 이루어진다면, 대체 클래스란 뭘까? "학생" 객체의 집합인가? 아닙니다.
클래스는 객체 생성의 청사진 blueprint 입니다. 여러분은 실제 객체에 대한 코드를 절대로 작성할 수 없으며, 객체를 만드는데 사용되는 패턴을 작성할 수 있을 뿐입니다. 클래스와 객체의 구별하는 것은 미묘하지만 객체지향 디자인을 이해하기 위한 기본입니다.
 
클래스와 객체 사이의 관계를 이해하는 좋은 방법은 구조적 프로그래밍 언어의 '타입 Type' 과 '변수 Variable'를 떠올리면 됩니다. 변수의 타입을 이야기 할때 그 변수로 어떤작업을 할수 있는지, 저장할 수 있는 범위가 어떻게 되는지로 기술합니다. 예를 들어, 정수 변수는 복소수를 저장 할수 없습니다. 같은 방법으로 정수와 소수의 곱하기 연산은 가능하지만, 맞춤법 검사는 오로지 문자열에 대해서만 가능합니다.

변수는 값을 저장한다는데 주목하라 : 타입이 하는 일이 아닙니다. 타입은 추상이라고 말할 수 있습니다. 실제로 정수 값을 저장하려면 정수 변수를 생성해야만 합니다. 정수 타입으로 지정된 청사진으로 변수가 생성되면 그 변수는 정수입니다. 정수 타입에 결정된 규칙에 의해 저장되고, 행동합니다. 각각의 i,j,k 정수를 정수 타입의 '인스턴스'라고 말합니다.

객체와 클래스는 비슷한 관계를 가지고 있습니다. 클래스는 객체가 소유하게 될 속성 attributes 과 행위 behaviors 를 정의합니다. 클래스는 객체 생성의 청사진 blueprint 입니다. 자바 프로그램을 작성할 때 남이 작성한 클래스를 사용하기도 하고, 자신의 장치에 맞는 새로운 클래스를 정의하기도 합니다. 새로운 클래스를 생성하는 일은 두 부분으로 나누어져 있습니다. 

1. 객체의 상태를 저장하는데 사용될 속성을 정의 합니다.
2. 객체가 이해할 수 있는 메세지와 메세지에 응답하는 과정을 정의 합니다. 각각의 메세지에 대해 메소드 method 라고 불리우는 프로시저를 만들고, 이것을 구현합니다.


캡슐화란 무엇인가? What is Encapsulation?

객체,클래스와는 다르게 캡슐화는 자바 언어의 요소가 아닙니다. 캡슐화는 잘-설계된 클래스를 만드는데 사용되는 기법입니다. 잘-설계된 객체-지향 클래스는 캡슐화 기법을 요구합니다.

"좋아, 캡슐화라는 것에 기꺼이 주목할께, 하지만 마치 우주인이 가지고 있을 만한 물건처럼 들리는데, 도대체 그게 뭐야." 캡슐화는 프로그램을 포장 packaging 하는 작업입니다. 클래스는 두 부분으로 나눌 수 있습니다: 인터페이스 interface 와 구현 implementation.

"잠깐, 당신은 이미 클래스는 속성 attributes 과 메소드 methods 의 두-부분으로 구성되어 있다고 말했잖아요, 인터페이스와 구현은 속성과 메소드를 부르는 새로운 용어 인가요?" 라고 반응할 수 있습니다.

여러분의 객체는 속성과 메소드로 만들어져 있습니다. 일부 속성과 메소드는 객체의 외부에서 접근할 수 있고 이것을 인터페이스 interface 라고 합니다. 다른 속성,메소드는 객체 자신만의 사적인 용도로 예약되어 있고 이것을 구현 implement 이라고 합니다. 구현으로 부터 인터페이스를 분리하는 것은 객체-지향 프로그램을 설계할 때 가장 중요한 결정 입니다. 

구현으로 부터 인터페이스를 나누는 일의 가치를 살펴보자면, 자동차를 연상하면 쉬울 것입니다. 자동차의 인터페이스는  핸들, 가속 페달, 브레이크로 간단하고 규격화 되어 있습니다. 운전방법을 한번 배우기만 하면 됩니다. 반면에 자동차의 내부 동작은 점화, 실린더, 연료 분사 등등 매년 역동적으로 변경됩니다. 여러분이 각기 다른 타입의 자동차의 점화 시스템을 직접 통제해야 한다면, 새차를 운전하기란 매우 어렵다는 걸 알게 될겁니다. 

잘-설계된 클래스 Well-Designed Class 는 이런 특성을 가지고 있습니다. 인터페이스는 여러분의 클래스와 어떻게 상호작용 해야 하는지 완벽하게 묘사합니다. 그리고 클래스의 대부분의 속성은 감추어 진다는 것을 의미합니다. 사용자는 데이터를 수정하기 위해서 메소드를 사용할 것입니다.



상속이란 무엇인가? What Is Inheritance?

상속 계층을 구조화 하는 것은 클래스 설계의 두번째 결정입니다. 캡슐화는 변경으로 부터 클래스를 견고하게 만드는데 필요합니다. 상속은 클래스 관계에서 "가족 famillis" 개념과 연관이 있습니다.  

"상속의 진가는 강력한 추상 구조화 이다."

클래스의 상속관계를 정의하면 두가지의 큰 잇점을 얻을 수 있습니다. 새로운 종류의 서브클래스를 작성하면, 여려분은 부모 super class 에 이미 내장된 기능들을 사용할 수 있습니다. 이는 상속을 사용하는 가장 일반적인 잇점입니다. 그러나 이게 전부는 아닙니다. 프로그래밍 언어에서 프로시저가 처음 사용 될 때, 중복 코드를 줄이라고 요구합니다. 그리고, 복잡한 컴퓨터를 구성하고 정복하는 엄청난 힘이라는 것이 증명되었습니다.  상속의 진가는 추상화를 구조화 한다는데 있습니다. 프로시저는 복잡한 문제를 단순한 부분으로 나눌 수 있게 합니다. 상속을 사용하면 공통적인 요소를 슈퍼클래스에 정의 함으로써 일반화 할 수 있습니다. 

구조적 프로그래밍은 분해 decomposition (프로시저 형태로의 분해) 에 의한 추상화에 기반하고 있습니다. 객체-지향 프로그래밍은 추상 메커니즘을 이용해 추상화를 포함합니다. 클래스화 classification 에 의한 추상화를 기반으로 상속을 적용하는 메커니즘을 이용합니다. 자연과학에서 처럼 계층구조를 묘사할 수 있음이 계층적 클래스화의 힘입니다. 계층구조는 우리 행성의  수백만 종에 달하는 동,식물의 정보를 구조화하는 강력한 도구 입니다.


다형성이란 무엇인가? What Is Polymorphism?

마지막으로, 다형성은 객체-지향 프로그래밍의 기반 원칙입니다. "많은 형태 many shapes" 라는 뜻을 가진 그리스어에서 유래한 말로,
다형성은 캡슐화, 상속과 함께 작동해서 객체-지향 프로그램의 흐름 제어 flow of control 를 단순화 합니다.

"다형성은 캡슐화, 상속과 함께 동작 함으로써 객체-지향 프로그램의 흐름 제어를 단순화 합니다."

흐름 제어, 다음에 일어날 일을 알아햐 한다는 것은 컴퓨터 프로그램의 아킬레스 건 입니다. 이게 바로 프로그램이 복잡하게 되는 주요 원인입니다. 모든 경로를 추적하지 않고서는 객체가 가질 상태의 가능성을 검사 할 방법이 없습니다. 따라서 여러분은 프로그램을 테스트 하다가 지쳐 버릴 것입니다. 구조적 프로그래밍에서 프로그램의 흐름 제어를 단순화 하는 초기 시도는 조건 없는 분기로 규범화된 순서 구조, 선택, 반복을 제공하는 것 이었습니다. 같은 방법으로 다형성은 상속 계층의 연관된 객체에 메세지를 보냄으로써 단순화 합니다.

객체에 메세지를 보낼 때, 그 객체는 메세지에 응답할 메소드를 가지고 있어야 합니다. 클래스가 상속 계층에 연결되어 있다면, 모든 서브클래스는 부모의 인터페이스를 자동으로 상속 받습니다. 어떤 일이라도 슈퍼클래스 객체가 할 수 있는 것이라면 서브클래스 객체도 할 수 있습니다. 예를 들어, 클래스 A가 클래스 B의 서브클래스 라면, A 객체는 B객체가 할 수 있는 모든 일을 할 수 있습니다. A객체는 B객체라고 말할 수 있고, 이런 경우의 상속 관계를 ISA 관계 라고 부릅니다. 비록 서브클래스 객체가 슈퍼클래스 객체가 하는 것 처럼 동일한 메시지에 응답할 책임이 있다 하더라도, 메세지는 동일한 동작을 하도록 강제하지 않습니다. 이것을 이해할 필요가 있습니다. 각각의 서브클래스는 슈퍼클래스가 정의하고 있는 적절한 응답 또는 새로 정의된 특화된 응답에 의존하고 있습니다. 그래서, 각각의 서브클래스는 같은 메세지에 다른 응답을 할 수 있습니다. 이게 "많은 형태"라고 말하는 의미 입니다.


주 : 다음 동작에 대한 조건의 선택을 객체의 계층구조로 추상화 해 흐름 제어를 다형성으로 처리하는 구현 방법은 마틴 파울러의 리팩토링의 Replace Conditional with Polymorphism(293) 에 기술되어 있습니다.

다형성으로 조건문을 제거하라 Replace Conditional with Polymorphism(293)

객체 전문용어 가운데 가장 멋지게 들리는 말 중의 하나가 다형성(polymorphism)이다. 다형성의 진가는 동작이 그 타입에 따라 변하는 객체를 가지고 있을 때, 명시적으로 조건문을 사용하지 않아도 되도록 한다는 데 있다. 

리팩토링, 마틴 파울러 지음/윤성준,조재박 옮김, 대청, p.293 

Object-Oriented Design in Java, by Stephen Gilbert and Bill McCarty, Sams, 1998 p.32~43
 
### 끝.



저작자 표시
신고

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

   


Posted by 반더빌트
객체지향설계에 있어서 맨 처음 하는 일은 도메인 분석에 의해서 객체를 뽑아 내는 작업 입니다.

명사 는 객체로, 동사는 메소드로, 상태는 객체의 속성으로 구별해 내는 것이지요. 하지만 이 작업이 말처럼 쉽지는 않습니다. 어떤 명사는 명확하게 객체로 구별이 되는데, 어떤 명사는 모호하며, 더 어려운 일은 메소드가 어느 객체에 할당 되어야 하는지 구별하는 것입니다. 또한, 도메인 분석에 의해 구별된 객체 이외에도  관계 및 로직을 구현하기 위해서는 부가 객체들도 필요 합니다. 


어떤 객체가 어떤 책임을 질것 이며, 그 책임에 따라 객체에 할당할 메소드를 찾고, 다른 객체와 공유해야 하는 정보를 구별해 내는 방법론이 Rebecca Wirfs-Brock 와 Brian Wilkerson 이 제안한 Responsibility-driven design, RDD 입니다.

Responsibility-driven design 은 client/server 모델에서 영감을 얻었는데요, 질의에 의한 계약에 집중합니다 : 

1. 이 객체가 책임질 액션은 무엇인가?
2. 이 객체가 공유(Share) 해야할 정보(Information)는 무엇인가?


객체에 책임을 할당 해야 하는데 그 할당하는 패턴 또는 원칙이 General Responsibility Assignment Software Patterns , GRASP 패턴, 또는 원칙 입니다.

객체의 책임을 부여하는 일은 항상 어려운 일 입니다. 마틴 파울러은 '리팩토링 Refactoring' 에서 책임부여의 어려움을 이렇게 말하고 있습니다.

객체 디자인에서 가장 기본이 되는 것 중의 하나(원칙은 아닐지라도)는 책임을 어디에 둘지를 결정하는 것이다. 나는 십년 이상 객체를 가지고 일했지만 처음 시작할 때는 여전히 적당한 위치를 찾지 못한다. 늘 이런 점이 나를 괴롭혔지만, 이제는 이런 경우에 리팩토링을 사용하면 된다는 것을 알게 되었다.
- "리팩토링", 마틴 파울러 , 윤성준 역, 대청미디어, 2002, p.169


GRASP은 이 어려움을 돕기 위한 책임 부여 원칙인 것이지요.

GRASP 은 Information Expert, Creator, Controller, Low Coupling, High Cohesion, Polymorphism, Pure Fabrication, Indirection, Protected Variations. 의 9가지 항목이며, 객체지향디자인 5원칙의 각론으로 볼수 있습니다.

책임은 매우 중요하다. 책임 진다고 하면 의외로 쉽게 해결되는 것도 있다.




9가지 항목에 대한 기술은 김대곤님의 글로 대체 합니다.

김대곤 님의 GRASP 패턴 에서 발췌

GRASP 패턴은 아홉 가지로 구성되어 있다. 사실 각 패턴들이 너무 간단해서 싱거울 정도이다.
  1. Information Expert: 역할을 수행할 수 있는 정보를 가지고 있는 객체에 역할을 부여하자. 단순해 보이는 이 원칙은 객체지향의 기본 원리 중에 하나이다. 객체는 데이터와 처리로직이 함께 묶여 있는 것이고, 자신의 데이터를 감추고자 하면 오직 자기 자신의 처리 로직에서만 데이터를 처리하고, 외부에는 그 기능(역할)만을 제공해야 하기 때문이다.

  2. Creator: 객체의 생성은 생성되는 객체의 컨텍스트를 알고 있는 다른 객체가 있다면, 컨텍스트를 알고 있는 객체에 부여하자. A 객체와 B 객체의 관계의 관계가 다음 중 하나라면 A의 생성을 B의 역할로 부여하라.
    - B 객체가 A 객체를 포함하고 있다.
    - B 객체가 A 객체의 정보를 기록하고 있다.
    - A 객체가 B 객체의 일부이다.
    - B 객체가 A 객체를 긴밀하게 사용하고 있다.
    - B 객체가 A 객체의 생성에 필요한 정보를 가지고 있다.

  3. Controller: 시스템 이벤트(사용자의 요청)를 처리할 객체를 만들자. 시스템, 서브시스템으로 들어오는 외부 요청을 처리하는 객체를 만들어 사용하라. 만약 어떤 서브시스템안에 있는 각 객체의 기능을 사용할 때, 직접적으로 각 객체에 접근하게 된다면 서브시스템과 외부간의 Coupling이 증가되고, 서브시스템의 어떤 객체를 수정할 경우, 외부에 주는 충격이 크게 된다. 서브시스템을 사용하는 입장에서 보면, 이 Controller 객체만 알고 있으면 되므로 사용하기 쉽다.

  4. Low Coupling: 객체들간, 서브 시스템들간의 상호의존도가 낮게 역할을 부여하자. Object-Oriented 시스템은 각 객체들과 그들 간의 상호작용을 통하여 요구사항을 충족시키는 것을 기본으로 한다. 그러므로, 각 객체들 사이에 Coupling이 존재하지 않을 수는 없다. 이 패턴은 요구사항은 충족시키면서도 각 객체들, 각 서브시스템 간의 Coupling를 낮은 수준으로 유지하는 방향으로 디자인하라고 말하고 있다. Low Coupling은 각 객체, 서브시스템의 재 사용성을 높이고, 시스템 관리에 편하게 한다.

  5. High Cohesion: 각 객체가 밀접하게 연관된 역할들만 가지도록 역할을 부여하자. 이 패턴은 Low Coupling 패턴과 동전의 양면을 이루는 것으로, 한 객체, 한 서브시스템이 자기 자신이 부여받은 역할만을 수행하도록 짜임새 있게 구성되어 있다면, 자신이 부여 받은 역할을 충족시키기 위해 다른 객체나 시스템을 참조하는 일이 적을 것이고, 그것이 곧 Low Coupling이기 때문이다.

  6. Polymorphism: 객체의 종류에 따라 행동양식이 바뀐다면, Polymorphism 기능을 사용하자. Object-Oriented 시스템은 상속과 Polymorphism(다형성)을 지원한다. 만약 객체의 종류에 따라 행동이 바뀐다면 객체의 종류를 체크하는 조건문을 사용하지 말고, Object-Oriented 시스템의 Polymorphism 기능을 사용하라.

  7. Pure Fabrication: Information Expert 패턴을 적용하면 Low Coupling과 High Cohesion의 원칙이 깨어진다면, 기능적인 역할을 별도로 한 곳으로 모으자. 데이터베이스 정보를 저장하거나, 로그 정보를 기록하는 역할에 대해 생각해 보자. 각 정보는 각각의 객체들이 가지고 있을 것이다. 이 때 Information Expert 패턴을 적용하면, 각 객체들이 정보를 저장하고, 로그를 기록하는 역할을 담당해야 하지만, 실제로 그렇게 사용하는 사람들은 없다. 이것은 그 기능들이 시스템 전반적으로 사용되고 있기 때문에 각 객체에 그 기능을 부여하는 것은 각 객체들이 특정 데이터베이스에 종속을 가져오거나, 로그을 기록하는 매커니즘을 수정할 경우, 모든 객체를 수정해야 하는 결과를 가져온다. 즉 Low Coupling의 원칙이 깨어지게 된다. 이럴 경우에는 공통적인 기능을 제공하는 역할을 한 곳으로 모아서 가상의 객체, 서브시스템을 만들어라.

  8. Indirection: 두 객체 사이의 직접적인 Coupling을 피하고 싶으면, 그 사이에 다른 객체를 사용하라. 여기서 말하는 다른 객체란 인터페이스가 될 수 있고, 주로 인터페이스인 경우가 많다. 그런 특별한 경우는 아래에 설명된 Protected Variations 패턴이라고 부를 수 있다.

  9. Protected Variations: 변경될 여지가 있는 곳에 안정된 인터페이스를 정의해서 사용하자. JDBC에 대해서 생각해 보자. JDBC는 일련의 인터페이스들로 구성되어 있으며, 각 데이터베이스 벤더들이 인터페이스를 구현한 Concrete 클래스를 제공하고 있다. 데이터베이스 기능을 사용하는 시스템의 입장에선 각 벤더들이 구현방식을 바꾸었을 때, 자신의 코드를 수정하고 싶지 않을 것이다. 그래서 Driver를 로딩하는 코드를 제외하고는 모두 인터페이스를 사용함으로서 데이터베이스의 변경시에도 Driver 로딩만 바꾸어 주면 되도록 데이터베이스 관련 작업이 필요한 곳에는 안정된 JDBC 인터페이스를 사용한 것이다.


http://en.wikipedia.org/wiki/GRASP_(object-oriented_design)
http://scottlee.tistory.com/13
http://blog.naver.com/eatist/10013301919
http://davidhayden.com/blog/dave/archive/2005/03/27/895.aspx
신고

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

   


Posted by 반더빌트
TAG GRASP, OOD, RDD


티스토리 툴바