Design Patterns2008. 5. 13. 18:49

객체지향 설계 원칙 5-2 LSP (Liskov Substitution Principle) 리스코프 대체 원칙


이번에는 리스코프 대체 원칙이다. 이 원칙은 Base-Class 와 Sub-Class 와의 관계에 대한 원칙이다.
이것을 먼저 한마디로 표현하면 다음과 같다.

"기반 타입은 서브타입을 대신 할수 있어야 한다."

말로 풀어서 설명하기는 자신이 없다. Exception 클래스를 예제로 설명해 보겠다.

사용자 삽입 이미지[그림 1]

 


[그림 1] 클 간단한 클래스 구성도 이다. 그럼 이것을 코드로 구현하면 [그림 2]가 된다.


 

 


[그림 2] 의 코드에서 try 부분에서 예외가 발생하면 어떻게 되겠는가 ?
"int* n = new int;" 는 메모리 생성에 대한 부분이므로 이부분에서 오류가 생기면 MemoryExecption 이 발생한다. 그리고 메모리 관련이외의 예외가 발생하면 CExecptoin 에서 catch 할 것이다.

그럼 [그림 2]의 코드에서 메모리 관련 예외를 catch 하는 부분을 제거 한다면 어떻게 되겠는가 ?
그러면 [그림 3]과 같이 코드가 구성될 것이다.

사용자 삽입 이미지[그림 3]


[그림 3] 코드에서 메모리 관련 예외가 발생하면 어떻게 되겠는가 ? CExecption 이 catch 에서 받아 처리 할것이다.

이 예는 기반타입(CExecption) 이 서브타입(CMemoryExecption) 을 대체하는 좋은 예이다.
서브타입을 설계할때 기반타입의 규칙을 잘 이해하여 설계해서 만들어야 하고, 후에 서브타입에 새로운 기능이 추가 될때 이를 기반타입에서도 처리할 수 있어야 한다.  다시말해, 기반 타입은 서브타입의 조건(서브타입의 조건을 포함)보다 더 많은 조건을 처리 할수 있어야 한다.
쉽게 그림으로 보면 [그림 4]와 같은 형태과 되어야 한다는 것이다.

사용자 삽입 이미지[그림 4]



그러면 여기서 한가지 의문을 가질수 있다. "서브타입의 모든 기능을 기반타입에서 소화 한다면, 왜 상속을 받아서 서브타입을 설계해야 하는가 ?" 라는 문제이다. 이 답을 하기전에 좋은 말 하나 먼저 들어보자.

"기반클래스 에서는 서브클래스에서 보다 사전 조건이 더 강해야 하고 사후 조건에서는 서브클래스에서 보다 약해야 한다."

무슨 말인지 감이 안온다. 먼저 사전 조건이란 위에서 Execption 의 예로 설명한 조건으로 생각해도 되겟다. CExecptoin 이 CMemoryExecption 보다 더 많은 예외를 잡아 낼수 있다. 이것은 사전 조건이 더 강하다. 라고 이해 하면 되겠다. 그럼 사후 조건에 대해서 한번 살펴 보자.

CExecption 과 CMemoryExecption 이 가지고 있는 char* ReportError() 메소드를 살표보자.

먼저 CExecption 의 ReportError 메소드를 호출하면 그 결과가 다음과 같다고 가정하자.
"[Error Code: 1234] [Error FileName] Test.cpp"
그리고 CMemoryExecptoin 에서 ReportError 메소드를 호출하면 그 결과과 다음과 같다고 가정하자.
"[Error Code: 1234] [Error FileName] Test.cpp [Error Memory Address] 0x12345678"

먼가 느낌이 오는가 ? 안온다면 [Error Memory Address] 부분에 집중해보자.
CExecption 의 ReportError 메소드에서 [Error Memory Address] 정보를 보여줄 책임이 있는가 ?
그렇지 않다 CExecption 에서는 [Error Memory Address] 대해서 알지 못한다. 그러면 CMemoryExecption 의 ReportError 메소드에서는 [Error Memory Address] 정보를 보여줄 책임이 있는가 ? 맞다. 책임이 있다.
이것을 어렵게 말하면 사후조건은 서브클래스(CMemoryExecption )가 기반클래스(CExecption ) 보다 강하다고 한다.

