본문 바로가기
[ Program ]/Program Etc.

객체지향설계 5원칙

by 관이119 2012. 9. 13.
오늘도 별도 없는 하늘을 쳐다본다 | 또리장군
http://blog.naver.com/parkjy76/30057770855
출처 : PHPSCHOOL

1. OCP (Open closed principle)

버틀란트 메이어박사가 1998년 객체지향 소프트웨어 설계라는 책에서 Open/Closed Principle 언급함.
http://en.wikipedia.org/wiki/Open/closed_principle#Meyer.27s_Open.2FClosed_Principle

" 소프트웨어 구성 요소(컴포넌트, 클래스, 모듈, 함수등 )는 확장에 대해서는 개방되어야 하지만
변경에 대해서는 폐쇄되어야 한다고 언급했습니다."
먼저 이원리를 설명하기전에, 부절적한 예를 들어 보겠습니다.

예 : 휴대전화와 충전기의 관계
http://www.zdnet.co.kr/ArticleView.asp?artice_id=00000039134727
최상훈(핸디소프트) - 마소에 기재된 글

인용문 =>
휴대전화마다, 충전기가 달라서, 휴대전화변경시 충전기도 같이 변경해야 하는 불편함을
충전기 자체를 24핀 표준화하므로써,
이제 휴대전화만 사고 충전기는 재사용할 수 있게 됐다.
따라서 휴대전화의 여러 종류에는 ‘개방하지만’
충전기의 쓸데없는 생산은 ‘닫아두는’ 효과를 얻은 것이다.
바로 이번 호에서 소개할 개방-폐쇄의 원칙을 잘 반영한 결과라고 생각한다.

=>위의 예는 ocp 원칙을 설명하기에는 2% 부족하거나, 잘못된 예라고 볼수 있습니다.
왜냐하면, 이예제는 변경되는 부분은 표준화(규격통일)해라.
그러면, 재사용성이 높아진다 라는 것이지만,
그러면, 클래스등 s/w요소를 개발할때, 표준화하면서 개발해라. 라는 결론을
도출할 위험성이 있기때문입니다.

위의 예를 근거로 클래스를 설계하면서, OCP 원리를 지켜라 라고 하면 생뚱맞아 집니다.
왜나하면, 클래스를 표준화하면서, 개발해라. 아니면, 개방 폐쇄원리를 적용하면서, 개발해라 것은
너무 추상적이기 때문입니다.

그럼 메이어박사가 이야기한 OCP에서 강조하고 있는 것은 무엇일까요?
풀이하면, 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소는 수정이 일어나지 말아야 하며,
기존 구성요소를 쉽게 확장해서 재사용할수 있어야 한다는 뜻입니다.
그러기 위해서는 구성요소의 단위속성중 외부로 노출할것과 노출하지 말것을 구분하여야 한다는 겁니다.
즉, 변하지 않을 필수적인 단위속성과 변할 가능성이 있는 비 필수적인 단위속성을 구분해서,
필수적인 요구사항만, 구성요소(클래스)안에 구현하고, 나머지는, 구현자체를 생략하라는 겁니다.

예를 들어 전자제품을 설계해보도록 하겠습니다.
전자제품의 속성을 먼저 나열해보면
1. 전기를 필요로한다.
2. 끄고, 켤수있다.
3. 작동하고, 멈춘다.
4. 소리가 난다.
5. 통신을 한다.
6. 방송을 본다.
7. 빨래를 한다.
8. 시원하게 한다.
9. 게임을 한다.
10. 복사를 한다.
....

전자제품의 속성은 위의 경우말고라도 무수히 많이 존재합니다.
여기에서 1번부터 3번까지는 전자제품의 고유한 기능이고,
4번이하는 개별전자제품의 기능이라고 분리할수 있습니다.

<?php
class electronic {
private $bolt = '' ;
public function setBolt($input) { $this->bolt = $input ; }
public function getBolt() { return $this->bolt ; }
public function powerOn() {}
public funciton powerOff() {}
public function play() {}
public funciton stop() {}
}
?>
1번부터 3번까지의 고유한 속성만을 모아서 전자제품 클래스를 만들었습니다.

이제 tv라는 전자제품을 만들어라는 추가 요구사항이 발생하였습니다.
tv는 리모컨으로 파워를 온,오프라는 기능이 있습니다.

