상속과 합성은 OOP에서 가장 널리 사용되는 코드 재사용 기법이다. 합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용한다. 상속에서 부모-자식 클래스 간
의존성은 컴파일타임에 해결되지만 합성에서 두 객체 사이의 의존성은 런타임에 해결된다.
상속 관계는 is-a 관계라고 부르고 합성 관계는 has-a 관계라고 부른다. 동일한 목적을 가진다는
점을 제외하면 두 방식은 많은 차이를 가진다.
상속을 이용하면 간단히 기존 코드를 쉽게 확장할 수 있으나, 부모 클래스이 내부 구현을 상세히
알아야 한다는 한계가 존재해, 부모-자식 간 결합도가 높아질 수 밖에 없다.
합성은 구현에 의존하지 않는다는 점에서 상속과 다르다. 합성은 내부 구현이 아닌 퍼블릭
인터페이스에 의존한다. 포함된 객체의 내부 구현이 변경되더라도 영향을 최소화할 수 있어
변경에 더 안정적인 코드를 얻을 수 있다.
상속 관계는 정적인 관계인 데 비해 합성 관계는 객체 사이의 동적인 관계다.
[코드의 재사용을 위해서는] 객체 합성이 클래스 상속보다 더 좋은 방법이다.
상속은 부모 클래스에 구현된 코드 자체를 재사용하지만 합성은 포함되는 객체의 퍼블릭
인터페이스를 재사용한다.
서브클래싱에 의한 재사용을 화이트박스 재사용이라고 부른다. 부모 클래스 내부가 자식
클래스에 공개되기 때문에 가시성 차원에서 화이트박스라고 불리운다.
반면, 객체 합성을 통한 재사용을 블랙박스 재사용이라고 칭한다.
10장에서 소개한 상속 예제를 합성으로 바꾸어보자.
Hashtable 과 Properties 간 상속 관계를 합성을 전환하자.
public class Properties {
private Hashtable<String, String> properties = new Hashtable <>();
public String setProperty(String key, String value) {
return properties.put(key, value);
}
public String getProperty(String key) {
return properties.get(key);
}
}
더 이상 불필요한 Hashtable 의 오퍼레이션들이 Properties 에 포함되지 않으며, 클라이언트는
오직 Properties 내에 정의된 오퍼레이션만 사용할 수 있다.
Vector 를 상속받는 Stack 역시 Vector 인스턴스를 Stack 클래스에 인스턴스 변수로 선언하여
합성 관계로 변경할 수 있다.
public class Stack<E> {
private Vector<E> elements = new Vector<>();
public E push(E item) {
elements.addElement(item);
return item;
}
public E pop() {
if(elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size()-1);
}
}
InstrumentedHashSet 의 경우도 HashSet 인스턴스를 내부에 포함한 후 HashSet 의 퍼블릭
인터페이스에서 제공하는 오퍼레이션들을 이용해 필요한 기능을 구현하면 된다.
public class InstrumetedHashSet<E> {
private int addCount = 0;
private Set<E> set;
public InstrumentedHashSet(Set<E> set) {
this.set = set;
}
public boolean add(E e) {
addCount++;
return set.add(e);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
하지만 이 경우 HashSet이 제공하는 퍼블릭 인터페이스를 그대로 제공해야 한다.
HashSet 에 대한 구현 결합도는 제거하면서 퍼블릭 인터페이스를 그대로 상속받을 수 있는
방법으로는 Set 인터페이스를 구현하며 관련 오퍼레이션에 합성된 HashSet 을 이용하면 된다.
public class InstrumetedHashSet<E> implements Set<E> {
private int addCount = 0;
private Set<E> set;
public InstrumentedHashSet(Set<E> set) {
this.set = set;
}
@Override
public boolean add(E e) {
addCount++;
return set.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c);
}
public int getAddCount() {
return addCount;
}
// serve HashSet's public interface
@Override
public boolean remove(Object o) { return set.remove(o); }
@Override
public void clear() { set.clear(); }
// and others ...
}
이와 같은 형태를 포워딩이라 부르고 동일한 메서드를 호출하기 위해 추가된 메서드를 포워딩
메서드라고 부른다. 기존 클래스 인터페이스를 그대로 외부에 제공하며 구현에 대한 결합 없이
일부 작동 방식을 변경하고 싶은 경우 사용한다.
안타깝게도 Playlist 의 경우 합성으로 변경하더라도 자식 클래스까지 함께 수정해야 하는
문제가 해소되지 않는다.
public class PersonalPlaylist {
private Playlist playlist = new Playlist();
public void append(Song song) {
playlist.append(song);
}
public void remove(Song song) {
playlist.getTracks().remove(song);
playlist.getSingers().remove(song.getSinger());
}
}
그렇다 하더라도 향후 Playlist 이 내부 구현 변경 시 파급효과를 최대한 PersonalPlaylist 내부로
캡슐화할 수 있기 때문에, 상속보단 합성을 사용하는 것이 좋다.
몽키 패치
몽키 패치란 현재 실행 중인 환경에만 영향을 미치도록 지역적으로 코드를 수정하거나
확장하는 것을 가리킨다.
코드를 수정할 권한이 없거나 소스코드가 존재하지 않더라도 몽키 패치가 지원되는 환경이라면
Playlist 에 직접 remove 메서드를 추가하는 것이 가능하다. 자바는 언어 차원에서 몽키 패치를
지원하지 않아 바이트코드를 직접 변환하거나 AOP를 이용해 구현할 수 있다.
상속으로 인해 결합도가 높아지면 코드 수정에 많은 어려움이 발생한다. 일반적으로 작은 기능들을 조합해서 더 큰 기능을 수행하는 객체를 만들어야 할 경우 다음과 같은 두 가지 문제가 발생한다.
10장에서 소개했던 폰 과금 시스템의 두 요금제에 부가 정책을 추가하는 요구사항을 추가해보자.
지금부터 폰 요금제가 ‘기본 정책’과 ‘부가 정책’을 조합해서 구성된다고 가정할 것이다.
기본 정책은 가입자의 통화 정보를 기반으로 하며, 통화량을 한달 통화량을 기준으로 부과할 요금을 계산한다. 앞 장에서 소개한 요금제들이 기본 정책으로 분류된다.
부가 정책은 통화량과 무관하게 기본 정책에 선택적으로 추가할 수 있는 요금 방식으로 ‘세금 정책’,
‘기본 할인 정책’이 여기 속한다.
이 장의 주제를 이해하기 위해서는 부가 정책이 다음과 같은 특성을 가진다는 것을 알아야 한다.
다음 조건들을 통해 설계는 다양한 조합을 수용할 수 있도록 유연해야 한다는 것을 파악할 수 있다.
상속을 이용해서 기본 정책과 부가 정책을 구현해보자. 기본 정책은 아래 세 클래스로 구성된다.
public abstract class Phone {
private List<Call> calls = new ArrayList<>();
public Money calculateFee() {
Money result = Money.ZERO;
for (Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return result;
}
abstract protected Money calculateCallFee(Call call);
}
public class RegularPhone extends Phone {
private Money amount;
private Duration seconds;
public RegularPhone(Money amount, Duration seconds) {
this.amount = amount;
this.seconds = seconds;
}
@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) {
this.nightlyAmount = nightlyAmount;
this.regularAmount = regularAmount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
} else {
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
}
일반 요금제에 세금 정책을 조합해야 한다면, 가장 간단히 RegularPhoen 을 상속받은 TaxableRegularPhone 을 추가할 수 있다.
public class TaxableRegularPhone extends RegularPhone {
private double taxRate;
public TaxableRegularPhone(Money amount, Duration seconds, double taxRate) {
super(amount, seconds);
this.taxRate = taxRate;
}
@Override
public Money calculateFee() {
Money fee = super.calculateFee();
return fee.plus(fee.times(taxRate));
}
}
부모 클래스 메서드 재사용을 위해 super 키워드를 사용하면 원하는 결과를 쉽게 얻을 수 있지만
자식, 부모 간 결합도가 높아지고 만다. 결합도를 낮추는 방법은 부모 클래스에 자식이 추상 메서드를
제공하는 것이다.
Phone 클래스에 새 추상 메서드인 afterCalculated 를 추가하자.
public abstract class Phone {
private List<Call> calls = new ArrayList<>();
public Money calculateFee() {
Money result = Money.ZERO;
for (Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return result;
}
protected abstract Money calculateCallFee(Call call);
protected abstract Money afterCalculated(Money fee);
}
자식 클래스는 이 afterCalculated 를 이용하여 계산된 요금에 적용할 작업을 추가한다. 기본 정책
클래스들은 따로 적용할 작업이 없기 때문에 모두 그저 전달된 요금을 반환토록 구현한다.
public class RegularPhone extends Phone {
// ...
@Override
protected Money afterCalculated(Money fee) {
return fee;
}
}
public class NightlyDiscountPhone extends Phone {
// ...
@Override
protected Money afterCalculated(Money fee) {
return fee;
}
}
위 코드에서 알 수 있듯 부모 클래스에 추상 메서드를 추가하면 자식 클래스들이 모두 그 메서드를
오버라이딩해야 하는 문제가 발생한다. 모든 추상 메서드의 구현이 동일하다는 사실도 존재한다.
유연성은 유지하면서도 중복 코드를 제거할 수 있는 방법은 Phone 에서 afterCalculated 에 대한
기본 구현을 함께 제공하는 것이다.
public abstract class Phone {
// ...
protected Money afterCalculated(Money fee){
return fee;
}
protected abstract Money calculateCallFee(Call call);
}
추상 메서드와 훅 메서드
OCP를 만족하는 설계를 구성할 수 있는 방법 중 하나는 부모 클래스에 새 추상 메서드를 추가하고 부모 클래스의 다른 메서드 내에서 호출하는 것이다. 자식 클래스는 추상 클래스를 오버라이딩하고 자신만의 로직을 구현해서 부모 클래스에서 정의한 플로우에 개입할 수 있게 된다.
추상 메서드의 단점은 상속 계층에 속하는 모든 자식 클래스가 해당 메서드를 오버라이딩해야
한다는 것인데, 메서드에 기본 구현을 제공하는 방법으로 이를 극복할 수 있다. 이처럼 오버라이딩을 위해 추가되었으나 기본 구현을 제공하는 메서드를 훅 메서드 라고 부른다.
TaxableRegularPhone 은 다음과 같이 수정할 수 있다.
public class TaxableRegularPhone extends RegularPhone {
private double taxRate;
public TaxableRegularPhone(Money amount, Duration seconds, double taxRate) {
super(amount, seconds);
this.taxRate = taxRate;
}
// ...
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRate));
}
}
NightlyDiscountPhone 에도 세금을 부과할 수 있도록 TaxableNightlyDiscountPhone 을 추가하자
public class TaxableNightlyDiscountPhone extends NightlyDiscountPhone {
private double taxRate;
public TaxableNightlyDiscountPhone(Money nightlyAmount, Money regularAmount,
Duration seconds, double taxRate) {
super(nightlyAmount, regularAmount, seconds);
this.taxRate = taxRate;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRate));
}
}
다음과 같은 구조에서 문제는 TaxableNightlyDiscountPhone 과 TaxableRegularPhone 사이에
코드를 중복했다는 것이다.
이번엔 부가 정책인 기본 요금 할인 정책을 Phone 의 상속 계층에 추가해보자. 이 정책은 매달
청구되는 요금에서 고정된 요금을 차감하는 부가 정책을 가리킨다.
일반 요금제와 기본 요금 할인 정책을 조합하고 싶다면 RegularPhone 을 상속받는 RateDiscountableRegularPhone 클래스를 추가하면 된다.
public class RateDiscountableRegularPhone extends RegularPhone {
private Money discountAmount;
public RateDiscountableRegularPhone(Money amount, Duration seconds, Money discountAmount) {
super(amount, seconds);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}
심야 할인 요금제와 기본 요금 할인 정책을 조합하고 싶다면 NightlyDiscountPhone 을 상속받는
RateDiscountableNightlyDiscountPhone 클래스를 추가하면 된다.
public class RateDiscountableNightlyDiscountPhone extends NightlyDiscountPhone {
private Money discountAmount;
public RateDiscountableNightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds,
Money discountAmount) {
super(nightlyAmount, regularAmount, seconds);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}
이번에도 부가 정책을 구현한 클래스 사이 중복 코드가 추가되었다.
부가 정책은 자유롭게 조합할 수 있어야 하고 적용 순서 역시 임의로 결정할 수 있어야 한다. 상속을 이용한 해결 방법은 모든 가능한 조합별로 자식 클래스를 하나씩 추가하는 것이다. 만약 일반
요금제의 계산 결과에 세금 정책을 조합한 후 기본 요금 할인 정책을 추가하고 싶다면 TaxableRegularPhone 을 상속받는 TaxableAndRateDiscountableRegularPhone 을 추가해야 한다.
public class TaxableAndRateDiscountableRegularPhone extends TaxableRegularPhone {
private Money discountAmount;
public TaxableAndRateDiscountableRegularPhone(Money amount, Duration seconds, double taxRate,
Money discountAmount) {
super(amount, seconds, taxRate);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return super.afterCalculated(fee).minus(discountAmount);
}
}
표준 요금제에 기본 요금 할인 정책을 먼저 적용한 후 세금을 나중에 부과하고 싶다면 RateDiscountableRegularPhone 을 상속받는 RateDiscountableAndTaxableRegularPhone 을
추가하면 된다.
public class RateDiscountableAndTaxableRegularPhone extends RateDiscountableRegularPhone {
private double taxRate;
public RateDiscountableAndTaxableRegularPhone(Money amount, Duration seconds,
Money discountAmount, double taxRate) {
super(amount, seconds, discountAmount);
this.taxRate = taxRate;
}
@Override
protected Money afterCalculated(Money fee) {
return super.afterCalculated(fee).plus(fee.times(taxRate));
}
}
조합 가능한 모든 클래스를 위한 같이 구현할 경우 아래와 같은 상속 계층이 형성될 수 있다.

이처럼 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스가 추가되
경우를 가리켜 클래스 폭발 문제 또는 조합의 폭발 문제라고 부른다. 자식 클래스가 부모 클래스의 구현에 강하게 결합되도록 강요하는 상속의 근본적인 한계 때문에 발생하는 문제다.
이 문제를 해결할 수 있는 최선의 방법은 상속을 포기하는 것이다.
합성은 컴파일타임 관계를 런타임 관계로 변경함으로써 클래스 폭발 문제를 해결한다. 합성을
사용하면 구현 시점에 정책들의 관계를 고정시킬 필요가 없으며 실행 시점에 정책들의 관계를
유연하게 변경할 수 있게 된다. 상속이 조합의 결과를 개별 클래스 안으로 밀어 넣는 방법이라면
합성은 조합 구성 요소들을 개별 클래스로 구현한 후 실행 시점에 인스턴스를 조립하는 방법을
사용하는 것이라고 할 수 있다.
대부분의 경우 단순한 설계가 정답이지만 변경에 따르는 고통이 복잡성으로 인한 혼란을 넘어서고 있다면 합성을 이용해 유연성에 중점을 두는 것이 현명한 판단이다.
가장 먼저 해야 할 일은 각 정책을 별도 클래스로 구현하는 것이다. 먼저 기본 정책과 부가 정책을
포괄하는 RatePolicy 인터페이스를 추가하자.
public interface RatePolicy {
Money calculateFee(Phone phone);
}
기본 정책부터 구현하자. 일반 요금제와 심야 할인 요금제는 개별 요금 계산 부분을 제외하면 전체 처리 로직이 거의 동일하다. 이 중복 코드를 담을 추상 클래스 BasicRatePolicy 를 추가하자.
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);
}
일반 요금제를 구현한 RegularPolicy 를 추가하자
public class RegularPolicy extends BasicRatePolicy {
private Money amount;
private Duration seconds;
public RegularPolicy(Money amount, Duration seconds) {
this.amount = amount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
심야 할인 요금제를 구현하는 NightlyDiscountPolicy 클래스 역시 유사하게 구현할 수 있다.
public class NightlyDiscountPolicy extends BasicRatePolicy {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
public NightlyDiscountPolicy(Money nightlyAmount, Money regularAmount, Duration seconds) {
this.nightlyAmount = nightlyAmount;
this.regularAmount = regularAmount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
기본 정책을 이용해 요금을 계산할 수 있게 Phone 을 수정하자
public class Phone {
private RatePolicy ratePolicy;
private List<Call> calls = new ArrayList<>();
public Phone(RatePolicy ratePolicy) {
this.ratePolicy = ratePolicy;
}
public List<Call> getCalls() {
return Collections.unmodifiableList(calls);
}
public Money calculateFee() {
return ratePolicy.calculateFee(this);
}
}
Phone 의 경우처럼 다양한 종류의 객체와 협력하기 위해 합성 관계를 사용하는 경우 합성하는
객체의 타입을 인터페이스나 추상 클래스로 선언하고 의존성 주입을 사용해 런타임에 필요한
객체를 설정할 수 있도록 구현하는 것이 일반적이다.

일반 요금제에 따라 요금을 계산하고 싶다면 Phone 과 RegularPolicy 인스턴스를 합성하면 된다.
Phone phone = new Phone(new RegularPolicy(Money.wons(10), Duration.ofSeconds(10));
심야 할인 요금제의 규칙에 따라 통화 요금을 계산하고 싶다면 Phone 과 NightlyDiscountPolicy 의
인스턴스를 합성하면 된다.
Phone phone = new Phone(new NightlyDiscountPolicy(Money.wons(5),
Money.wons(10, Duration.ofSeconds(10)));
이제 부가 정책을 추가해보자. 만약 일반 요금제에 기본 요금 할인 정책을 적용한 후 세금 정책을
적용해야 한다면 아래와 같은 순서로 인스턴스들을 연결해야 한다.

이 그림은 다음 제약에 따라 부가 정책을 구현해야 한다는 사실을 보여준다.
Phone 입장에서는 자신이 어떤 정책(기본, 부가)의 인스턴스에게 메시지를 전송하고 있는지를 몰라야 한다. 다시 말해 기본, 부가 정책은 협력 내에서 동일 ‘역할’을 수행해야 한다. 이는 부가RatePolicy 인터페이스를 구현해야 한다는 것을 의미한다.부가 정책을 AdditionalPolicy 추상 클래스로 구현하자
public abstract class AdditionalPolicy implements RatePolicy {
private RatePolicy next;
public AdditionalPolicy(RatePolicy next) {
this.next = next;
}
@Override
public Money calculateFee(Phone phone) {
Money fee = next.calculateFee(phone);
return afterCalculated(fee);
}
protected abstract Money afterCalculated(Money fee);
}
다른 요금 정책과 조합될 수 있도록 RatePolicy 타입의 next 라는 인스턴스 변수를 내부에
포함한다.
먼저 세금 정책부터 구현하자.
public class TaxablePolicy extends AdditionalPolicy {
private double taxRatio;
public TaxablePolicy(RatePolicy next, double taxRatio) {
super(next);
this.taxRatio = taxRatio;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRatio));
}
}
기본 요금 할인 정책을 구현하는 것도 간단하다.
public class RateDiscountablePolicy extends AdditionalPolicy {
private Money discountAmount;
public RateDiscountablePolicy(RatePolicy next, Money discountAmount) {
super(next);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}
다양한 방식으로 정책을 조합할 수 있는 설계가 준비되었다. 남은 일은 원하는 정책 인스턴스를
생성한 후 의존성 주입을 통해 다른 정책의 인스턴스에 전달하는 것뿐이다.
// 일반 요금제 + 세금 정책
Phone phone = new Phone(new TaxablePolicy(0.05, new RegularPolicy(...)));
// 일반 요금제 + 기본 할인 정책 + 세금 정책
Phone phone = new Phone(
new TaxablePolicy(0,05,
new RateDiscountablePolicy(Money.wons(1000),
new RegularPolicy(...))));
상속 기반 설계에서는 새 부가 정책 추가를 위해 상속 계층에 필요 이상의 클래스를 추가해야 했다. 합성 기반 설계에서는 이 변경을 간단히 반영할 수 있다. 이를테면 고정 요금제가 필요할 경우 고정 요금제를 구현한 클래스 ‘하나’만 추가한 후 원하는 방식으로 조합하면 된다.

오직 하나의 클래스만 추가하고 런타임에 필요한 정책들을 조합해서 원하는 기능을 얻을 수 있다.
설계를 필요한 조합의 수만큼 매번 새 클래스를 추가해야 했던 상속과 비교하면 훨씬 수월하다.
더 중요한 것은 요구사항 변경시 오직 하나의 클래스만 수정해도 된다는 것이다. 세금 정책을
변경한다고 가정했을 경우 TaxablePolicy 클래스 하나만 변경하면 된다. SRP를 준수하고 있는
것이다.
상속은 부모 클래스의 세부 구현에 자식 클래스가 강하게 결합되어 코드의 진화를 방해한다. 코드를 재사용하면서도 건전한 결합도를 유지할 수 있는 방법은 합성을 사용하는 것이다. 상속은 구현을
재사용하는 데 비해 합성은 객체의 인터페이스를 재사용한다.
믹스인 은 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법을 가리키는
용어다. 합성이 실행 시점에 객체를 조합하는 재사용 방법이라면 믹스인 컴파일 시점에 필요한 코드 조각을 조합하는 재사용 방법이다. 방법이 무엇이건 코드를 다른 코드 안에 유연하게 섞어 넣을 수 있다면 믹스인이라고 부를 수 있다.
여기서는 스칼라 언어에서 제공하는 트레이트(trait) 를 이용해 믹스인을 구현해본다.
기본 정책인 BasicRatePolicy 는 추상 클래스로 구현한다.
abstract class BasicRatePlicy {
def calculateFee(phone: Phone): Money =
phone.calls.map(calculateCallFee(_)).reduce(_+_)
protected def calculateCallFee(call: Call): Money;
}
RegularPolicy 는 BasicRatePolicy 를 상속받아 calculateCallFee 를 오버라이딩한다.
class RegularPolicy(val amount: Money, val seconds: Duration) extends BasicRatePlicy {
override protected def calculateCallFee(call: Call): Money =
amount * (call.duration.getSeconds / seconds.getSeconds)
}
NightlyDiscountPolicy 역시 마찬가지로 구현한다.
class NightlyDiscountPolicy(
val nightlyAmount: Money,
val regularAmount: Money,
val seconds: Duration) extends BasicRatePolicy {
override protected def calculateCallFee(call: Call): Money =
if (call.from.getHour >= NightltDiscountPolicy.LateNightHour) {
nightlyAmount * (call.duration.getSeconds / seconds.getSeconds)
} else {
regularAmount * (call.duration.getSeconds / seconds.getSeconds)
}
}
object NightltDiscountPolicy {
val LateNightHour: Integer = 22
}
스칼라에서는 다른 코드와 조합해서 확장할 수 있는 기능을 트레이트로 구현할 수 있다. 트레이트로 구현된 기능들을 섞어 넣게 될 대상은 기본 정책에 해당하는 RegularPolicy , NightlyDiscountPoilcy 이다.
먼저 부가 정책 중 세금 정책에 해당하는 TaxablePolicy 트레이트를 구현해보자.
trait TaxablePolicy extends BasicRatePolicy {
val taxRate: Double
override def calculateFee(phone: Phone): Money = {
val fee = super.calculateFee(phone)
return fee + fee * taxRate
}
}
위 코드에서 트레이트가 BasicRatePolicy 를 확장하는 것은, TaxablePolicy 가 BasicRatePolicy 나 BasicRatePolicy 의 자손에 해당되는 경우에만 믹스인될 수 있다는 의미이다.
유의할 점은 extends 문은 상속의 개념이 아닌 단순히 TaxablePolicy 가 사용될 문맥을 제한할
뿐이라는 것이다. 따라서 RegularPolicy , NightlyDiscountPolicy 더 나아가 미래에 추가될 BaseRatePolicy 의 자손에게도 믹스인될 수 있고 이외 클래스나 트레이트에는 믹스인될 수 없다.
이 사실에서 상속은 정적이지만 믹스인은 동적이라는 차이를 발견할 수 있다. 실제로 트레이트를
믹스인하는 시점에 가서야 믹스인할 대상을 결정할 수 있다.
같은 맥락에서 super 호출로 실행되는 calcualteFee 메서드를 보관한 코드는 실제로 트레이트가 믹스인되는 시점에 결정된다. 이 말은 super 의 참조 대상이 컴파일이 아닌 실행 시점에 결정된다는 것을 의미한다. 상속의 경우 super 참조는 컴파일 시점에 결정된다.
이런 면에서 믹스인은 상속보다는 합성과 유사하다.
다음 부가 정책을 RateDiscountablePolicy 트레이트로 구현하자.
trait RateDiscountablePolicy extends BasicRatePolicy {
val discountAmount: Money
override def calculateFee(phone: Phone): Money = {
val fee = super.calculateFee(phone)
fee - discountAmount
}
}
스칼라는 트레이트를 클래스나 다른 트레이트와 믹스인할 수 있도록 extends 와 with 를
제공한다. 믹스인하려는 대상 클래스의 부모 클래스가 존재하는 경우 부모 클래스는 extends 를
이용해 상속받고 트레이트는 with 를 이용해 믹스인해야 한다. 이를 트레이트 조합 이라고 부른다.
먼저 표준 요금제에 세금 정책을 조합해보자.
class TaxableRegularPolicy(amount: Money, seconds: Duration, val taxRate: Double)
extends RegularPolicy(amount, seconds)
with TaxablePolicy
스칼라는 특정 클래스에 믹스인한 클래스와 트레이트를 선형화(linearization)해서 어떤 메서드를
호출할지 결정한다. 클래스의 인스턴스를 생성할 때 클래스 자신, 조상 클래스, 트레이트를 일렬로
나열하여 순서를 정하고 실행 중인 메서드 내부에서 super 호출을 하면 다음 단계에 위치한
클래스나 트레이트의 메서드가 호출된다.
위 코드의 경우 TaxableRegularPolicy -> TaxablePolicy(trait) -> RegularPolicy 순서이다. 중요한 것은 믹스인되기 전까지는 상속 계층 안에서 TaxablePolicy 트레이트의 위치가 결정되지 않는다는 것이다. 이 위치는 어떤 클래스를 믹스인할지에 따라 동적으로 변한다.

이번엔 임의의 순서에 따라 부가 정책을 조합하는 경우를 생각해보자. 표준 요금제에 세금 정책을
적용한 후에 비율 할인 정책을 적용하는 경우를 살펴보자.

위와 같이 선형화를 이용한 구조를 만들기 위해선 다음과 같이 코드를 구성할 수 있다.
class RateDiscountableAndTaxableRegularPolicy(
amount: Money,
seconds: Duration,
val discountAmount: Money,
val taxRate: Double)
extends RegularPolicy(amount, seconds)
with TaxablePolicy
with RateDiscountablePolicy
트레이트는 선형화할 때 자신을 맨 앞에 두고 오른쪽부터 트레이트를 쌓아올린다. 만약 표준
요금제에 비율 할인 정책을 적용한 후 세금 정책을 적용하고 싶다면 트레이트 순서만 바꾸면 된다.
class TaxableAndRateDiscountableRegularPolicy(
amount: Money,
seconds: Duration,
val discountAmount: Money,
val taxRate: Double)
extends RegularPolicy(amount, seconds)
with RateDiscountablePolicy
with TaxablePolicy
어떤 사람들은 믹스인을 사용하더라도 클래스 폭발 문제는 여전히 남아있느 것이 아니냐고 반문할 수 있다. 사실 그 문제의 단점은 클래스가 늘어나는 것이 아닌, 클래스 증가에 중복 코드가
기하급수적으로 동반된다는 것이다. 믹스인은 이런 문제가 발생하지 않는다.
클래스를 만들지 않고 인스턴스 생성시 트레이트를 믹스인할 수도 있다.
new RegularPlicy(Money(100), Duration.ofSeconds(10))
with RateDiscountablePolicy
with TaxablePolicy {
val discountAmount = Money(100)
val taxRate = 0.02
}
이 방법은 일회성으로 필요한 경우 사용할 수 있다.
믹스인은 상속 계층 안에서 확장한 클래스보다 더 하위에 위치하게 된다. 다시 말해 믹스인은 대상 클래스의 자식 클래스처럼 사용될 용도로 만들어지는 것이다. 따라서 믹스인을 추상 서브클래스라고 부르기도 한다.
믹스인을 사용하면 특정 클래스에 대한 변경 또는 확장을 독립적으로 구현한 후 필요 시점에
차례대로 추가할 수 있다. 믹스인의 이러한 특징을 쌓을 수 있는 변경(stackable modification)이라고 부른다.