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

상속의 두 가지 용도

  • 상속의 첫 번째 용도는 타입 계층을 구현하는 것이다.

    • 타입 계층의 관점에서 부모 클래스는 자식 클래스의 일반화이고 자식 클래는 부모 클래스의 특수화다.

  • 상속의 두 번째 용도는 코드 재사용이다.

    • 재사용 가능 but 부모 클래스와 자식 클래스가 강하게 결합

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

상속의 가치는 이러한 타입 계층을 구현할 수 있는 쉽고 편안한 방법을 제공한다는 것이다.

상속을 사용하는 일차적인 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것이다. (즉 상속의 두 가지 용도에서 첫 번째 용도의 중요성을 강조)

이번 장에서는 올바른 타입 계층구성하는 원칙을 살펴본다.

  • 상속서브타입 다형성동적 메서드 탐색에 밀접하게 연관돼 있다.

    → 용어가 너무 어렵게 설명되어 있음 ..

    → 결국 상속을 통해 서브타입 다형성 구현을 하고, 동적 메서드 탐색을 할 것이라고 예상

    → 이번 장에서 그 방법을 설명할 것 같음

1. 타입

→ 타입의 개념부터 설명하였다.

개념 관점의 타입

  • 우리가 인식하는 개체들에 적용하는 개념이나 아이디어를 가리켜 타입이라고 부른다.

  • 어떤 대상이 타입으로 분류될 때, 그 대상을 타입의 **인스턴스**라고 부른다.

    타입으로 분류된 대상을 타입의 인스턴스라고 한다.

  • 일반적으로 타입의 인스턴스**객체**라고 부른다.

    → 그리고 그 인스턴스를 객체라고 한다.

→ 다소 좀 추상적이라고 느꼈다.

타입의 구성 요소

  • 심볼(symbol)이란 타입에 이름을 붙인 것이다.

  • 내연(intenstion)이란 타입의 정의로서, 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동을 가리킨다.

  • 외연(extension)이란 타입에 속하는 객체들의 집합이다.

프로그래밍 언어 관점의 타입

프로그래밍 언어 관점에서 타입은 연속적인 비트에 의미와 제약을 부여하기 위해 사용된다.

객체지향 패러다임 관점의 타입

두 가지 관점에서의 타입 정의

  • 개념 관점에서 타입이란 공통의 특징을 공유하는 대상들의 분류

  • 프로그래밍 언어 관점에서 타입이란 동일한 오퍼레이션을 적용할 수 있는 인스턴스들의 집합

    • 객체지향 프로그래밍에서 오퍼레이션은 객체가 수신할 수 있는 메시지

    • 퍼블릭 인터페이스 : 객체가 수신할 수 있는 메시지의 집합

객체지향 프로그래밍에서 타입을 정의하는 것객체의 퍼블릭 인터페이스를 정의하는 것과 동일하다.

→ 앞서 말했던 ‘동일한 메시지에 대해 서로 다르게 행동할 수 있는 다형적인 객체를 구현하기 위해서는 객체의 행동을 기반으로 타입 계층을 구성해야 한다. ‘ 의 주장과 일맥상통하다고 생각했다.

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

→ 같은말 강조

💡 객체에게 중요한 것은 속성이 아니라 행동이라는 사실이다.

→ 책의 초반에, 메서드를 중심으로 객체를 선택하라는 파트에서 메서드를 강조한 맥락과 비슷하다고 생각했다.

2. 타입 계층

타입 사이의 포함관계

타입 계층을 구성하는 두 타입 간의 관계에서 더 일반적인 타입슈퍼타입(supertype)이라고 부르고 더 특수한 타입서브타입(subtype)이라고 부른다.

일반적인 타입의 인스턴스 집합은 특수한 타입의 인스턴스 집합을 포함하는 슈퍼셋(superset)이다. 반대로 특수한 타입의 인스턴스 집합은 일반적인 타입의 인스턴스 집합에 포함된 서브셋(subset)이다.

슈퍼타입은 다음과 같은 특징을 가지는 타입을 가리킨다.

  1. 집합이 다른 집합의 모든 멤버를 포함한다.

  2. 타입 정의가 다른 타입보다 좀 더 일반적이다.

서브타입은 다음과 같은 특징을 가지는 타입을 가리킨다.

  1. 집합에 포함되는 인스턴스들이 더 큰 집합에 포함된다.

  2. 타입 정의가 다른 타입보다 좀 더 구체적이다.

→ 상대적이라는 생각이 들었음.

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

→ 타입 계층의 개념을 객체지향에 적용하여 설명하였다.

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

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

→ 되게 상대적이다라는 생각이 많이 들었다. 결국엔 퍼블릭 인터페이스, 행동, 메서드가 중요한 것인가라고 생각하였다.

서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.

3. 서브클래싱과 서브타이핑

  • 객체지향 프로그래밍 언어에서 타입을 구현하는 일반적인 방법클래스를 이용하는 것이다.

  • 타입 계층을 구현하는 일반적인 방법은 상속을 이용하는 것이다.

