본문 바로가기

Study/오프젝트

11) 합성과 유연한 설계

1. 상속과 합성의 차이

상속

  • 부모 클래스와 자식 클래스 사이의 의존성은 컴파일 타임에 해결된다.
  • is-a 관계
  • 클래스 사이의 정적인 관계
    • 코드 작성 시점에 상속 관계 변경 불가
  • 부모 클래스 안에 구현된 코드 자체를 재사용

합성

  • 의존성은 런타임에 해결되며 구현에 의존하지 않고 퍼블릭 인터페이스에 의존한다.
  • has-a 관계
  • 객체 사이의 동적인 관계
    • 실행 시점에 동적으로 변경 가능
  • 객체의 퍼블릭 인터페이스를 재사용

 

2. 상속 관계를 합성 관계로 변경하기

이번 장에서는 10장에서 구현했던 코드를 통해 상속의 문제점을 알아보고 상속으로 구현한 코드를 합성을 통해 변경에 유연한 코드로 전환하는 장을 설명한다. 우선 상속을 통해 코드를 구현을 하면 안 되는 이유를 알아보자.

 

중복 코드의 덫에 걸린다. 만약 부가 정책 또는 추가 정책 요구 사항이 추가 될 경우 상속을 이용해 모든 가능한 조합별로 자식 클래스를 하나씩 추가하는 방법으로 진행한다. 하지만 여러 정책에 따라 조합으로 여러 자식 클래스의 코드를 구현할 경우 중복 코드의 덫에 걸린다. 그렇게 살펴보면 부모 코드와 자식 코드가 별로 차이가 없다는 것을 확인할 수 있다. 밑에 11.6 그림처럼 상속을 통해 많은 클래스가 구현돼서 나오지만 중복이 비슷해 코드를 작성하지는 않겠다.

이렇게 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가리켜 클래스 폭발, 조합의 폭발 문제라고 부른다.

 

자! 이제 합성 관계로 변경에 유연한 설계로 바꿔보자. 우선 정책을 별도의 클래스로 구현하자. 여러 정책들을 연결할 수 있도록 하자. 기본 정책과 부가 정책을 포괄하는 RatePolicy 인터페이스를 추가한다.

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

 

기본 정책을 구현하자. 요금제를 계산하는 중복 코드를 담을 추상 클래스 BasicRatePolicy를 구현하자.

public abstract class BasicRatePolicy implements RatePolicy {
    @Override
    public long calculateFee(Phone phone) {
        int fee = 0;

        for (Call call : phone.getCalls()) {
            fee += calculateCallFee(call);
        }

        return fee;
    }

    protected abstract long calculateCallFee(Call call);
}

 

추상 메서드 claculateCallFee를 오버라이딩해서 각자 방식에 따라 요금을 계산하는 메서드를 만들면 된다. 일반 요금제인 RegularPolicy와 심야 할인 요금제 NightlyDiscountPolicy를 구현해 보자.

public class RegularPolicy extends BasicRatePolicy {
    private final long amount;
    private final Duration seconds;

    public RegularPolicy(long amount, Duration seconds) {
        this.amount = amount;
        this.seconds = seconds;
    }

    @Override
    protected long calculateCallFee(Call call) {
        return amount * (call.getDuration().getSeconds() / seconds.getSeconds());
    }
}
public class NightlyDiscountPolicy extends BasicRatePolicy {
    private static final int LATE_NIGHT_HOUR = 22;

    private final long nightlyAmount;
    private final long regularAmount;
    private final Duration seconds;

    public NightlyDiscountPolicy(long nightlyAmount, long regularAmount, Duration seconds) {
        this.nightlyAmount = nightlyAmount;
        this.regularAmount = regularAmount;
        this.seconds = seconds;
    }

    @Override
    protected long calculateCallFee(Call call) {
        if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
            return nightlyAmount * (call.getDuration().getSeconds() / seconds.getSeconds());
        }

        return regularAmount * (call.getDuration().getSeconds() / seconds.getSeconds());
    }
}

 

이제 기본 정책을 이용해 요금을 계산하는 Phone을 구현하자

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

    public Phone(RatePolicy ratePolicy) {
        this.ratePolicy = ratePolicy;
    }

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

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

 

Phone 코드 내부에는 RatePolicy를 생성자를 통해 인스턴스에 대한 의존성을 주입받는다. 이것이 합성이다. 퍼블릭 인터페이스에 의존하도록 설계하자. 지금 모습은 아래와 같이 의존성 주입을 통해 런타임에 필요한 객체를 설정하도록 구현되어 있다.

 

여기서 만약 추가 요구 사항을 적용하게 된다면 기존 상속처럼 중복된 코드를 작성을 해야 할까? 정답은 당연히 아니다. 우선 설명에 앞서 실제 코드를 작성해 보자.. 부가 정책을 나타내는 AdditionalRatePolicy를 추상 클래스로 구현하자.

public abstract class AdditionalRatePolicy implements RatePolicy {
    private RatePolicy nextRatePolicy;

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

    @Override
    public long calculateFee(Phone phone) {
        long fee = nextRatePolicy.calculateFee(phone);

        return afterCalculated(fee);
    }

    protected abstract long afterCalculated(long fee);
}

 

이제 세금 정책 TaxablePolicy을 추가구현 해주고 기본요금 할인 정책 RateDiscountablePolicy 도 추가 해주자.

public class TaxablePolicy extends AdditionalRatePolicy {
    private final double taxRage;

    public TaxablePolicy(RatePolicy nextRatePolicy, double taxRage) {
        super(nextRatePolicy);
        this.taxRage = taxRage;
    }

    @Override
    protected long afterCalculated(long fee) {
        return (long) (fee + (fee * taxRage));
    }
}
public class RateDiscountablePolicy extends AdditionalRatePolicy {
    private long discountAmount;

    public RateDiscountablePolicy(RatePolicy nextRatePolicy, long discountAmount) {
        super(nextRatePolicy);
        this.discountAmount = discountAmount;
    }

    @Override
    protected long afterCalculated(long fee) {
        return fee - discountAmount;
    }
}

 

추가 요구 사항을 수행했음에도 불구하고 다른 코드는 손대지 않고 추가 기능 코드만 추가를 하면 바로 원하는 결과를 얻을 수 있다. 이전 상속에서의 부가 정책을 추가하기 위해서는 불필요할 정도로 많은 클래스를 추가를 했어야 했지만 합성을 기반으로 한 설계에서는 간단하게 해결할 수 있다. 예를 들어 고정 요금제가 필요하다면 고정 요금제 클래스 하나만 추가 구현하면 끝이다.

 

'Study > 오프젝트' 카테고리의 다른 글

부록 A) 계약에 의한 설계  (0) 2024.02.01
12) 다형성  (1) 2024.01.25
10) 상속과 코드 재사용  (0) 2024.01.10
09) 유연한 설계  (1) 2024.01.06
08) 의존성 관리하기  (0) 2024.01.02