Notice
Recent Posts
Recent Comments
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

SOLID (3) - 리스코프 치환 원칙, LSP 본문

Design Pattern/References

SOLID (3) - 리스코프 치환 원칙, LSP

defacto standard 2017. 10. 28. 19:03

리스코프 치환 원칙 (LSP, Liskov Substitution Principle)

일반화 관계에 대한 이야기며 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다.


LSP를 만족하면 프로그램에서 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체해도 프로그램의 의미는 변화되지 않는다.

이를 위해 부모 클래스와 자식 클래스 사이는 행위가 일관되어야 한다.


LSP를 이해하려면 일반화 관계를 다시 생각해야한다. 일반화 관계는 'is a kind of 관계' 라고도 한다.

예를들어 원숭이는 포유류이고, 원숭이와 포유류 사이에 '원숭이 is a kind of 포유류 관계'가 성립한다.

따라서 부모 클래스로 포유류, 자식 클래스로 원숭이를 설정하는 것에 대체로 반론의 여지가 없다.


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

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

 - 포유류(원숭이)는 체온이 일정한 정온 동물이며 털이나 두꺼운 피부로 덮여 있다.


이는 포유류의 여러 특징을 설명한 것이고, 원숭이에게도 해당되는지 알아보려면 포유류를 원숭이라는 단어로 대체하면 된다.

원숭이와 포유류는 행위(번식, 양육, 호흡, 체온조절, 피부 보호 방식 등)에 일관성이 있다고 말할 수 있다.


그러나 오리너구리는 해당되지 않는다. 오리너구리는 알을 낳아 번식하는 동물이다. 그러나 오리너구리는 포유류이다. 즉, 위 포유류에 대한 설명이 잘못된 것이다. 객체지향 관점에서 보면 오리너구리의 'is a kind of 관계' 설명은 LSP를 만족하지 않은 설명이다.

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


자식 클래스가 부모 클래스 인스턴스의 행위를 일관성 있게 실행하려면, 최소한 부모 클래스의 인스턴스가 실행하는 행위는 자식 클래스의 인스턴스들도 일관성 있게 실행할 수 있어야 한다. 이를 위해서는 부모 클래스의 행위를 더 명확하게 정의할 수 있는 수단이 필요하다.

여기서는 어떤 클래스의 행위를 일종의 방정식 형태로 기술해 자식 클래스의 인스턴스가 이 방정식을 만족하는지 점검한다.

만약 만족한다면 자식 클래스가 부모 클래스의 행위를 일관성 있게 실행한다고 말할 수 있다.


public class Bag {
private int price;

public void setPrice(int price) {
this.price = price;
}

public int getPrice() {
return price;
}
}

Bag 클래스는 가격을 설정(setPrice())하고 가격을 조회(getPrice())하는 기능이 있다.

따라서 Bag 클래스의 행위는 다음과 같이 표현가능 가격은 설정된 가격 그대로 조회된다.

이를 좀 더 형식적으로 작성하면 다음과 같다.

// 모든 Bag 객체는 b와 모든 정수 값 p에 대해
[b.setPrice(p)].getPrice() == p;

여기에서 '[객체.메서드(인자 리스트)]'는 메서드가 실행된 후의 b 객체를 나타낸다.

이런 경우 Bag 클래스의 행위를 손상하지 않고 일관성 있게 실행하는 클래스를 가장 직접적이고 직관적으로 만드는 방법은, 슈퍼 클래스에서 상속받은 메서드들이 서브 클래스에 오버라이드(재정의)되지 않도록 하면 된다.

 

다음은 Bag 클래스를 상속받아 가방 가격을 할인받을 수 있게 하는 DiscountedBag 클래스를 구현한 것이다.

public class DiscountedBag extends Bag {
private double discountedRate = 0;

public void setDiscounted(double discountedRate) {
this.discountedRate = discountedRate;
}

public int getPrice() {
super.setPrice(price - (int)(discountedRate * price));
}
}


DiscountedBag 클래스는 할인율을 설정해서 할인된 가격을 계산하는 기능이 추가되었다. 기존의 Bag 클래스에 있던 가격을 설정하고 조회하는 기능은 변경 없이 그대로 상속받았다.


다음 표에서 왼쪽에 있는 코드는 Bag 객체로 작성되었고, 오른쪽에 있는 코드는 DiscountedBag 객체를 사용해 작성됨.

Bag

DiscountedBag

Bag b1 = new Bag();

DiscountedBag b3 = new DiscountedBag();

Bag b2 = new Bag();

DiscountedBag b4 = new DiscountedBag();

b1.setPrice(50000);

b3.setPrice(50000);

b1.getPrice();

b3.getPrice();

b2.setPrice(b1.getPrice());

b4.setPrice(b3.getPrice());

b2.getPrice();

b4.getPrice();

현재 Bag 클래스의 setPrice와 getPrice 메서드가 DiscountedBag 클래스에서 재정의되지 않았으므로 왼쪽에 있는 코드와 오른쪽에 있는 코드의 실행 결과가 동일하다. 이는 현재의 DiscountedBag 클래스와 Bag 클래스의 상속 관계가 LSP를 위반하지 않는다는 것을 뜻한다.


그러나 다음과 같이 setPrice메서드를 오버라이드하면 위 표에서 2개의 코드가 동일한 결과를 가져오지 않는다. 수정된 DiscountedBag 클래스가 일반적으로 방정식 [b.setPrice(p)].getPrice() == p를 만족하지 못함을 알 수 있다. 이유는 할인율이 0이 아닐 때는 setPrice 메서드를 실행한 후 DiscountedBag 객체의 price 속성 값이 p에서 discountedRate 객체의 price 속성 값이 p에서 discountedRate * price를 차감한 결과가 되며 이는 p와 같지 않다.

p - (int)(discountedRate * price) != p;


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

public class DiscountedBag extends Bag {
private double discountedRate;

public void setDiscounted(double discountedRate) {
this.discountedRate = discountedRate;
}

public int setPrice() {
super.setPrice(price - (int)(discountedRate * price));
}
}


'피터 코드의 상속 규칙'에서 "서브 클래스가 슈퍼 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행한다"라는 규칙을 살펴보았다. 이는 슈퍼 클래스의 메서드를 오버라이드하지 않는 것과 같은 의미다. 즉, 피터 코드의 상속 규칙을 지키는 것은 LSP를 만족시키는 하나의 방법에 해당한다.


LSP를 만족시키는 간단한 방법은 재정의하지 않는 것이다.

 

Comments