13장 서브클래싱과 서브타이핑

swucs·2022년 2월 14일
0

오브젝트

목록 보기
12/13

출처 : 오브젝트 (조영호 저)

상속의 두 가지 용도

  • 타입 계층 안에서 부모 클래스는 일반적이 개념을 구현하고 자식 클래스는 특수한 개념을 구현한다.

  • 코드의 재사용이다. 그러나 재사용을 위해 상속을 사용할 경우 부모와 자식 클래스가 강하게 결합되는 문제가 있다.

동일한 메시지에 대해 서로 다르게 행동할 수 있는 다형적인 객체를 구현하기 위해서는 객체의 행동을 기반으로 타입 계층을 구성해야 한다.

객체지향 프로그래밍과 타입 계층

객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다. 따라서 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다.
객체의 타입을 결정하는 것은 퍼블릭 인터페이스다.

슈퍼타입이란 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의한것이다.

서브타입이란 슈퍼타입이 정의한 퍼블릭 인터페이스를 특수화시켜 상대적으로 구체적이고 좁은 의미로 정의한 것이다.

서브타입의 인스턴스 집합은 슈퍼타입의 인스턴스 집합의 부분 집합이기 때문에 더 특수한 퍼블릭 인터페이스를 가지는 객체들은 동시에 더 일반적인 퍼블릭 인터페이스를 가지는 객체들의 집합에 포함된다.
즉, 서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.

is-a 관계

마틴 오더스키의 조언에 따르면 두 클래스가 어휘적으로 is-a 관계를 모델링할 경우에만 상속을 사용해야 한다.

어떤 타입 S가 다른 타입 T의 일종이라면 당연히 "타입 S는 타입 T다"라고 말할 수 있어야 한다.

  • 펭귄은 새다.
  • 새는 날 수 있다.

하지만 is-a 관계가 생각처럼 직관적이고 명쾌하지 않다. 어떤 애플리케이션에서 새에게 날 수 있다는 행동을 기대하지 않고 단지 울음 소리를 낼 수 있다는 행동만 기대한다면 새와 펭귄을 타입 계층으로 묶어도 무방하다.

따라서 슈퍼타입과 서브타입 관계에서는 is-a 보다 행동 호환성이 더 중요하다.

행동 호환성

두 타입 사이에 행동이 호환될 경우에만 타입 계층으로 묶어야 한다.
여기서 행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이다.

public void flyBird(Bird bird) {
	//인자로 전달된 모든 Bird는 날 수 있어야 한다.
    bird.fly();
}

펭귄이 새의 서브타입이 아닌 이유는 클라이언트 입장에서 모든 새가 날 수 있다고 가정하기 때문이다. 따라서 이 둘을 상속 관계로 연결한 위 설계는 수정돼야 한다.

클라이언트 기대에 따라 계층 분리하기

날 수 있는 새와 날 수 없는 새를 명확하게 구분할 수 있게 상속 계층을 분리하면 서로 다른 요구사항을 가진 클라이언트를 만족시킬 수 있을 것이다.

public class Bird {
	...
}

public class FlyingBird extends Bird {
	public void fly();
}

public class Penguin extends Bird {
	...
}

이 문제를 해결하는 다른 방법은 클라이언트에 따라 인터페이스를 분리하는 것이다. 인터페이스를 클라이언트의 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙을 인터페이스 분리 원칙(Interface Segregation Principle, ISP)이라고 한다.

요점은 자연어에 현혹되지 말고 요구사항 속에서 클라이언트가 기대하는 행동에 집중하라는 것이다. 두 클래스 사이에 행동이 호환되지 않는다면 올바른 타입 계층이 아니기 때문에 상속을 사용해서는 안된다.

서브클래싱과 서브타이핑

  • 서브클래싱 : 다른 클래스의 코드를 재사용할 목적으로 상속을 사용
  • 서브타이핑 : 타입 계층을 구성하기 위해 상속을 하용하는 경우

서브타이핑 관계가 유지되기 위해서는 서브타입이 슈퍼타입이 하는 모든 행동을 동일하게 할 수 있어야 한다. 즉 어떤 타입이 서브타입이 되기 위해서는 행동 호환성을 만족시켜야 한다.