개인적으로 5원칙 중에 LSP 가 개념적으로 제일 어렵다. "왜 그럴까 ?" 생각해 봤는데 기존에 알고 사용했던 것과 많이 달라서 인거 같다. 이전에 클래스 상속관계에 대한 가이드는 IS-A 관계였다. 그래서 보통 서브클래스를 설계할때 기반클래스의 특징을 가지지만 더 확장하는 개념으로 많이 사용했었던거 같다. 앞으로 서브클래스 설계할때 LSP가 잘 지켜질지 의문이다.

"원칙은 원칙일뿐 현재 상황에서 최선이라고 생각하는 자신만의 원칙을 만들어 가자" ㅡㅡ

 

Posted by 상현달
Design Patterns2008. 5. 6. 17:57
객체지향 설계 원칙 5-1 OCP(Open-Close Principle) 개방 폐쇄 원칙

원리

첫번째 원칙인 개방-폐쇄의 원칙이다.
무엇을 개방하고 무엇을 폐쇄한다는 것일까 ? 이것을 한마디로 표현하는 말이 있다.
"Should be Open for Extension, but Closed for Modification" (확장에는 개방하고 수정에는 폐쇄하라)

이렇게 말하면 또 이해가 안간다 ㅡㅡ 나는 용어에 참 약하다.
그래서 좋은 예를 하나 훔쳐왔다. 핸드폰 이야기 ...

예전에 핸드폰 충전잭은 핸드폰 마다 각기 달랐다 그래서 핸드폰을 새로 구입하면 충전기도 무용지물이 되었다. 그러다 핸드폰 충전잭이 24핀으로 통일 되었다. 그래서 이제는 충전기 하나만 구입하면 핸드폰을 새로 구입해도 그대로 사용할 수 있게 되었다. 그럼 여기서 무엇이 확장이고 무엇이 수정일까 ?
확장은 핸드폰의 종류이다. 어떠한 종류의 핸드폰이라도 24핀 충전기 하나면 OK 이다. 그러면 수정은 무엇일까 ? 수정은 충전핀이다. 이제는 모든 핸드폰의 충전 단자를 24핀으로 만들어야 한다.

좋은 예가 되었나 ? 더 와닿는 예를 생각해 봤는데 ... 쩝 ...

방법

자 그럼 이제 뜬구름 잡는 얘기는 그만 두고 실제로 적용 방법을 알아보자
먼저 OCP 원칙에 의한 설계를 하려면 변화지 않는것과 변하는것을 최대한 엄격히 구분지어야 한다. (사실 말은 쉽게 하지만 이부분은 정말 어렵고 잘 안되는 부분이다.) 그리고 나서 변하는것을 개방(Open) 하고 변하지 않는것을 Close(폐쇄) 하여야 한다. 이것이 무슨 말이냐 하면 변하는 것에 대해서는 변화하는 것은 변화가 쉽게 유연하게 설계하고 변화 하지 않는것은 변하는 것에 영향을 받지 않도록 설계 해야 한다.

그럼 이러한 것들을 유연하게 하려면 도데체 어떻게 해야 하는 것일까 ? 그 답은 추상화와 다형성에 있다.
이렇게 말하면 또 와 닿지 않는다. ㅡㅡ 샘플로서 알아보도록 하자.

% OCP 를 가장 잘 표현한 패턴은 Command 패턴이다.

시나리오

스타 크래프트 라는 게임을 모두 알것이다.
게임에서  SCV, 질럿, 저글링 등 모든 유닛들은  마우스로 선택한 다음 이동하려고 하는 목표 지점에 마우스로 선택하면 선택한 지점으로 이동한다.

이 기능을 구현해야 한다고 생각해 보자.

먼저 단계를 나누자
1, 유닛 선택: 특정 유닛을 마우스로 선택한다.
2, 위치 지정: 유닛이 선택된 상태에서 이동을 원하는 위치로 마우스 클릭 한다.
3, 유닉 이동: 각 종류(질럭, SCV, 저글링 등등)의 유닛별로 선택된 위치로 이동한다.

