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

uglyduck.dev·2020년 9월 27일
2

개념 모아 🗂

목록 보기
29/40

" A Type hierarchy is composed of subtypes and supertypes. The intuitive idea of a subtype is one whose objects provide all the behavior of another type(the supertype) plus something extra. 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."  -> 1987년 MIT 컴퓨터 공학과 리스코프 교수 - LSP의 일반화 관계를 설명한 원문 中

  • 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 함
  • LSP를 만족하면 프로그램에서 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체해도 프로그램의 의미는 변하지 않음
  • 부모 클래스와 자식 클래스 사이는 행위가 일관되어야 함

일반화 관계에서의 LSP

일반화 관계 "is a kind of 관계"

ex) 원숭이는 포유류이다.

  • 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스를 그대로 사용할 수 있을 때 성립

"is a kind of 관계" 검증(포유류)

  • 포유류는 알을 낳지 않고 새끼를 낳아 번식한다.
  • 포유류는 젖을 먹여서 새끼를 키우고 폐를 통해 호흡한다.
  • 포유류는 체온이 일정한 정온 동물이며 털이나 두꺼운 피부로 덮여 있다.

"is a kind of 관계" 검증(원숭이)

  • 원숭이는 알을 낳지 않고 새끼를 낳아 번식한다.
  • 원숭이는 젖을 먹여서 새끼를 키우고 폐를 통해 호흡한다.
  • 원숭이는 체온이 일정한 정온 동물이며 털이나 두꺼운 피부로 덮여 있다.
    -> 원숭이와 포유류는 행위(번식 방식, 양육 방식, 호흡 방식, 체온 조절 방식, 피부 보호 방식 등)에 일관성이 있음

"is a kind of 관계" 검증(오리너구리)**

  • 오리너구리는 알을 낳지 않고 새끼를 낳아 번식한다.
  • 오리너구리는 젖을 먹여서 새끼를 키우고 폐를 통해 호흡한다.
  • 오리너구리는 체온이 일정한 정온 동물이며 털이나 두꺼운 피부로 덮여 있다.
    -> 오리너구리는 알을 낳아 번식하는 동물임
    -> LSP를 만족하려면 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대신할 수 있어야 함

자식 클래스가 부모클래스의 행위를 일관성 있게 실행하려면?

  • 최소한 부모 클래스의 인스턴스가 실행하는 행위는 자식 클래스의 인스턴스들도 일관성이 있게 실행해야 함
    -> 부모 클래스의 행위를 더 명확하게 정의할 수 있는 수단 필요
public class Bag {
    private int price;
    
    public void setPrice(int price){
        this.price = price;
    }
    
    public int getPrice() {
        return price;
    }
}
  • setPrice 메서드 : 가격을 설정
  • getPrice 메서드 : 가격을 조회
    • Bag 클래스의 행위 : 가격은 설정된 가격 그대로 조회됨
// 모든 Bag 객체 b와 모든 정수 값 p에 대해
[be.setPrice(p)].getPrice() == p;
  • '[객체.메서드(인자리스트)]'는 메서드가 실행된 후의 b 객체
  • 슈퍼 클래스에서 상속받은 메서드들이 서브 클래스에 오버라이드, 즉 재정의되지 않도록 해야 함
public class DiscountedBag extends Bag{
    private double discountedRage = 0;
    
    public void setDiscounted(double discountedRate) {
        this.discountedRage = discountedRate;
    }
    
    public void applyDiscount(int price) {
        super.setPrice(price - (int)(discountedRage * price));
    }
}
  • DiscountedBag 클래스는 할인율을 설정해서 할인된 가격을 계산하는 기능이 추가됨
  • 기존의 Bag 클래스에 있던 가격을 설정하고 조회하는 기능은 변경 없이 그대로 상속 받음
BagDiscountedBag
Bag b1 = new Bag();DiscountedBag b3 = new DiscountedBag();
Bag b2 = new Bag();DiscountedBag b4 = new DiscountedBag();
b1.setPrice(50000);b3.setPrice(50000);
System.out.println(b1.getPrice());System.out.println(b3.getPrice());
b2.setPrice(b1.getPrice());b4.setPrice(b1.getPrice());
System.out.println(b2.getPrice());System.out.println(b4.getPrice());
  • Bag 클래스의 메서드가 DiscountedBag클래스에서 재정의되지 않았으므로 왼쪽에 있는 코드와 오른쪽에 있는 코드의 실행 결과는 동일함
  • DiscountedBag 클래스와 Bag 클래스의 상속 관계가 LSP를 위반하지 않음
public class DiscountedBag extends Bag{
    private double discountedRage;
    
    public void setDiscounted(double discountedRate){
        this.discountedRage = discountedRage;
    }
    
    public void setPrice(int price){
        super.setPrice(price - (int)(discountedRate * price));
    }
}

Q. setPrice 메서드를 오버라이드 할 경우 위 두 개의 코드 2개가 동일한 결과를 가져오는가?**

A. 그렇지 않다. 

  • 수정된 DiscountedBag 클래스가 일반적으로 방정식 [b.setPrice(p)].getPrice() == p를 만족하지 않음
    -> 할인율이 0이 아닐 경우 setPrice 메서드를 실행한 후 DiscountedBag 객체의 price 속성 값이 p에서 discountedRage * price을 차감한 결과가 되며 이는 p와 같지 않음
p - (int)(discountedRate * price) != p;
  • Bag 클래스의 setPrice를 재정의한 DiscountedBag 클래스의 구현은 Bag클래스의 행위와 일관되지 않으므로 LSP를 만족하지 않음

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

Reference

  • 정인상, 채흥석, 『JAVA 객체 지향 디자인 패턴』, 한빛미디어(2019.3.8), 116~120p
profile
시행착오, 문제해결 그 어디 즈음에.

0개의 댓글