→ 초반에서 이 챕터의 목표가 올바른 타입 계층을 구성하는 것이라고 하였다. 클래스로 타입을 구현하고, 상속으로 타입 계층을 구현할 것이라고 예상하였다.

언제 상속을 사용해야 하는가 ?

→ 상속의 두 가지 용도가 1) 타입 계층 구현, 2) 코드 재사용 이라고 하였으니 두 가지를 설명하지 않을까 ..?

다음과 같은 경우에 상속을 사용

  • 상속 관계가 is-a 관계를 모델링하는가 ?

    • 이것은 애플리케이션을 구성하는 어휘에 대한 우리의 관점에 기반한다. 일반적으로 “**[자식 클래스]는 [부모 클래스]**다”라고 말해도 이상하지 않다면 상속을 사용할 후보로 간주할 수 있다.

      → 토끼는 동물이다.

  • 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가 ?

    • 상속 계층을 사용하는 클라이언트의 입장에서 부모 클래스와 자식 클래스의 차이점을 몰라야 한다. 이를 자식 클래스와 부모 클래스 사이의 행동 호환성이라고 부른다.

→ 코드 재사용 관점보다는 타입 계층 구현의 관점에서 상속을 언제 사용해야할지 말하는 것인가 ?

is-a 관계

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

  • 어떤 타입 S가 다른 타입 T의 일종이라면 당연히 “타입 S는 타입 T다(S is-a T)”라고 말할 수 있어야 한다.

→ 똑같은 얘기 .. 타입 토끼는 타입 동물이다

행동 호환성

  • 타입의 이름 사이에 개념적으로 어떤 연관성이 있다고 하더라도 행동에 연관성이 없다면 is-a 관계를 사용하지 말아야 한다.

  • 여기서 중요한 것은 행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이라는 것이다.

→ 토끼가 할 수 있는 것중에 동물이 할 수 없는거 ..?

→ 토끼는 행동 호환성이 좋은듯

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

→ 이 전에 펭귄이 새를 상속 받았는데, 새의 날아댕기기(fly)를 펭귄은 할 수가 없음.

→ 즉 상속을 사용해야 하는 경우에 포함되지 않음 (행동 호환성을 충족시키지 못함.)

→ 그거에 따른 해결책을 설명하는 챕터다.

  1. 클라이언트의 기대에 따라 상속 계층을 분리

public class Bird {
	...
}

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

public class Peguin extends Bird {
	...
}

→ FlyingBird로 말 그대로 상속 계층을 분리 시켰다.

  1. 클라이언트의 기대에 따른 인터페이스 분리

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

  • 합성을 이용한 코드 재사용

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

요점은 클라이언트가 기대하는 행동에 집중하는 것이다.

서브클래싱과 서브타이핑

상속을 사용하는 두 가지 목적 중 하나는 코드 재사용을 위해서고, 다른 하나는 타입 계층을 구성하기 위해서다. 전자를 서브클래싱이라하고 후자를 서브타이핑이라 한다.

  • 서브클래싱(subclassing) :

    • 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우를 가리킨다. 자신 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다.

    • 서브클래싱을 구현 상속(implementation inheritance) 또는 클래스 상속(class inheritance)이라고 부르기도 한다.

    → 상속 대신 합성을 사용하라고 했으니까 결국은 상속은 서브 클래싱보다 서브타이핑을 목적으로 사용하는 것인가 ?

    → 그러면 서브클래싱은 단순 코드 재사용을 목적으로 하므로, 행동 호환성을 굳이 만족시키지 않아도 되는가 ?

    → 뒤에 답 나옵니다.

  • 서브타이핑(subtyping) :

    • 타입 계층을 구성하기 위해 상속을 사용하는 경우를 가리킨다. 서브타이핑에서는 자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다.

    • 서브타이핑을 인터페이스 상속(interface inheritance)이라고 부르기도 한다.

서브 클래싱과 서브타이핑을 나누는 기준은 상속을 사용하는 목적이다.

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

4. 리스코프 치환 원칙

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

    • 서브타입은 그것의 기반 타입에 대해 대체 가능해야 한다

    • 리스코프 치환 원칙은 앞에서 논의한 행동 호환성을 설계 원칙으로 정리한 것

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

클라이언트와 대체 가능성

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

→ 앞서

  • 행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이라는 것이다.

  • 요점은 클라이언트가 기대하는 행동에 집중하는 것이다.

와 일맥상통하다고 생각했다.

is-a 관계 다시 살펴보기

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

  • ‘펭귄은 새다.’의 경우 속성을 기준으로 is-a 관계를 정의한 것

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

→ <언제 상속을 사용해야 하는가>의 1) is-a 관계 충족, 2) 행동 호환성 모두 서브타이핑의 경우를 언급했던 것이었다.

→ 단순 서브클래싱을 구현하기 위해 상속을 사용했다면 이는 상속을 사용해야 하는 조건을 충족시키지 못한다.

→ 즉, 상속은 서브타이핑의 경우에 사용해야 한다.

