스터디/오브젝트
오브젝트 - 11장 합성과 유연한 설계
realizers
2022. 10. 28. 21:08
728x90
반응형
서론
- 상속은 부모 클래스와 자식 클래스 사이의 의존성이 컴파일 시점에 고정되어 높은 결합도를 가지지만 합성은 두 객체 사이의 의존성이 런타임시점에 고정되어 낮은 결합도를 가지게 됩니다.
- 상속과 합성은 코드 재사용이라는 동일한 목적을 가지지만 구현 방법부터 변경을 위해 다루는 방식 모두에서 차이점을 가집니다.
- 상속을 사용하면 자식 클래스가 부모 클래스의 내부 구현까지 자세히 알아야하기 때문에 결합도가 높아지고 캡슐화에 약해집니다.
반면 합성을 사용하면 오직 퍼블릭 인터페이스에만 의존하므로 내부 구현이 변경되더라도 영향을 최소화할 수 있으며 캡슐화를 지킬 수 있습니다.
상속과 합성은 재사용의 대상이 다르다
- 상속과 합성은 재사용의 대상이 다릅니다. 상속은 부모 클래스의 내부 구현을 재사용하지만 합성은 퍼블릭 인터페이스를 재사용합니다. 따라서 상속 대신 합성을 사용하면 구현에 대한 의존성을 퍼블릭 인터페이스에 대한 의존성으로 변경할 수 있으며 이는 캡슐화와
낮은 결합도를 유지할 수 있습니다.
화이트 박스 재사용
- 화이트 박스 재사용은 상속의 관점으로부터 나온 말입니다. 상속은 부모 클래스를 자식 클래스에서 재정의할 수 있으므로 화이트 박스 재사용이라 불리는데 그 이유는 가시성 때문입니다. 상속을 받게 되면 부모 클래스의 내부를 자식 클래스에게 공개해야 하기 때문입니다.
블랙 박스 재사용
- 블랙 박스 재사용은 합성의 관점으로부터 나온 말입니다. 객체를 합성하기 위해서는 추상화를 이용하며 추상화에 선언된 퍼블릭 인터페이스를 통해 각 구현체마다 내부 구현이 다를 테고 이로 인해 객체의 내부는 공개하지 않고 오직 퍼블릭 인터페이스를 통해서만 재사용할 수 있기 때문입니다.
상속으로 인한 클래스 폭발
💡 상황 가정
- 요금을 계산하기 위해서 기본 정책과 부가 정책을 적용할 수 있습니다.
- 기본 정책의 계산 결과에 적용됩니다. 이는 기본 정책 이후에 부과 정책을 적용할 수 있음을 나타냅니다.
- 선택적으로 적용할 수 있습니다. 기본 정책의 계산 결과에 부가 정책을 적용할수도 있고 적용하지 않을 수도 있습니다.
- 조합이 가능합니다. 기본 정책의 계산 결과에 세금 정책만 적용할 수도 있고 모두 적용할 수도 있습니다.
- 부가 정책은 임의의 순서로 적용할 수 있습니다. 이는 세금 정책 적용 후 기본 할인 정책을 할수있고 또는 반대로 할 수 있습니다.
💡 예제 코드
- 아래 예제 코드에서는 Phone 이라는 추상 클래스를 만들어 변하는 부분과 변하지 않는 부분을 파악하여 문맥에 따라 변하는 부분을
자식 클래스에서 구현하도록 강요하고 있습니다. 이렇게만 봤을때 문제될게 없습니다.
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);
}
// 일반 요금제
@AllArgsConstructor
public class RegularPhone extends Phone {
private Money amount;
private Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
// 심야 할인 요금제
@AllArgsConstructor
public class NightlyDiscountPhone extends Phone {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
if (call.getStartTime().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
🧨 기본 정책에 부가 정책 적용
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 Money afterCalculated(Money fee) {
return fee;
}
abstract protected Money calculateCallFee(Call call);
}
// 일반 요금제
@AllArgsConstructor
public class RegularPhone extends Phone {
private Money amount;
private Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
// 심야 할인 요금제
@AllArgsConstructor
public class NightlyDiscountPhone extends Phone {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
if (call.getStartTime().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
// 일반 요금제 + 세금 정책
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));
}
}
// 일반 요금제 + 기본 요금 할인 정책
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);
}
}
// 심야 할인 요금제 + 세금 정책
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));
}
}
// 심야 할인 요금제 + 기본 요금 할인 정책
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);
}
}
🧨 상속으로 인한 문제점
- 자식 클래스의 생성자에서 super를 호출하기 때문에 결합도가 높아집니다.
- 훅 메서드를 사용하여 중복을 줄였지만 그래도 어느정도의 afterCalculated(훅 메서드) 메서드에서 중복이 발생할 수 밖에 없습니다.
- 부가 정책은 임의의 순서로 조합할 수 있으므로 어떻게 조합하느냐에따라 클래스의 수가 증가할 수 있습니다. 이는 클래스 폭발 문제
또는 조합의 폭발 문제를 야기시킬 수 있습니다. - 클래스 폭발 문제는 자식 클래스가 부모 클래스의 내부 구현에 강하게 결합되도록 강요하는 상속의 근본적인 한계로 인해 발생하는 문제점입니다. 컴파일타임에 고정된 관계는 변경될 수 없기 때문에 다양한 조합이 필요한 상황에서 유일한 해결책은 필요한 다양한 조합만큼 클래스를 만드는 것뿐입니다.
합성관계로 리펙토링
- 합성은 컴파일타임 관계를 런타임 관계로 변경함으로써 문제를 해결할 수 있습니다. 합성을 사용하면 퍼블릭 인터페이스에만 의존할 수 있기 때문에 런타임시점에 유동적으로 객체를 변경할 수 있습니다.
💡 기본 정책 적용
- RatePolicy 인터페이스를 만들어 변하지 않는 부분을 파악하여 퍼블릭 인터페이스를 선언합니다.
- BasicRatePolicy 추상 클래스의 역할은 변하지 않는 부분을 기본적으로 제공함으로써 중복 코드를 줄이고 변하는 부분은 자식 클래스에서 구현할 수 있도록 만듭니다.
- RegularPolicy, NightlyDiscountPolicy 클래스는 일반 요금제와 심야 할인 요금제의 책임을 수행하고 있습니다.
- Phone 클래스에서 추상화에 의존할 수 있도록 RatePolicy 속성을 추가합니다. Phone 클래스는 생성자 주입을 통해 유동적으로 일반 요금제 또는 심야 할인 요금제를 선택할 수 있습니다.
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);
}
@AllArgsConstructor
public class RegularPolicy extends BasicRatePolicy {
private Money amount;
private Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
@AllArgsConstructor
public class NightlyDiscountPolicy extends BasicRatePolicy {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
@Override
protected Money calculateCallFee(Call call) {
if (call.getStartTime().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
@Getter
public class Phone {
private RatePolicy ratePolicy; // 추상화에 의존
private List<Call> calls = new ArrayList<>();
public Phone(RatePolicy ratePolicy) {
this.ratePolicy = ratePolicy;
}
public Money calculateFee() {
return ratePolicy.calculateFee(this);
}
}
💡 부가 정책 적용
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);
}
public class TaxablePolicy extends AdditionalRatePolicy {
private double taxRate;
public TaxablePolicy(double taxRate, RatePolicy next) {
super(next);
this.taxRate = taxRate;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.plus(fee.times(taxRate));
}
}
public class RateDiscountPolicy extends AdditionalRatePolicy {
private Money discountAmount;
public RateDiscountPolicy(Money discountAmount, RatePolicy next) {
super(next);
this.discountAmount = discountAmount;
}
@Override
protected Money afterCalculated(Money fee) {
return fee.minus(discountAmount);
}
}
public class Main {
public static void main(String[] args) {
Phone phoneA = new Phone(new TaxablePolicy(..., new RegularPolicy(...)));
Phone phoneB = new Phone(new RateDiscountPolicy(..., new TaxablePolicy(..., new RegularPolicy(...))));
Phone phoneC = new Phone(new RateDiscountPolicy(..., new TaxablePolicy(..., new NightlyDiscountPolicy(...))));
}
}
믹스인
- 믹스인은 객체를 생성할 때 코드 일부를 클래스의 안에 섞어 넣어 재사용하는 기법을 말합니다.
- 합성이 런타임 시점에 객체를 조합해서 재사용하는 방법이라면 믹스인은 컴파일 시점에 필요한 코드를 조합하는 재사용 방법입니다.
- 상속이 부모 클래스와 자식 클래스 사이의 관계를 고정시키는데 비해 믹스인은 유연하게 관계를 재구성할 수 있습니다.
합성과 믹스인
- 합성과 믹스인은 유사한 면이 있는데, 합성은 독립적으로 작성된 객체들을 런타임 시점에 조합해서 더 큰 기능을 만들어내고
믹스인은 독립적으로 작성된 트레이트와 클래스를 코드 컴파일 시점에 조합해 더 큰 기능을 만들어 낼 수 있습니다.
728x90
반응형