간략하게 그림으로 그려보자
사용자 삽입 이미지

그림으로 보면 일단 마우스로 유닛을 선택하여 다른 위치로 이동 시키는 부분은 모두 동일하다.
수정이 불필요한 구간이라는 것이다. 이 구간은 폐쇄(Close)해야 한다.

그러면 유닛이동에서 하단 부분은 개방(Open) 되어있다. 왜 그럴까 ?
그것은 유닛별로 이동 속도가 다르고 지상유닛은 걸어가고 비행유닛은 날라가고 각각 행동이 틀리다.
그래서 개방(Open)된 구간에서는 변경이 되어야 하는 구간인 것이다.

그러면 개방(Open) 된 부분의 클래스 구성도로 살펴 보자.
사용자 삽입 이미지
간략하게 그려보면 위 그림과 같을 것이다.
물론 Move 메소드 이외에 새로운 메소드가 추가로 필요하다면 CUint 클래서도 변경이 되겠지만 일단은
유닛이동만 생각하기로 하자.

OCP 는 추상화, 다형성이 중요하다고 말한것을 기억하는가 ? 바로 위에 써 놓았다.
이것이 바로 추상화와 다형성을 보여주는 간단한 샘풀이다.

% C++ 개발자 이면서 아직도 Vitual 키워드를 잘 모른다면 여태까지 객체지향을 제대로 사용하지 않았을 가능성이 높다. 지금이라도 확실히 알고 넘어가길 바란다.

결론

허접한 설명과 허접한 샘플을 만들어 보았다.

다시 정리해 보자면 OCP 에 맞게 설계를 하려면 변하는 것과 변하지 않는것을 먼저 구분하고, 어느 정도까지 추상화 시킬지를 결정해야 한다. 말은 아주 쉽다. ㅡㅡ  
그러나 이 간단한 원리를 잘 적용하기 위해서는 많은 경험이 필요하다. 그냥 막 되는대로 코딩의 경험이 필요한것이 아니라 작은 일이라도 먼저 생각해서 설계하는 경험이 중요하다는 말이다.


 
Posted by 상현달
Design Patterns2008. 5. 1. 23:25

시작 하며 ...

요즘 점점 많은 개발자들이 객체지향 설계에 디자인 패턴을 적용한다. 설계시 뿐만 아니라 리펙토링 에도 많이들 적용한다. 이러한 패턴을 공부하다보면 알게 모르게 먼가 기본이되는 원칙이 있을거 같은 느낌이 든다.
(나만 드나 ? ㅡㅡ) 하여튼 나는 그런 느낌이 들었다. 그런데 정말 그런 원칙이 있단다. 그래서 그 원칙들을 정리해 볼까 한다.

DataBase 설계시 정규화 라는것이 있다. 이것도 아마 5가지 이다.(맞나 ?)  정규화라는 것은 DataBase 설계시 가이드라인이 되어주는 방법이다. 물론 이 가이드라인이 어느 상황에나 유리한 절대 불변의 법칙은 아니다. 어떨때는 이런 정규화 때문에 성능상 더 나쁜 영향을 줄수도 있다. 그래서 이럴대 비정규화를 사용하여 정규화를 원칙을 깨버린다. 객체 지향 설계 원칙도 이와 같다. 더 좋은 설계를 위해서는 굳이 원칙을 따를 필요가 없다. 그리고 5가지 원칙을 사용하다보면 서로 상호 배타적인 것도 있어서 5가지 원칙을 모두 따르는것이 불가능할 때도 있을것이다.

% OCP (Open-Close Principle): 개방-폐쇄의 원칙
% LSP (Liskov Substitution Principle): 리스코프 교체 원칙
% SRP (Single Responsibility Principle): 단일 책임의 원칙
% DIP (Dependency Inversion Principle): 의존 관계 역전 원칙
% ISP (Interface Segregation Principle): 인터페이스 격리 원칙

약자를 풀어보면 대충 감이 오는것도 있고 안오는것도 있을 것이다.
하여튼 이 5가지 원칙을 하나씩 정리해 보도록한다.

Posted by 상현달