목차

오브젝트; 상속을 합성으로 바꾸기

🗓️

합성 관계로 변경하기

컴파일 타임 관계를 런타임 관계로 변경함으로써 이 문제를 해결한다. 이것이 합성이다. 상속을 사용하는 것은 컴파일 타임의 의존성과 런타임의 의존성을 동일하게 만들겠다고 선언하는 것이다. 그래서 상속을 사용하면 부모 클래스와 자식 클래스 사이의 관계가 정적으로 고정되기 때문에 실행 시점에 동적으로 관계를 변경할 수 있는 방법이 없다.

합성을 사용하면 구현 시점에 정책들의 관계를 고정시킬 필요가 없으며 실행 시점에 정책들의 관계를 유연하게 변경할 수 있게 된다.

  • 상속이 조합의 결과를 개별 클래스 안으로 밀어 넣는 방법이라면,
  • 합성은 조합을 구성하는 요소들을 개별 클래스로 구현한 후 실행시점에 인스턴스를 조립하는 방법을 사용하는 것.

컴파일 타임 의존성과 런타임 의존성의 거리가 멀면 설계의 복잡도가 상승하기 때문에 코드 파악이 어려워지는 문제가 있지만, 변경하기 편리한 설계를 만들기 위해 복잡성을 더하고 나면 원래의 설계보다 단순해진느 경우가 있다. 다음의 예시가 바로 그런 경우다.

기본 정책 합성하기

인터페이스를 추가한다. 그리고 기본 정책을 구현한다.

public interface RatePolicy {
    Money calculateFee(Phone phone);
}

public abstract class BasicRatePolicy implements RatePolicy {
    @Override
    public Money calculateFee(Phone phone) {
        Money result = Money.ZERO;

        for(Call call : phone getCalls()) {
            result.plus(calculateCallFee(call));
        }

        return result;
    }

    protected abstract Money calculateCallFee(Call call);
}

calculateCallFee()에 자신만의 방식을 구현할 수 있다.

public class RegularPhone extends BasicRatePolicy {
    private Money amount;
    private Duration seconds;

    public RegularPhone(Money amount, Duration seconds) {
        // constructor..
    }

    @Override
    protected Money calculateCallFee(Call call) {
        return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}

심야 할인 요금제.

public class NightlyDiscountPhone extends Phone {
    private static final int LATE_NIGHT_HOUR = 22;

    private Money nightlyAmount;
    private Money regularAmount;
    private Duration seconds;

    public NightlyDiscountPhone(Money nightlyAmount,
                                Money regularAmount,
                                Duration seconds) {
        // constructor... 
    }

    @Override
    protected Money calculateCallFee(Call call) {
        //...
    }
}

기본 정책을 이용해 요금을 계산 할 수 있도록 Phone 수정

public class Phone {
    private RatePolicy ratePolicy;
    private List<Call> calls = new ArrayList<>();

    public Phone(RatePolicy ratePolicy) {
        // constructor.. 
    }

    public List<Call> getCalls() {
        return Collections.unmodifiableList(calls);
    }

    public Money calculateFee() {
        return ratePolicy.calculateFee(this);
    }
}

Phone 내부에 RatePolicy에 대한 참조자가 포함되어있다. 이것이 합성이다. 인스턴스에 대한 의존성을 주입받음으로써 런타임 의존성으로 대체된다.

다양한 종류의 객체와 협력하기 위해 합성 관계를 사용하는 경우에는 합성하는 객체의 타입을 인터페이스나 추상 클래스로 선언하고 의존성 주입을 사용해 런타임에 필요한 객체를 설정할 수 있도록 구현하는 것이 일반적이다.

// 일반요금제의 인스턴스 합성
Phone phone = new Phone(new RegularPolicy(Money.wons(10),
                                          Duration.ofSeconds(10)));

// 심야요금제의 인스턴스 합성
Phone phone = new Phone(new NightlyDiscountPolicy(Money.wons(5),
                                                  Money.wons(10),
                                                  Duration.ofSeconds(10)));

여기까지의 구현이 더 복잡해 보일 수 있으나 이 설계에서 부가 정책을 추가해보면 합성이 얼마나 강력한지 알 수 있다.

부가 정책 적용하기

부가정책을 추가할때 두가지 제약에 따라 부가 정책을 구현해야 한다.

  • 부가 정책은 기본 정책이나 다른 부가 정책의 인스턴스를 참조할 수 있어야 한다. 다시 말해서 부가 정책의 인스턴스는 어떤 종류의 정책과도 합성될 수 있어야 한다.
  • Phone의 입장에서는 자신이 기본 정책의 인스턴스에게 메시지를 전송하고 있는지, 부가 정책의 인스턴스에게 메시지를 전송하고 있는지를 몰라야 한다. 자시 말해 기본 정책과 부가 정책은 협력 안에서 동일한 ‘역할’을 수행해야 한다. 이것은 부가 정책이 기본 정책과 동일한 RatePolicy인터페이스를 구현해야 한다는 것을 의미한다.

부가 정책을 위한 추상 클래스

public abstract class AdditionalRatePolicy implements RatePolicy {
    private RatePolicy next;

    public AdditionalRatePolicy(RatePolicy next) {
        this.next = next;
    }

    @Override
    public Money calculateFee(Phone phone) {
        Money fee = next.calculateFee(phone);
        return afterCalculated(fee);
    }

    abstract protected Money afterCalculated(Money fee);
}

next로 다음 정책을 이어받을 수 있다.

부가 정책 상속 클래스

public class TaxablePolicy extends AdditionalRatePolicy {
    private double taxRatio;

    public TaxablePolicy(double taxRatio, RatePolicy next) {
        super(next);
        this.taxRatio = taxRatio;
    }

    @Override
    protected Money afterCalculated(Money fee) {
        return fee.plus(fee.times(taxRatio));
    }
}


public class RateDiscountablePolicy extends AdditionalRatePolicy {
    private Money discountAmount;

    public RateDiscountablePolicy(Money discountAmount, RatePolicy next) {
        super(next);
        this.discountAmount = discountAmount;
    }

    @Override
    protected Money afterCalculated(Money fee) {
        return fee.minus(discountAmount);
    }
}

기본 정책과 부가 정책 합성하기

Phone phone = new Phone(new TaxablePolicy(0.05),
                        new RateDiscountablePolicy(Money.wons(1000),
                                                   new RegularPolicy(...)));

새로운 정책 추가하기

기본 정책의 추가

부가정책의 추가

상속과 달리 하나의 클래스만 추가하고 런타임에 필요한 정책들을 조합해서 원하는 기능을 얻을 수 있다

객체 합성이 클래스 상속보다 더 좋은 방법이다.