본문 바로가기
스터디/오브젝트

오브젝트 13 - 서브클래싱과 서브타이핑

by 디토20 2024. 4. 9.
반응형

 

 

 

 

오브젝트 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
반응형

댓글