행동 오환성을 만족하는 상속 관계는 부모 클래스를 새로운 자식 클래스로 대체하더라도 시스템이 문제없이 동작할 것이라는 것을 보장해야 한다. 다시 말해서 부모 클래스에 대한 자식 클래스의 대체 가능성을 포함한다.

리스코프 치환 원칙

"서브타입은 그것의 기반 타입에 대해 대체 가능해야 한다."
"차이점을 인식하지 못한 채 파생 클래스의 인터페이스를 통해 서브클래스를 사용할 수 있어야 한다."

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

is-a 관계의 애매모함을 설명하기 위한 펭귄과 새는 리크코프 치환 원칙을 위반한다.

대부분 "정사각형은 사각형이다"라는 이야기를 당연하게 생각한다.

public class Rectangle {
	...
}

public class Square extends Rectangle {
	...
}

Square는 Rectangle의 자식 클래스이기 때문에 Rectangle이 사용되는 모든 곳에서 Rectangle로 업캐스팅 된다.

public void resize(Rectangle rectangle, int width, int height) {
	rectangle.setWidth(width);
    rectangle.setHeight(height);
    assert rectangle.getWidth() == width && rectangle.getHeight() == height;
}

위 코드는 Square의 너비와 높이는 항상 더 나중에 설정된 hiehgt로 설정된다. 따라서 다음과 같이 작성하는 경우 메서드 실행이 실패하고 만다.

Squere sqaure = new Square(10, 10, 10);
resize(square, 50, 100);

Square는 Rectangle의 구현을 재사용하고 있을 뿐 두 클래스는 리스코드 치환 원칙을 위반하기 때문에 서브타이핑 관계가 아니라 서브클래싱 관계다.

클라이언트와 대체 가능성

Rectangle을 사용하는 클라이언트는 Rectangle의 너비와 높이가 다를 수 있다는 가정하에 코드를 개발한다. 반면 Square는 너비와 높이가 항상 같다.

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

행동 호환성과 리스코프 치환 원칙에서 한 가지만 기억해야 한다면 이것을 기억하라. 대체 가능성을 결정하는 것은 클라이언트다.

is-a 관계 다시 살펴보기

is-a 관계로 표현된 문장을 볼 때 마다 문장 앞에 "클라이언트 입장에서"라는 말이 빠져 있다고 생각하라.

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

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

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

자식 클래스가 클라이언트의 관점에서 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 추가하더라도 코드를 수정할 필요가 없어진다.

따라서 리스코프 치환 원칙은 개방-폐쇄 원칙을 만족하는 설계를 위한 전제 조건이다. 일반적으로 리스코프 치환 원칙 위반은 잠재적인 개방-폐쇄 원칙 위반이다.

계약에 의한 설계와 서브타이핑

클라이언트와 서버 사이의 협력을 의무와 이익으로 구성된 계약의 관점에서 표현하는 것을 계약에 의한 설계(Design By Contract, DBC)라고 한다.

  • 사전조건 : 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 함
  • 사후조건 : 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 함
  • 클래스 불변식 : 메서드 실행 전과 실행 후에 인스턴스가 만족시켜함

계약의 관점에서 상속이 초래하는 가장 큰 문제는 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 수 있다는 것이다.

어떤 타입이 슈퍼타입에서 정의한 사전조건보다 더 약한 사전조건을 정의하고 있다면 그 타입은 서브타입이 될 수 있지만 더 강한 사전조건을 정의한다면 서브타입이 될 수 없다. 어떤 타입이 슈퍼타입에서 정의한 사후조건보다 더 강한 사후조건을 정의하더라도 그 타입은 여전히 서브타입이지만 더 약한 사후조건을 정의한다면 서브타입의 조건이 깨지고 만다.

계약에 의한 설계는 클라이언트 관점에서의 대체 가능성을 계약으로 설명할 수 있다는 사실을 잘 보여준다. 따라서 서브타이핑을 위해 상속을 사용하고 있다면 부모 클래스가 클라이언트와 맺고 있는 계약에 관해 깊이 있게 고민하기 바란다.

profile
백엔드 개발자

0개의 댓글

관련 채용 정보