본문 바로가기

IT/오브젝트

ch.13 서브클래싱과 서브타이핑

04. 리스코프 치환 원칙

한마디로 정리하면 서브타입은 그것의 기반 타입에 대해 대체 가능해야 한다.

즉 리스코프 치환 원칙에 따르면 자식 클래스가 부모클래스와 행동 호환성을 유지함으로써 부모 클래스를 대체할 수 있도록 구현된 상속 관계만을 서브타이핑이라고 불러야한다.

 

직사각형은 사각형이다.

하지만 직사각형은 정사각형이 아닐 수 있다. 사실 직사각형과 정사각형의 상속 관계는 리스코프 치환 원칙을 위반하는 고전적인 사례중 하나다.

 

public class Rectangle {

	private int x, y, width, height;

	public Rectangle(int x, int y, int width, int height) {
		this.x = x;
		this.y = y;
		this.width = width; this.height = height;
     }

	public int getWidth() {
		return width; 
	}

	public void setWidth(int width) {
		this.width = width; 
	}

	public int getHeight() {
		return height; 
	}
    
    public void setHeight(int height) {
		this.height= height;
	}

	public int getArea() {
		return width * height; 
    }
}

 

정사각형은 너비와 높이가 동일해야 한다. 따라서 Square 클래스는 width와 height를 동일하게 설정해야 한다.

 

public class Square extends Rectangle {

	public Square(int x, int y, int size) {
		super(x, y, size, size); 
	}

	@Override
	public void setWidth(int width) {
		super.setWidth(width); 
        super.setHeight(width); 
     }

	@Override 
    public void setHeight(int height) { 
    	super.setWidth(height); 
        super.setHeight(height);
     } 
}

 

Square는 Rectangle의 자식 클래스이기 때문에 Rectangle이 사용되는 모든 곳에서 Rectangle로 업캐스팅 될 수 있다. 문제는 여기서 발생한다. Rectangle과 협력하는 클라이언트는 사각형의 너비와 높이가 다르다고 가정한다.

  public get area() {
    return this.width * this.height;
  }
Rectangle rec = new Rectangle();
rec.width = 3;
rec.height = 4;

rec.area == 12 // true

Rectangle rec2 = new Square();
rec2.width = 3;
rec2.height = 4;

rec2.area === 12; // false

해당 코드를 보면 어떤부분이 위반됐는지 이해가 된다. 

직사각형을 상속받는 정사각형은 정확히 리스코프 치환원칙을 위배하고 있다. 


클라이언트와 대체 가능성

Square가 Rectangle을 대체할 수 없는 이유는 클라이언트의 관점에서 Sqaure와 Rectangle이 다르기 때문이다. 

Rectangle은 너비와 높이가 다를 수 있나든 가정하에 코드를 개발하지만

Square는 너비와 높이가 같다라는 가정하에 코드를 개발한다.

 

리스코프 치환 원칙은 자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스에 대한 클라이언트의 가정을 준수해야 한다는 것을 강조한다.

 

즉 상속관계는 클라이언트의 관점에서 자식 클래스가 부모클래스를 대체할 수 있을때만 올바르다.


is-a 관계 다시 살펴보기

is-a는 클라이언트 관점에서 is-a일 때만 참이다.

is-a 관계는 객체지향에서 중요한 것은 객체의 속성이 아니라 객체의 행동이라는 점을 강조한다. 

 

결론적으로 상속이 서브타이핑을 위해 사용될 경우에만 is-a 관계다. 서브클래싱을 구현하기 위해 상속을 사용했다면 is-a 관계라고 할수 없다.


리스코프 치환 원칙은 유연한 설계의 기반이다.

클라이언트가 어떤 자식 클래스와도 안정적으로 협력할 수 있는 상속 구조를 구현할 수 있는 가이드라인을 제공한다. 새로운 자식 클래스를 추가하더라도 클라이언트의 입장에서 동일하게 행동하기만 한다면 클라이언트를 수정하지 않고도 상속 계층을 확장할 수 있다. 

 

리스코프 치환 원칙을 따르는 설계는 유연할뿐만 아니라 확장성이 높다. 8장에서 중복할인 정책을 구현해도 기존의 DiscountPolicy 상속 계층에 새로운 자식 클래스인 OverlappedDiscountPolic를 추가하더라도 클라이언트를 수정할 필요가 없었것을 기억해야한다.

public class OverlappedDiscountPolicy extends Discount Policy {

	private List<Discount Policy> discountPolicies = new ArrayListo;

	public OverlappedDiscountPolicy(DiscountPolicy ... discountPolicies) {
		this.discountPolicies = Arrays.aslist(discount Policies); 
     }
	
	@Override 
    protected Money getDiscount Amount (Screening screening) { 
    	Money result =Money ZERO;
		for(Discount Policy each : discount Policies) {
			result = result.plus(each.calculateDiscount Amount (screening)); 
		}
		return result;
	}
}

 

의존성 역전 원칙 : 구체 클래스인 Moive와 OverlappedDiscountPolicy 모두 추상 클래스인 DiscountPolicy에 의존한다. 상위 수준의 모듈인 Movie와 하위수준 모듈인 OverlappedDiscountPolicy는 모두 추상 클래스인 DisCountPolicy의 의존한다. 따라서 DIP를 만족한다.

 

리스코프 치환 원칙 : DiscountPolicy와 협력하는 Movie 관점에서 DicountPolicy대신 OverlappedDiscountPolicy와 렵력하더라도 아무 문제가 없다 따라서 이 설계는 LSP를 만족한다.

 

개방-폐쇄원칙 : 중복 할인 정책이라는 새로운 기능을 추가하기 위해 DiscountPolicy의 자식 클래스인 OverlappedDiscountPolicy를 추가하더라도 Movie에는 영향을 끼치지 않는다. 다시 말해서 기능 확장을 하면서 기존 코드를 수정할 필요가 없다.

 

리스코프 치환 원칙은 개방-폐쇄 원칙을 지원한다. 자식 클래스가 클라이언트 관점에서 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 추가하더라도 코드를 수정 할 필요가 없어지기 떄문이다.

반대로 둘중하나라도 위반하면 다른 한쪽을 잠재적으로 위반한다.

 

DIS, LSP, OCP가 조합된 유연한 설계


타입 계층과 리스코프 치환 원칙

클래스의 상속은 타입 계층을 구현할 수 있는 다양한 방법 중 하나일 뿐이라는 것이다. C#의 인터페이스나 스칼라의 트레이트 동적 타입 언어의 덕 타이핑 등의 기법을 사용한다면 클래스 사이의 상속을 사용하지 않고 서브타이핑을 구현할 수 있다.

물론 위의 기법들도 리스코프 원칙을 준수해야만 서브타이핑 관계라고 할수 있다.

'IT > 오브젝트' 카테고리의 다른 글

ch.15 프레임워크와 코드 재사용  (0) 2021.08.07
ch 12. 다형성  (0) 2021.07.24
ch11. 합성과 유연한 설계  (0) 2021.07.15
ch.10 상속과 코드 재사용  (0) 2021.07.10
ch.09 유연한 설계  (0) 2021.07.03