오브젝트

[오브젝트] 상속과 합성 그리고 인터페이스

chadongmin 2024. 2. 13. 16:14

1. 상속 (Inheritance)

예를 들어, 할인정책이 없는 영화가 있다고 가정해보자.

Movie 클래스에서 할인 된 요금을 계산하는 메서드에 아래와 같이 if 문을 추가해서 discountPolicy가 존재하는지 존재하지 않는지 검증하는 로직이 추가된다.

 

이러한 방식은 일관된 협력을 무너뜨리는 방식이다.

기존에는 할인된 가격을 계산하는 책임이 discountPolicy의 자식클래스에 있었지만, 할인정책이 없는 경우라면 Movie클래스의 책임이 되고 만다.

 

어떻게 책임을 일관되게 유지할 수 있을까?

//할인요금 반환
    public Money calculateMovieFee(Screening screening) {
        if (discountPolicy == null){
            return fee;
        }
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }

 

해답은 바로, 할인 정책이 없을 때 0원을 반환하는 책임을 DiscountPolicy라는 계층에 유지시키는 것이다.

 

public class NoneDiscountPolicy extends DiscountPolicy{

    @Override
    protected Money getDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}

위와 같이 DiscountPolicy를 상속받는 NoneDiscountPolicy라는 클래스를 추가한다.

 

Movie avatar = new Movie(
       "아바타",
       Duration.ofMinutes(120),
       Money.wons(15000),
       new NoneDiscountPolicy());

그러면 위와 같이 할인정책이 없는 영화도 인스턴스를 생성할 수 있게 된다.

 

2. 추상클래스와 인터페이스의 Trade off

상속을 이용해서 할인정책이 없는 경우도 DiscountPolicy의 계층에서 책임지도록 만들었다. 하지만 이 방식의 문제점이 있다.

public abstract class DiscountPolicy {
.
.
.  
    public Money calculateDiscountAmount(Screening screening) {
        for (DiscountCondition c : conditions) {
            if (c.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening);
            }
        }
        return Money.ZERO;
    }
}

위 코드를 보면 할인조건이 없는 경우 Money.ZERO를 반환한다.

NoneDiscountPolicy의 getDiscountAmount가 호출되지 않으면 할인조건이 없다는 것이고,  할인금액이 0을 반환해버린다.

즉 NoneDiscountPolicy의 getDiscountAmount가 어떤 값을 반환하더라도 시스템에는 아무런 영향이 없게 돼버리는 문제가 발생한다. 

 

DiscountPolicy를 추상클래스가 아닌 인터페이스로 바꿔보자.

 

public interface DiscountPolicy {

    Money calculateDiscountAmount(Screening screening);

}

 

calculateDiscountAmount를 인터페이스의 메서드로 선언하고 구현체들이 구현하도록 위임한다.

public class NoneDiscountPolicy implements DiscountPolicy{
    @Override
    public Money calculateDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}

NoneDiscountPolicy는 DiscountPolicy를 직접 구현한다. 

나머지 AmoundDiscountPolicy와 PercentDiscountPolicy는 기존의 추상 클래스를 상속받아 메서드를 구현한다.

 

즉 위 그림과 같은 설계로 구현이 바뀐다. 

NoneDiscountPoicy라도 메서드가 호출되지 않는 문제점이 해결된다. 

상속의 문제점, 그리고 합성(Composition)

상속은 객체지향에서 코드를 재사용하기 위해 널리 사용되지만 두 가지 관점에서 설계에 안좋은 영향을 미친다.

  • 상속의 단점
    • 캡슐화를 위반
      • 상속을 이용하기 위해서는 부모클래스의 내부 구조를 잘 알고 있어야함
      • 자식 클래스를 구현하는 개발자는 상위 클래스의 메서드의 호출방식 등을 알고 있어야 함
      • 결국 상위 클래스의 구현이 하위 클래스에 노출되기 때문에 캡슐화가 약화된다.
      • 캡슐화의 약화는 하위 클래스가 부모 클래스에 강하게 결합되도록 만들어서 상위 클래스를 변경할때 하위 클래스도 변경할 가능성을 높인다.
    • 설계를 유연하지 못하게 만듦
      • 상위 클래스와 하위 클래스 사이의 관계를 컴파일 시점에 결정함 → 실행시점에 객체의 종류 변경 불가
      • 상속보다 인스턴스 변수로 관계를 연결하는 설계가 더 유연함 → 실행시점에 객체 종류 바꿀 수 있음

실행시점에 '금액 할인 정책'인 영화를 '비율 할인 정책'으로 변경해야 하는 상황을 가정해보자.

 

public class Movie {

    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = discountPolicy;
    }
}

 

movie 인스턴스는 전략패턴을 이용해서 런타임에 movie의 생성자의 파라미터로 discountPolicy의 자식 클래스를 주입받는다.

하지만 movie 인스턴스가 생성된 이후에 어떻게 discountPolicy 속성을 변경할 수 있을까?

 

public class Movie {

    private DiscountPolicy discountPolicy;
.
.
.

    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = discountPolicy;
    }
    
    public void changeDiscountPolicy(DiscountPolicy discountPolicy){
        this.discountPolicy = discountPolicy;
    }

 

changeDiscountPolicy 메서드를 추가하면 간단하게 해결된다.

 

이처럼 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다.

 

인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 클래스 내부 구현을 완벽히 캡슐화 할 수 있으며,

의존하는 인스턴스를 교체하는 것이 쉽다.

 

위 처럼 Movie를 상속받는 AmountDiscountMovie와 PercentDiscountMovie라는 두 클래스로 구현해도 기능적으로 똑같이 동작한다.

하지만 이 방식은 런타임에 할인정책을 바꿀 수 없다. 

 

위 그림과 같이 DiscountPolicy를 인터페이스로 분리하고 movie가 DiscountPolicy 인터페이스를 속성으로 갖고 있는다면, 

changeDiscountPolicy 메서드를 사용해서 업캐스팅 된 DiscountPolicy의 구현체를 교체할 수 있게 된다.

 

즉, 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용할 수 있게 된다.