본문 바로가기
Language/객체지향 설계

[page.49 ~ 65] 다형성

by 박서현 2022. 11. 10.

오브젝트 49 ~ 65p

Upcasting과 다형성

public class Movie {
    private DiscountPolicy discountpolicy;
    
    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

위의 Movie 클래스는 영화 예매 비용을 계산하는 메서드에서 discountPolicy 객체에 "calculateDiscountAmount"라고 메세지를 보낸다. 그런데 일반적으로는 외부에서 discountPolicy 객체에 구현한 메서드를 사용할 때 메서드를 호출한다고 한다. "메세지를 보낸다"와 "메서드를 호출한다"는 어떤 차이가 있을까?

public abstract class DiscountPolicy {
    public Money calculateDiscountAmount() {
        ...
    }
    
    abstract protected Money getDiscount(Screening screening);
}

// 금액 할인 정책
public class AmountPolicy extends DiscountPolicy {
    @Overriding
    protected Money getDiscount() {
        ...
    }
}

// 비율 할인 정책
public class PercentPolicy extends DiscountPolicy {
    @Overriding
    protected Money getDiscount() {
        ...
    }
}

차이는 다형성에서 비롯한다. 먼저 상속을 살펴보자. DiscountPolicy는 금액 할인 정책과 비율 할인 정책의 인터페이스다. 내부 구현(calculateDiscountAmount)도 상속하기 때문에 추상 클래스로 정의한다.

자식 클래스(AmountPolicy, PercentPolicy)는 부모 클래스의 데이터, 구현을 모두 상속받기 때문에 부모 클래스를 대신할 수 있다. 자식 클래스가 부모 클래스를 대신하는 것을 Upcasting이라고 한다. Upcasting을 통해 한 클래스가 여러 가지 형태를 가질 수 있는 특성을 "다형성"이라고 한다. Movie 코드에는 DiscountPolicy 객체를 이용해 할인 금액을 계산(calculateDiscountAmount)하지만 실제로는 Upcasting을 통해 AmountPolicy 혹은 PercenPolicy 객체를 이용한다.

즉, 컴파일 타임(코드)과 런 타임(실행) 사이에 객체 의존성 구조가 다르다. 코드만 보면 Movie는 calculateDiscountAmount가 AmountPolicy 객체에서 동작하는지 PercentPolicy에서 동작하는지 모른다. 그저 "할인 금액을 계산해주세요(calculateDiscountAmount)"라고 discountPolicy에 메세지를 보내는 것이다. 하지만 런타임에서 메세지는 실제 객체에서 메서드를 호출한다. 다시 말해 동일한 코드더라도 컴파일 타임에는 "클래스가 불특정 객체에 메세지를 보낸다"고 표현할 수 있고, 런타임에는 "클래스가 특정 객체의 메서드를 호출한다"라고 할 수 있다.

참고 : 다형성은 꼭 상속으로만 구현할 수 있는 건 아니라고 한다. (아직 다른 방법은 모른다) 


코드 의존성 vs 실행 시점 의존성

최상단 코드에서 Movie 클래스는 DiscountPolicy 클래스에 의존한다. 이를 코드 의존성이라고 한다. 런타임에서는 AmountPolicy 혹은 PercentPolicy에 의존한다. 이를 실행 시점 의존성이라고 한다. 반면 아래 코드에서 Movie는 AmountPolicy 혹은 PercentPolicy에 의존한다. 즉 코드 의존성과 실행 시점 의존성이 줄어든 것이다.

public class Movie {
    public Money calculateMovieFee(AmountPolicy amountPolicy, Screening screening) {
        return fee.minus(amountPolicy.calculateDiscountAmount(screening));
    }
    
    public Money calculateMovieFee(PercentPolicy percentPolicy, Screening screening) {
        return fee.minus(percentPolicy.calculateDiscountAmount(screening));
    }
}

코드 의존성과 실행 시점 의존성이 줄어들면 가독성이 좋아진다. 어떤 클래스를 지칭하는지 구체적으로 명시하기 때문이다. 반면 유연성은 떨어질 수 있다. 새로운 할인 정책이 추가되면 그에 맞춰 calculateMovieFee를 오버로딩 해야 하기 때문이다. 즉 코드 의존성과 실행 시점 의존성이 다를 수록 가독성은 떨어지고 유연성은 좋아진다. 결국 상황에 따라 가독성과 유연성 사이에 선택해야 한다. (트레이드 오프)


지연 바인딩 vs 초기 바인딩

자바와 같이 실행 시점(Runtime)에 메세지와 메서드를 바인딩하는 것을 지연 바인딩(Lazy Binding) 혹은 동적 바인딩(Dynamin Binding)이라고 한다. 반면 절차 지향 프로그래밍 언어에서는 컴파일 시점에 메세지와 메서드를 바인딩하는데 이를 초기 바인딩(Early Binding) 혹은 정적 바인딩(Static Binding)이라고 한다.


추상 클래스와 인터페이스의 차이는 무엇일까?

추상 클래스는 내부 데이터나 구현까지도 상속해서 코드를 재사용할 수 있다. 인터페이스는 구현 없이 어떻게 구현해야 하는지 클래스 구조만 정의한다. 인터페이스에서도 디폴트 메서드(default method)와 스태틱 메서드(static method)를 지원하지만 인터페이스의 본질적인 기능과는 관련이 없다. 디폴트 메서드는 인터페이스를 구현한 모든 클래스에 일괄적으로 구현이 필요한 경우, 즉 변경 이슈가 있을 때 사용한다. 스태틱 메서드를 이용하면 유틸리티성 인터페이스를 구현할 수 있다.


구현 상속 vs 인터페이스 상속

구현 상속은 서브 클래싱(subclassing), 인터페이스 상속은 서브타이핑(subtyping)이라고도 한다. 구현 상속은 오직 코드를 재사용하기 위한 목적으로 상속한다. 인터페이스 상속은 부모와 자식이 인터페이스를 공유하기 위한 목적으로 상속한다. "오브젝트"에 따르면 구현 상속은 변경에 취약하고, 인터페이스 상속으로 구현하는 것을 권장한다. 둘을 구분하는 기준은 Upcasting의 필수 여부다. 클래스나 인터페이스를 상속했는데 Upcasting을 활용하지 않아도 상관없는 경우는 구현 상속, Upcasting이 꼭 필요한 경우는 인터페이스 상속이라고 할 수 있을 것 같다.

'Language > 객체지향 설계' 카테고리의 다른 글

[page.37 ~ 49] 객체지향 설계  (0) 2022.11.10
[page.7 ~ 36] 캡슐화  (0) 2022.11.10