<?
class tv extends electronic {
public function setChannel() {} //추가기능
public function setVolumn() {} //추가기능
private function remote($mode) { } //추가기능 : 리모트 기능
public funciton powerOn() { this->remote('on'); }
public funciton powerOff() { this->remote('off'); }
}
?>
상속의 다형성을 이용해서
추가요구사항 발생에도 불구하고, 기존 클래스인 electronic 수정이 발생하지 않고도,
쉽게 tv 클래스를 만들수 있습니다.

=>메이어박사가 언급한 ocp 개념에서는 상속에 의한 다형성이 중요하였지만,
요즘은, 추상인터페이스 방식의 ocp원칙을 중요시 하고 있는것 같습니다.(이부분은 예제생략)

정리) 보통 객체지향 입문자의 경우, 발생할지 안할지 모르는 요구사항을 미리 고민해서,
클래스 설계에 반영해야 한다는 강박관념이 있는듯 합니다.
이 원리에서 강조하고 있는것은, 발생할 모든 요구사항을 파악하는 것이 중요한것이 아니라,
변경되지 않을 필수적인 요구사항만 파악해라는 뜻으로 생각할 수 있습니다.

ocp 적용설계시 주의할점3가지
- 확장되는 것과 변경되지 않는 모듈을 분리하는 과정에서 크기 조절에 실패하면 관계가 더 복잡해진다.
- 인터페이스는 가능하면 변경해서는 안 된다
- 인터페이스는 본질적인 특성만 추상화해야 한다.


2. SRP (The Single responsibility principle) - 단일책임원리
http://en.wikipedia.org/wiki/Single_responsibility_principle
(Robert C. Martin 언급함)

Martin defines a responsibility as a reason to change, and concludes that a class or module should have one, and only one, reason to change
(첵임이란, 변경해야 할이유이다. 이것은 클래스 또는 모듈이 변경할 이유가 단 한가지만 가져야 한다는 것을 의미한다.)

모든 객체는 단일책임을 가져야 한다.라고 언급하였습니다.
즉 클래스나 모듈은 단지 하나의 이유에 의해서만 변경되어야 한다는 것을 강조하는 원리입니다.

두개이상의 책임을 하는 경우, 변하는 책임에 의해 변하는 않는 책임도 영향을 받아서,
연쇄적인 수정이 발생할 가능성 또는 위험성을 미연에 방지하자는데 있습니다.
얼핏보면, SRP는 OCP와 비슷해보이지만, OCP는 클래스에 존재해야 할 책임의 범위를 규정하는 것이라면,
SRP는 단일 책임을 강조하는 겁니다.
여기서 단일책임은 메소드 한개만을 의미하지는 않습니다.

예) 로그인 처리는 아이디 및 패스워드로 인증여부를 체크후,
인증에 성공하면, 세션이나 쿠키로 인증사실 기록후, 페이지 처리하는 행동을 합니다.

즉 loginProcess 는 다음과 같이 3개의 책임(행동)이 존재할수 있습니다.

<?php
class LoginProcess {
//1.로그인처리
function loginCheck($id,$password) {}
//2.인증처리
function saveAuthority() {}
//3. 페이지처리
function movePage() {}
}
?>

=>SRP 원칙에 의거 클래스를 분리하겠습니다.

<?php
class Login {
function Check($id,$password) {}
}

class Authority {
function save() {}
function delete() {}
}

class PageControl {
function go() {}
function back() {}
}
//구현 및 사용법 생략
?>


정리) 클래스나 모듈은 단지 하나의 이유에 의해서만 변경되어야 한다는 것을 강조하는 원리입니다.
위에서는 LoginProcess클래스는 3가지 이유에 의해 변경될 가능성이 있습니다.

