반응형
오브젝트 13 - 서브클래싱과 서브타이핑
- 상속의 첫번째 용도는 타입 계층을 구현하는 것이다.
- 계층 안에서 부모 클래스는 일반적인 개념을 구현하고 자식 클래스는 특수한 개념을 구현한다.
- 상속의 두번째 용도는 코드 재사용이다.
- 간단한 선언만으로 부모 클래스의 코드를 재사용할 수 있다.
- 그러나 부모 클래스와 자식 클래스가 강결합 된다.
- 상속의 사용의 목표는 코드 재사용이 아닌 타입 계층을 구현하는 것이어야한다.
13.1 타입
13.1.1 개념 관점의 타입
- 개념 관점에서 타입이란 우리가 인지하는 세상의 사물의 종류를 의미한다.
- 어떤 대상이 타입으로 분류될 때 그 대상을 타입의 인스턴스라고 부른다.
- 일반적으로 타입의 인스턴스를 객체라고 부른다.
13.1.2 프로그래밍 언어 관점의 타입
- 프로그래밍 언어 관점에서 타입은 연속적인 비트에 의미와 제약을 부여하기 위해 사용된다.
- 타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의한다.
- 타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공한다.
13.1.3 객체지향 패러다임 관점의 타입
- 객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이다.
- 즉, 객제지향 프로그래밍에서 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일하다.
- 객체지향에서는 객체가 수신할 수 있는 메시지를 기준으로 타입을 분류하기 때문에 동일한 퍼블릭 인터페이스를 가지는 객체들은 동일한 타입으로 분류할 수 있다.
- 타입의 정의는 객체에게 중요한것은 속성이 아니라 행동이라는 사실을 강조한다.
13.2 타입 계층
13.2.1 타입 사이의 포함관계
- 타입은 객체들의 집합이므로 다른 타입을 포함할 수 있다.
- 타입은 집합의 관점에서 더 세분화된 타입의 집합을 부분집합으로 포함할 수 있다.
- 타입이 다른 타입에 포함될 수 있기 때문에 동일한 인스턴스가 하나 이상의 타입으로 분류되는 것도 가능하다.
- 다른 타입을 포함하는 타입은 포함되는 타입보다 좀 더 일반화된 의미를 표현할 수 있다.
- 다른 타입을 포함하는 타입은 포함되는 타입보다 더 많은 인스턴스를 가진다.
- 타입 계층을 구성하는 두 타입 간의 관계에서 더 일반적인 타입을 슈퍼타입이라고 부른다.
- 더 특수한 타입을 서브타입이라고 부른다.
13.2.2 객체지향 프로그래밍과 타입 계층
- 슈퍼타입이란 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의한 것이다.
- 서브타입이란 슈퍼타입이 정의한 퍼블릭 인터페이스를 특수화시켜 상대적으로 구체적이고 좁은 의미로 정의한것이다.
- 서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.
13.3 서브클래싱과 서브타이핑
13.3.1 언제 상속을 사용해야 하는가?
- 아래 두 질문에 모두 "예"라고 대답할 경우에만 상속을 사용하자
- 상속 관계가 is-a 관계를 모델링할 때
- 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?
13.3.2 is-a 관계
- 어떤 타입 S가 다른 타입 T의 일종이라면 "타입 S는 타입 T다" 라고 말할 수 있어야 한다.
- 그러나 is-a 관계가 생각처럼 직관적이고 명쾌한 것은 아니다.
- 펭귄은 새다
- 새는 날 수 있다
- 펭귄은 새지만 날 수 없다.
- 타입은 어휘적인 정의가 아니라 기대되는 행동에 따라 타입 계층을 구성해야한다.
- 어휘적으로 펭귄은 새지만 만약 새의 정의에 낼 수 있다는 행동이 포함된다면 펭귄은 새의 서브타입이 될 수 없다.
- 만약 새의 정의에 날 수 있다는 행동이 포함되지 않는다면 펭귄은 새의 서브타입이 될 수 있다.
- 따라서 슈퍼타입과 서브타입 관계는 is-a 보다 행동 호환성이 더 중요하다.
13.3.3 행동 호환성
- 타입의 이름 사이에 개념적으로 어떤 연관성이 있어도 행동에 연관이 없다면 is-a 관계를 사용하지 말아야한다.
- 행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이다.
- 클라이언트가 두 타입이 동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층으로 묶을 수 있다.
13.3.4 클라이언트의 기대에 따라 계층 분리하기
- 이제 FlyingBird 타입의 인스턴스만이 fly 메시지를 수신할 수 있다.
- 이 문제를 해결하는 다른 방법은 클라이언트에 따라 인터페이스를 분리하는 것이다.
- 만약 Bird가 날 수 있으면서도 걸을 수 있어야 하고, Penguin은 오직 걸을 수만 있다면 인터페이스를 분리하자.
- 만약 Penguin이 Bird의 코드를 재사용해야 한다면 합성을 이용하자
- Client1의 기대가 바뀌어서 Flyer의 인터페이스가 변경되어야 해도 Flyer에 의존하는 Bird만 영향을 받을 뿐 Client와 Penguin은 아무 영향을 받지 않는다.
- 이처럼 인터페이스를 클라이언트 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙을 인터페이스 분리 원칙이라고 부른다.
- 설계가 꼭 현실세계를 반영할 필요는 없다.
- 중요한것은 설계가 반영할 도메인의 요구사항이고 그 안에서 클라이언트가 객체에게 요구하는 행동이다.
13.3.5 서브클래싱과 서브타이핑
- 서브클래싱 : 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는경우를 가리킨다 (= 구현 상속, 클래스 상속)
- 서브타이핑 : 타입 계층을 구성하기 위해 상속을 사용하는 경우를 가리킨다 ( = 인터페이스 상속)
- 서브타이핑 관계가 유지되기 위해서는 서브타입이 슈퍼타입이 하는 모든 행동을 동일하게 할 수 있어야 한다.
13.4 리스코프 치환 원칙
- 리스코프 치환 원칙을 정리하면 아래와 같다.
- 서브타입은 그것의 기반 타입에 대해 대체 가능해야 한다.
- 클라이언트가 차이점을 인식하지 못한 채 파생 클래스의 인터페이스를 통해 서브클래스를 사용할 수 있어야 한다.
13.4.1 is-a 관계 다시 살펴보기
- 클라이언트 입장에서 부모 클래스 대신 자식 클래스를 사용할 수 있는가?
- is-a는 클라이언트 관점에서 is-a일 때만 참이다.
- 객체지향에서 중요한 것은 객체의 속성이 아니라 객체의 행동이라는 점을 강조한다.
13.4.2 리스코프 치환 원칙은 유연한 설계의 기반이다
- 리스코프 치환 원칙은 어떤 자식 클래스가 와도 안정적으로 협력할 수 있는 상속 구조를 구현할 수 있는 가이드라인을 제공한다.
- 리스코프 치환 원칙을 따르는 설계는 유연할뿐만 아니라 확장성이 높다.
- 리스코프 치환 원칙은 개방-폐쇄 원칙을 지원한다.
- 자식 클래스가 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 수정하더라도 코드를 수정할 필요가 없어진다.
13.5 계약에 의한 설계와 서브타이핑
- 계약에 의한 설계 : 클라이언트와 서버 사이의 협력을 의무와 이익으로 구성된 계약의 관점에서 표현하는 것
- 사전조건 : 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 조건
- 사후조건 : 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 사후 조건
- 클래스 불변식 : 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 것
public abstract class DiscountPolicy {
public Money calculateDiscountAmount(Screening screening) {
checkPrecondition(screening);
Money amount = Money.ZERO;
for(DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
amount = getDiscountAmount(screening);
checkPostcondition(amount);
return amount;
}
}
amount = screening.getMovieFee();
checkPostcondition (amount);
return amount;
}
protected void checkPrecondition(Screening screening) {
assert screening != null && screening.getStartTime().isAfter(LocalDateTime.now());
}
protected void checkPostcondition(Money amount) {
assert amount != null && amount.isGreaterThanOrEqual(Money.ZERO);
}
abstract protected Money getDiscountAmount(Screening screening);
}
- 사전조건(checkPrecondition)과 사후조건(checkPostcondition) 메서드가 존재한다.
- DiscountPolicy의 메서드를 호출하는 Movie는 사전 조건을 위반하는 screening을 전달해서는 안된다.
public class Movie {
public Money calculateMovieFee(Screening screening) {
if (screening == null || screening.getStartTime().isBefore(LocalDateTime.now())) {
throw new InvalidScreeningException();
}
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
13.5.1 서브타입과 계약
- 부모 클래스의 메서드를 오버라이딩 해서 새로 작성할 경우 아래의 규칙을 지켜야한다.
- 부모 클래스의 사전조건보다 더 강력한 사전 조건을 정의할 수 없다.
- 부모 클래스의 사후조건보다 더 약한 사후 조건을 정의할 수 없다.
- 서브타이핑을 위해 상속을 사용하고 있다면 부모 클래스가 클라이언트와 맺고 있는 계약에 관해 깊이 있게 고민하기 바란다.
728x90
반응형
'스터디 > 오브젝트' 카테고리의 다른 글
오브젝트 15 - 디자인 패턴과 프레임워크 (0) | 2024.04.16 |
---|---|
오브젝트 14 - 일관성 있는 협력 (0) | 2024.04.15 |
오브젝트 12 - 다형성 (0) | 2024.04.02 |
오브젝트 11 - 합성과 유연한 설계 (0) | 2024.03.26 |
오브젝트 10 - 상속과 코드 재사용 (0) | 2024.03.26 |
댓글