→ 이러면 애당초 서브 클래싱이라는 개념을 왜 만들어서 헷갈린게 한걸까 ..

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

리스코프 치환 원칙을 따르는 설계는 유연할뿐만 아니라 확장성이 높다.

→ 리스코프 치환 원칙 (feat. 행동호환성) 즉, 상속을 서브타이핑의 경우로 사용하면 유연하고 확장성 높은 코드를 짤 수 있다.

→ 초반에 올바른 타입 계층을 구성하는 원칙이 아래와 같은 방법들로 추려지는 것이 맞나 ..?

⇒ 상속을 이용 (서브타이핑 경우)

⇒ 클라이언드의 기대에 따라 상속 계층 분리하기, 인터페이스 분리하기

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

한 가지 잊지 말아야 하는 사실은 클래스 상속은 타입 계층을 구현할 수 있는 다양한 방법 중 하나일 뿐이라는 것이다.

핵심은 구현 방법과 무관하게 클라이언트 관점에서 슈퍼타입에 대해 기대하는 모든 것이 서브타입에게도 적용돼야 한다는 것이다.

→ 행동호환성이 중요한 것이다. 상속은 타입 계층을 구현하는 다양한 방법 중 하나이다.

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

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

계약에 의한 설계의 구성요소

  • 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 사전조건(precondition)

  • 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 사후조건(postcondition)

  • 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 클래스 불변식

리스코프 치환 원칙은 어떤 타입이 서브타입이 되기 위해서는 슈퍼타입의 인스턴스와 협력하는 ‘클라이언트’의 관점에서 서브타입의 인스턴스가 슈퍼타입을 대체하더라도 협력에 지장이 없어야 한다는 것을 의미한다.

→ 행동 호환성과 같은 말인 것 같다.

서브클래스와 서브타입은 서로 다른 개념이다.

  • 어떤 클래스가 다른 클래스를 상속받으면 그 클래스의 자식 클래스 또는 서브클래스가 되지만 모든 서브클래스가 서브타입인 것은 아니다.

  • 코드 재사용을 위해 상속을 사용했다면, 그리고 클라이언트의 관점에서 자식 클래스가 부모 클래스를 대체할 수 없다면 서브타입이라고 할 수 없다.

  • 서브타입이 슈퍼타입처럼 보일 수 있는 유일한 방법은 클라이언트가 슈퍼타입과 맺은 계약을 서브타입이 준수하는 것뿐이다.

→ 서브클래스 ) 서브타입

서브타입과 계약

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

자식 클래스가 부모 클래스의 서브타입이 되기 위해서는 다음 조건을 만족시켜야 한다.

public class BrokenDiscountPolicy extends DiscountPolicy {
	public BrokenDiscountPolicy(DiscountCondition ... conditions) {
		super(conditions);
	}

	@Override
	public Money calculateDiscountAmount(Screening screeing) {
		checkPrecondtition(screening);
		checkStrongerPrecondition(screening);

		Money amount = screening.getMovieFee();
		checkPostconsition(amount);
		return amount; 
	}

	private void checkStrongerPercondition(screening screening) {
		seert screeing.getEndTime().toLocalTime().isbefore(LocalTime.MIDNIGHT);
	}

	@Override
	protected Money getDiscountAmount(Screening screening) {
		return Money.ZERO;
	}
}
  1. 서브타입에 더 강력한 사전조건을 정의할 수 없다.


public class BrokenDiscountPolicy extends DiscountPolicy {
...

	@Override
	public Money calculateDiscountAmount(Screening screeing) {
		// checkPrecondtition(screening); // 기존의 사전조건 제거

		Money amount = screening.getMovieFee();
		checkPostconsition(amount); // 기존의 사후조건
		return amount; 
	}

...
}
  1. 서브타입에 슈퍼타입과 같거나 더 약간 사전 조건을 정의할 수 있다.

public class BrokenDiscountPolicy extends DiscountPolicy {
	public BrokenDiscountPolicy(DiscountCondition ... conditions) {
		super(conditions);
	}

	@Override
	public Money calculateDiscountAmount(Screening screeing) {
		checkPrecondtition(screening); // 기존의 사전조건 제거

		Money amount = screening.getMovieFee();
		checkPostconsition(amount); // 기존의 사후조건
		checkStrongerPostcondition(amount); // 더 강력한 사후조건
		return amount; 

}
  1. 서브타입에 슈퍼타입과 같거나 더 강한 사후조건을 정의할 수 있다.

public class BrokenDiscountPolicy extends DiscountPolicy {
...
	@Override
	public Money calculateDiscountAmount(Screening screeing) {
		checkPrecondtition(screening); // 기존의 사전조건 제거

		Money amount = screening.getMovieFee();
		// checkPostconsition(amount); // 기존의 사후조건
		checkWeakerPostcondition(amount) // 더 약한 사후조건
		return amount; 
	}

...
}
  1. 서브타입에 더 약한 사후조건을 정의할 수 없다.

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

Last updated