3. LSP (The Liskov Substitution Principle) 리스코브의 치환 원리
(http://en.wikipedia.org/wiki/Liskov_Substitution_Principle :위키)

바바라 리스코브(Barbara Liskov) 1987년에 언급함.
Let q(x) be a property provable about objects x of type T.
Then q(y) should be true for objects y of type S where S is a subtype of T.

함수q(x)에서 클래스 T의 객체 x가 잘 작동한다면,
T클래스의 서브클래스인 S클래스의 객체 y도 q(x) 매소드에서 잘 작동되어야 한다는 원리이다.
따라서, 리스코브의 정의에 의하면, 만일 S가 T의 서브클래스라면,
프로그램상에서 T클래스의 객체는 S 클래스의 객체로 변환하더라도 어떠한 변경이 없어야 한다.

LSP는 당연하다고 할수 있으나, LSP원칙에 위배되는 코딩을 자주 목격하게 됩니다.

위배되는 경우는 주로 다음의 3가지 경우입니다.
첫째, 하위클래스의 동일한 메소드가 유저가 요구하는 메소드로서, 행동하지 않는 경우, 즉 무늬만 같고, 행동이 아예 다른 경우
둘째, 하위클래스의 메소드가 존재하지 않는 경우
셋째, 상위클래스와 하위클래스의 형타입 맞지 않는 경우로 요약할 수 있습니다.

나쁜예
<?
function q($x) {
$x->dispaly();
}

class introduce {
var $name = '홍길동' ;
function display() {
print '저의 이름은 ' . $this->name . '입니다' .
}
}

class DetailIntroduce extends introduce {
var $money = '2억' ;
function display() {
print '저의 재산은 ' . $this->money . '입니다' ;
}
}

$kildong = new introduce ;
q($kildong);

$DetailKildong = new DetailIntroduce ;
q($kildong);

?>

위의 나쁜 예를 보듯이
introduce 를 상속한 DetailIntroduce 클래스가
함수 q가 요구하는 행동을 하지 않고 엉뚱한 소리를 하고 있습니다.
위의 예는 error클래스를 만들때, 주로 나타날수 있습니다.
하위클래스의 행동또한 상위클래스의책임범위내에서 만들어 져야 합니다.

이런 경우말고도 문제가 되는 경우는
q함수(메소드)에서 instanceof같은 키워드로 객체의 형체크를 한다면,
q함수에서 하위클래스의 객체를 처리못해서, 에러가 발생할 수도 있습니다.

그리고 또한 final 같은 키워드 사용으로 인해, 하위클래스에서 메소드가 부존재로 인해,
q함수에서 에러가 발생할수도 있습니다.
(예외적으로 상속으로 구현하지 않았지만, 개념상 상속인경우에도, 메소드 부존재로 인한 에러가 발생할수도 있습니다.)

수정
<?
class DetailIntroduce extends introduce {
var $money = '2억' ;
function display() {
print '저의 이름은 ' . $this->name . '이며' ;
print '저의 현재재산은 ' . $this->money . '입니다' ;
}
}

?>

정리하자면 LSP원칙에서 강조하고 있는 것은
클래스는 생성목적에 맞게끔, 설계되어야 상위,하위클래스의 호환성이 높아 진다는 겁니다. 즉 LSP의 의미는 상속받는 하위 클래스는 상위 클래스의 책임을 넘지 말아야 하며,
하위클래스는 유저가 요구하는 행동되는 설계되어야 한다는 겁니다.
다형성을 오용해서 사용하면 안된다는 것과, 다형성이 작동안되게끔 하면 안된다는 것임.
거꾸로 생각해보면, 리스코브 전환식이 성립하는 경우에는 상속으로 치환할 가능성이 있는 경우입니다. is a 관계 여부 판단후 상속으로 치환해야 합니다.


4. DIP(Dependency inversion principle) 의존관계역전의 원리
http://en.wikipedia.org/wiki/Dependency_inversion_principle
Robert C. Martin가 언급함.

High level modules should not depend upon low level modules. Both should depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions
높은레벨의 모듈은 낮은레벨의 모듈을 의존하면 안된다. 서로 추상에 의존해야 한다.
추상은 구체적인것에 의존하면 안되고, 구체적인것은 추상에 의존해야 한다.

흔히, 상위개념은 하위개념을 포함하게 된다,
예를 들어, 고양이과 동물에는 호랑이, 사자,표범 등등 동물들이 포함된다.
즉 고양이라는 상위개념은 하위개념인, 호랑이, 사자,표범등을 다 포괄하는개념이다.

하지만, S/W설계에서는, 개념상 포함되어 있다고 하더라도, 상위개념이 하위개념 모두를 표현하거나,
참조하는 것은 나쁘고, 하위개념이, 상위개념을 참고하는 것이 좋다.
즉 이런것을 의존관계가 역전되어 있다고 표현합니다.

예) 고양이과에는 호랑이, 사자, 표범등이 있다. ( 안좋은 참조의 예)
호랑이는 고양이과이다. (좋은 참조의 예)

