11장: 합성과 유연한 설계

상속과 합성

  • 상속 관계는 is-a 관계라고 부르고 합성 관계는 has-a 관계로 나타낼 수 있다.

  • 상속은 부모 클래스 안에 구현된 코드 자체를 재사용하지만 합성은 포함되는 객체의 퍼블릭 인터페이스를 재사용한다.

    • 상속은 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에 결합도가 높아진다는 단점이 있다.

    • 합성은 부모 객체의 내부 구현이 아니라 퍼블릭 인터페이스에 의존하므로 결합도가 낮다.

  • 코드 재사용을 위해서는 객체 합성이 클래스 상속보다 더 좋은 방법이다.

  • 상속을 합성으로 바꾸면서 로직의 변경이 필요 없는 부모 클래스의 퍼블릭 인터페이스를 모두 제공하기 위해서 포워딩 메서드를 사용할 수 있다.

    • 인터페이스를 구현할 때 오버라이드 하는 메서드 내에서 내부 인스턴스의 메서드를 그대로 호출할 때 이를 포워딩 메서드라고 부른다.

    • 기존 클래스의 인터페이스를 그대로 외부에 제공하면서 구현에 대한 결합 없이 일부 작동 방식을 변경하고 싶은 경우에 사용할 수 있는 유용한 기 법이다.

  • 상속을 합성으로 바꾸더라도 자식 클래스의 요구사항에 맞게 부모 클래스도 변경해야 하는 파급 효과가 있을 수 있다. 이 때 몽키 패치라는 방식을 사용할 수도 있다.

    • 현재 실행 중인 환경에만 영향을 미치도록 지역적으로 코드를 수정하거나 확장하는 것

    • Playlist의 코드를 수정할 권한이 없거나 소스코드가 존재하지 않는다고 하더라도 몽키 패치가 지원되는 환경이라면 Playlist에 직접 remove 메서드를 추가하는 것이 가능하다.

    • 자바는 언어 차원에서 몽키 패치를 지원하지 않기 때문에 바이트코드를 직접 변환하거나 AOP(Aspect-Oriented Programming)를 이용해야 한다.

  • 훅 메서드

    • 개방 폐쇄 원칙을 만족시키기 위해 부모 추상 클래스에 새로운 추상 메서드를 추가할 경우 모든 자식 클래스가 이를 구현해야 한다. 만약 여러 자식 클래스에서 구현이 중복될 경우, 편의를 위해 기본 구현이 되어있는 훅 메서드를 제공할 수 있다.

클래스 폭발 문제 해결하기

  • 상속을 이용해 핸드폰 이용 요금에 여러 할인/세금 정책을 붙이려면 아래와 같이 정책마다 자식 클래스를 구현해야 한다. 여러 세금 정책 간의 조합이 필요하다면 해당 조합을 위해 또 다른 자식 클래스를 만들어야 할 것이다.

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

  • 이러한 문제의 원인은 컴파일 타임에 결정된 자식 클래스와 부모 클래스의 결합이다.

  • 상속을 합성으로 변경하면 컴파일 타임 의존성을 런타임 의존성으로 변경하여 클래스 폭발 문제를 해결할 수 있다.

  • 일단은 Phone을 상속받아 구현하는 RegularPhone/NightlyDiscountPhone을 합성으로 변경한다. 즉, Phone 클래스 내부에서 RatePolicy라는 인터페이스 필드를 가지게 된다.

  • 그리고 부가적으로 적용되는 할인/세금 정책을 적용하기 위해 AdditionalRatePolicy라는 추상 클래스를 만들고 내부에서 함께 적용할 다른 정책을 참조하도록 한다.

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 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 RateDiscountablePolicy(Money.wons(1000),
		new TaxablePolicy(0.05, 
			new NightlyDiscountPolicy(...)));
  • 합성을 이용하면 아래와 같이 조합과 변경에 용이한 다이어그램이 나오게 된다.

믹스인

  • 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법

  • 합성이 실행 시점에 객체를 조합하는 재사용 방법이라면 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 재사용 방법이다.

  • 자식클래스를 부모 클래스와 동일한 개념적인 범주로 묶어 is-a 관계를 만드는 것이 목적이다.

  • 스칼라에서는 다른 코드와 조합해서 확장할 수 있는 기능을 트레이트로 구현할 수 있다.

  • 아래는 BasicRatePolicy나 BasicRatePolicy을 상속받은 경우에만 믹스인될 수 있는 TaxablePolicy이다.

trait TaxablePolicy extends BasicRatePolicy {
	def taxRate: Double
	override def calculateFee(phone: Phone): Money = { 
		val fee = super.calculateFee(phone)
		return fee + fee * taxRate
	} 
}
  • 아래는 기본 요금 정책에 부가 세금 정책을 믹스인 한 클래스이다. 스칼라는 믹스인한 클래스와 트레이트를 선형화하여, 가장 마지막에 정의한 트레이트부터 점점 super를 호출하여 클래스 자신과 부모 클래스 메서드가 호출되게 만든다.

class TaxableRegularPolicy(
	amount: Money,
	seconds: Duration,
	val taxRate: Double)
extends RegularPolicy(amount, seconds)
with TaxablePolicy
with RateDiscountablePolicy
  • 상속은 부모 클래스와 자식 클래스의 관계를 코드를 작성하는 시점에 고정시켜 버리지만(정적) 믹스인은 제약을 둘 뿐 실제로 어떤 코드에 믹스인될 것인지를 결정하지 않는다(동적).

  • 클래스 폭발 문제의 단점은 클래스가 늘어난다는 것이 아니라 클래스가 늘어날수록 중복 코드도 함께 기하급수적으로 늘어난다는 점이다. 믹스인에는 이런 문제가 발생하지 않는다.

  • 믹스인은 항상 상속 계층의 하위에 위치하게 되어 추상 서브클래스라고도 불린다.

  • 믹스인을 사용하면 특정 클래스에 대한 변경/확장을 독립적으로 구현한 후 필요한 시점에 차례로 쌓을 수 있는 변경(stackable modification)이라는 특징을 가진다.

  • 이펙티브 자바에서는 믹스인을 제공하는 대표적인 예로 Comparable을 든다. Comparable 인터페이스를 각 클래스에서 구현해야 한다는 점은 스칼라의 trait와 다르지만, 여러 클래스에서 공통된 기능을 제공한다는 측면에서 보면 믹스인의 일종이라고 느껴지기도 한다.

Last updated