디자인 패턴 - solid 원칙 정리 1

디자인 패턴 - solid 원칙 정리 1

목차

1.단일 책임원칙 - 객체는 하나의 책임만 갖는다.
2.개방 폐쇄 원칙 - 새로운 기능을 추가할때 기존 소스에 영향을 주지 않는다.
3.리스코프 치환 원칙 - 자식 클래스는 최소한 자신의 부모 클래스의 기능을 수행할 수 있어야 한다.(일반화와 관련)
(리스코프 치환 원칙은 부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다는 의미다.)

 

1.단일 책임 원칙

객체는 하나의 책임만 가져야 한다.

책임 : 해야 하는 것, 할 수 있는 것

 

학생 클래스가 수강과목을 추가하거나 조회하고, 데이터 베이스에 객체 정보를 저장하거나, 데이터베이스에서 객체 정보를 읽고, 성적표와 출석부에 출력한다고 가정해보자.

 

public class Student{



public void getCourse(){ … }

public void addCourse(Course c){ … }

public void save(){ … }

public void load(){ … }

public void printOnReportCard(){ … }

public void printOnAttendanceBook(){ … }

}

 

위의 학생 class는 너무 많은 책임이 있다.

 

Student 클래스가 가장 잘할 수 있는 것은 수강 과목을 추가하고 조회하는 일이다.

 

설계원칙을 학습하는 이유?

-> 예측하지 못한 변경사항이 발생하더라도 유연하고 확장성이 있도록 시스템 구조를 설계하기 위해서.

 

좋은 설계란?

->시스템에 새로운 요구사항이나 변경이 있을 때 가능한 한 영양받는 부분을 줄일 수 있는 설계.

 

 

예를 들어 어떤 클래스가 잘 설계 되었는지를 판단하려면 언제 변경되어야 하는지를 물어보는 것이 좋다.

 

Student 클래스는 언제 변경되어야 하나? Student 클래스의 변경 이유를 찾아보자

 

1.데이터 베이스의 스키마가 변경되면 Student 클래스도 변경되어야 하나?

 

2.학생이 지도 교수 찾는 기능이 추가되면 Student 클래스는 영향을 받나?

 

3.학생 정보를 성적표와 출석부 이외의 형식으로 출력한다면 어떻게 하나?

 

위 사항 모두 학생 클래스를 변경해야 하는 이유가 된다.

 

또한 책임을 많이 가질 수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아진다.

 

예) 현재 수강 과목을 조회하는 코드(getCourse 메서드)와 데이터 베이스에서 학생 정보를 가져오는 코드 (load 메서드) 가 연결되어 있을 수 있다.

 

책임의 분리

 

 

Student 클래스의 경우 변경 사유가 될 수 있는 것은 학생의 [1]고유 정보, [2]데이터베이스 스키마, [3]출력 형식의 변화 등 3가지다.

 

따라서 Student 클래스는 학생 고유의 역할을 수행하게끔 변경하고 학생 클래스의 인스턴스를 데이터베이스에 저장하거나 읽어들이는 역할을 담당하는 학생 DAO 클래스 등으로 분리하는 편이 좋다.

 

아래와 같이 개선된 설계에서는 데이터베이스의 스키마가 변화되면 학생 DAO 클래스나 이를 사용하는 클래스만 영향을 받는다.

 

2.개방폐쇄 원칙

정의 : 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다는 뜻이다.

 

성적표나 출석부에 학생의 성적이나 출석 기록을 출력하는 기능을 SomeClient에서 이용할 때를 가정.

 

 

만약 성적표, 출석부 이외에 도서관 대여 명부에 학생의 대여 기록을 출력하는 기능이 추가된다면?

 

-> 도서관 대여 명부 클래스를 만들어서 SomeClient 클래스가 사용하게 하면 되나?

 

-> NO. 위 방법은 OCP를 위반한다.

 

-> 새로운 기능(도서관 대여 명부에 학생의 대여 기록 출력)을 추가하려고 SomeClient 클래스를 수정해야 하기 때문이다.

 

 

[!] OCP를 위반하지 않는 설계를 할때 중요한점

 

-> 무엇이 변하는 것인지?

 

-> 무엇이 변하지 않는 것인지?

 

위의 것 구분하기.

 

변해야 하는것은 쉽게 변하게 처리하고, 변하지 않아야 할 것은 변하는 것에 영향을 받지 않게 해야 한다.

 

위의 경우에서 변해야 하는것 : 학생의 대여 기록을 출력하는 매체(도서관 대여 명부) 

 

 

따라서 새로운 출력 매체를 표현하는 클래스를 추가해도 기존의 클래스 (SomeClient 클래스)가 영향을 받지 않게 하려면 SomeClient 클래스가 개별적인 클래스를 처리하지않게 하고, 위의 그림과 같이 인터페이스에서 구체적인 출력 매체를 캡슐화 하도록 해야 한다.

(전략패턴 - 스트래터지 패턴)

 

keyPoint : 클래스 = 변화의 단위

 