나쁜 의존관계
<?php
class tiger {}
class lion {}
class cheeta {}

class cat {
var $tiger ;
var $lion ;
var $cheeta ;
function cat() {
$this->tiger = new tiger ;
$this->lion = new lion ;
$this->cheeta = new cheeta ;
}
}
?>
상위개념인 고양이 클래스에서 하위개념인 타이거, 라이온, 치타를 참고하고 있습니다.
구현을 생략했지만, 틀림없이, 불필요한 중복코드가 많아 생깁니다.

객체와 객체의 참조관계에서 is a 관계이면 상속, has a 관계면 합성(위임)의 방법으로 설계해야 합니다. 지금은 is a 관계이기 때문에 상속으로 해결해야 합니다.

<?php
class cat { }
class tiger extends cat {}
class lion extends cat {}
class cheeta extends cat{}
?>

DIP원칙을 지키면
상위모듈개발할때, 하위모듈 개발을 미리 할 필요가 없다는 것입니다.
즉, TopDown 접근방식으로 설계 및 개발이 가능하다는 겁니다.

DIP원리를 설명하면서, 헐리우드 원리가 설명되기도 하는데, 관련은 있지만,
완벽하게 일치 하는 개념은 아닙니다.

헐리우드원리란 배우지망생이 많아서, 오디션 담당자가 전화응답이 너무 힘들어
거꾸로, 오디션 합격자에게만, 전화를 거는 현상을 이야기 합니다.
전화하지 마세요. 우리가 연락할께요..

헐리우드원리예를 온라인 채팅서버와 클라이언트의 관계에 적용시켜보면, 잘 알수가 있습니다.
채팅정보를 원하는 클라이언트는 채팅서버에게 채팅정보를 게속 요구합니다.
하지만, 서버가 클라이언트에게 전달한 정보가 없다면, 불필요한 서비스요청을 하게 되는 겁니다. http 같이, 비연결성 프로토콜경우 이러한 현상이 잘 나타나는데,
서버가 클라이언트와의 연결정보를 유지할수 없기때문에,
부득이하게, 클라이언트가 몇초단위로 요구하게 됩니다. 즉 불필요한 서비스요청이 게속 발생하게 됩니다. 그래서 이를 해결하기 위한 방법이 서비스제공자인 서버가, 서비스요청자인 클라이언트에게 서비스를 거꾸로 전달하는 겁니다. 이러한 원리를 헐리우드원리 또는 제어권의 역전 현상이라고 이야기 합니다.

이러한 제어권의 역전현상 모두를 상위개념과 하위개념으로 나눌수 없기때문에
DIP 원칙이다 하기에는 무리가 있지만 밀접한 관련이 있는것은 사실입니다.

참고) 대규모의 채팅서비스는 서버소켓방식 통신이 적합하고, 소규모의 채팅서비스는, http통한 서비스도 가능할것 같습니다.


5. ISP (The Interface Segregation Principle) 인터페이스 분리원칙

ISP원리는 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원리입니다.
즉 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스만을 사용해야 한다는겁니다.

예를 들어보겠습니다.
IWorker 인터페이스에는 work() 메소드와 eat() 메소드가 있습니다.
정규직은, 일하면서, 점식식사를 하지만,
파트타임 알바는 점식식사가 제공되지 않습니다.
이경우 알바는 eat()라는 메소드가 불필요함에도 불구하고,
구현을 해야합니다. 즉, 인터페이스가 분리가 안되어 나오는 문제입니다.
<?php
interface IWorker {
public function work();
public function eat();
}

//정규직
class standardWorker implements IWorker{
public function work() {//todo }
public function eat() {//todo}
}

//알바
class partTimeWorker implements IWorker{
public function work() {//todo }
public function eat() {//사용하지 않음}
}
?>

=>수정

<?php
interface IWorkable {
public function work();
}

interface IFeedable{
public function eat();
}

//정규직
class standardWorker implements IWorkable,IFeedable{
public function work() {//todo }
public function eat() {//todo}
}

//알바
class partTimeWorker implements IWorkable{
public function work() {//todo }
}
?>
즉 파트타임 노동자는 불필요한 메소드를 구현하지 않게 되었습니다.

ISP 인터페이스 분리원칙는 SRP 원칙과 관련이 있지만, SRP는 클래스의 단일책임을 강조하는 거라면
ISP는 인터페이스의 단일책임을 강조하는 겁니다.

댓글