OCP를 보는 또 하나의 관점은 클래스를 변경하지 않고도 대상 클래스의 환경을 변경할 수 있는 설계가 되어야 한다는 것.(단위테스트 할때 중요)

 

1.테스트 대상 기능이 사용하는 실제 외부의 서버스를 흉내내는 가짜객체를 만들어서 효율성을 높일 필요가 있을때.

 

2.실제 서비스에서 사용할 객체를 그대로 테스트 할때 위험이 따르므로(예 db  접근 후 삭제, 추가등) 테스트를 위해 실제 데이터베이스 기능을 대체하는 가짜 객체를 만들 필요가 있을 때.

 

 

3.리스코프 치환 원칙

MIT 컴퓨터 공학과 리스코프 교수가 1987년에 제안한 원칙이다.

 

이 원칙은 일반화 관계에 대한 이야기이다.

 

즉, 자식 클래스는 최소한 자신의 부모 클래스의 기능을 수행할 수 있어야 한다는 뜻.

 

리스코프 치환원칙을 만족하면 프로그램에서 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체해도 프로그램의 의미는 변화되지 않는다.

 

keyPoint - 리스코프 치환 원칙은 부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다는 의미다.

 

일반화 관계

일반화 관계는 is a kind of 관계 이다.

 

객체지향 관점에서 is a kind of 관계는 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스를 별다른 변경 없이 그대로 사용할 수 있을 때 성립한다.

 

아래는 포유류를 설명하는 글이다. 

 

포유류는 알을 낳지 않고 새끼를 낳아 번식한다.(0)

포유류는 젖을 먹여서 새끼를 키우고 폐를 통해 호흡한다.(0)

 

이제 포유류를 원숭이로 바꿔보자

 

원숭이는 알을 낳지 않고 새끼를 낳아 번식한다.(0)

원숭이는 젖을 먹여서 새끼를 키우고 폐를 통해 호흡한다.(0)

 

전혀 문제가 없다. 즉 일관성이 있다. 

 

그럼 이번에는 오리 너구리를 넣어보자.

 

오리 너구리는 알을 낳지 않고 새끼를 낳아 번식한다.(x)

오리 너구리는 젖을 먹여서 새끼를 키우고 폐를 통해 호흡한다.

 

위의 설명은 오리너구리에는 해당되지 않는다. 오리너구리는 알을 낳아서 번식하는 동물이기 때문이다.

우리는 그럼에도 오리너구리가 포유류라는 사실을 알고 있다. 즉, 위의 포유류에 대한 설명이 잘못된것이다.!!

 

객체지향 관점에서 보면 오리너구리의 is a kind of 관계 설명은 LSP를 만족하지 않은 설명이다.

 

LSP를 만족하려면 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대신할 수 있어야 한다.

 

자식 클래스가 부모 클래스 인스턴스의 행위를 일관성 있게 실행하려면 어떻게 해야 하나?

 

 

 

위의 Bag 클래스는 가격을 설정하고 가격을 조회하는 기능이 있다.

즉 아래와 같이 이야기 할 수 있다. 

 

가격은 설정된 가격 그대로 조회된다!

 

위의 경우 Bag 클래스의 행위를 손상하지 않고 일관성 있게 실행하는 클래스를 만들려면 어떻게 해야 하는가?

 

-> 슈퍼 클래스에서 상속받은 메서드들이 서브클래스에서 오버라이드, 즉 재정의 되지 않도록 한다.

 

아래와 같이 Bag 클래스를 상속받아 가방 가격을 할인 받을 수 있게 하는 DiscountedBag 클래스를 구현해 보자.

 

 

DiscountedBag 클래스는 할인율을 설정해서 할인된 가격을 계산하는 기능이 추가되었다.

기존의 Bag 클래스에서 있던 가격을 설정하고 조회하는 기능은 변경 없이 그대로 상속받았음을 알 수 있다.

 

 

현재 Bag 클래스의 setPrice 와 getPrice 메서드가 DiscountedBag 클래스에서 재정의 되지 않았으므로 왼쪽에 있는 코드와 오른쪽에 있는 코드의 실행 결과가 동일하다.

 

이는 현재의 DiscountedBag 클래스와 Bag 클래스의 상속 관계가 LSP를 위반하지 않는다는것을 뜻한다.

 

만약 setPrice 메서드를 오버라이드 하면?

 

할인율이 0이 아닐 때는 setPrice 메서드를 실행한 후 DiscountedBag 객체의 price 속성 값이 p에서 discountedRate * price를 차감한 결과가 되기 때문이다.

 

즉 아래 코드 처럼 Bag 클래스의 setPrice를 재정의한 DiscountedBag 클래스의 구현은 Bag 클래스의 행위와 일관되지 않으므로 LSP를 만족하지 않는다.

 

 

Key point : 리스코프 치환 원칙을 만족시키는 간단한 방법은 재정의 하지 않는 것이다.

 

 

참고 : JAVA 객체지향 디자인 패턴