상속과 합성의 차이상속을 합성으로 변경하기불필요한 인터페이스 상속 문제 : java.util.Properties, java.util.Stack메서드 오버라이딩의 오작용 문제 : InstrumentedHashSet부모 클래스와 자식 클래스의 동시 수정 문제 : PersonalPlaylist상속으로 인한 조합의 폭발적인 증가합성 관계로 변경하기부가 정책 적용하기기본 정책과 부가 정책 합성하기새로운 정책 추가하기
상속과 합성의 차이
ㅤ | 상속 | 합성 |
재사용방법 | 부모 클래스와 자식 클래스를 연결해서 부모 클래스의 코드를 재사용 (is-a 관계) | 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용 (has -a 관계) |
결합도 | 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에 자식 클래스와 부모 클래스 사이의 결합도가 높아질 수 밖에 없다. | 구현에 의존하지 않는다는 점에서 상속과 다름. 합성은 내부에 포함되는 객체의 구현이 아닌 퍼블릭 인터페이스에 의존. 따라서 포함되는 객체의 내부 구현이 변경되더라도 영향을 최소화 할 수 있기에 결합도가 떨어짐 |
관계 | 클래스 사이의 정적 관계 (컴파일 의존)
컴파일타임 의존성 = 런타임 의존성 | 객체 사이의 동적인 관계 (런타임 의존성)
런타임에 동적으로 변경가능 |
의존성 | 구현에 대한 의존성 | 인터페이스에 대한 의존성 |
표현 방식 | 화이트박스 재사용 (부모 클래스의 내부가 자식 클래스에 공개되기에) | 블랙박스 재사용 (내부는 공개되지 않고 인터페이스를 통해서만 재사용되기에) |
[코드 재사용을 위해서는] 객체 합성이 클래스 상속보다 더 좋은 방법이다.
상속을 합성으로 변경하기
불필요한 인터페이스 상속 문제 : java.util.Properties, java.util.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); } } 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); } }
- 이렇게 구현하면 Properties와 Stack에 불필요한 operation이 노출되지 않음
메서드 오버라이딩의 오작용 문제 : InstrumentedHashSet
- 이 문제는 위의 문제와 조금 다른 것이 HashSet이 제공하는 퍼블릭 인터페이스를 그대로 제공하면서 기능을 추가한 것임. ⇒ HashSet에 대한 구현 결합도는 제거하면서 퍼블릭 인터페이스는 그대로 상속 받아야 함
public class InstrumentedHashSet<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; } @Override public boolean remove(Object o) { return set.remove(o); } @Override public void clear() { set.clear(); } @Override public boolean equals(Object o) { return set.equals(o); } @Override public int hashCode() { return set.hashCode(); } @Override public Spliterator<E> spliterator() { return set.spliterator(); } @Override public int size() { return set.size(); } @Override public boolean isEmpty() { return set.isEmpty(); } @Override public boolean contains(Object o) { return set.contains(o); } @Override public Iterator<E> iterator() { return set.iterator(); } @Override public Object[] toArray() { return set.toArray(); } @Override public <T> T[] toArray(T[] a) { return set.toArray(a); } @Override public boolean containsAll(Collection<?> c) { return set.containsAll(c); } @Override public boolean retainAll(Collection<?> c) { return set.retainAll(c); } @Override public boolean removeAll(Collection<?> c) { return set.removeAll(c); } }
- 인스턴스 메서드에서 내부의 Set 인스턴스에게 동일한 메서드 호출을 그대로 전달함 (=
포워딩
) - 동일한 메서드를 호출하기 위해 추가된 메서드를
포워딩 메서드
라고 부름
부모 클래스와 자식 클래스의 동시 수정 문제 : PersonalPlaylist
- 안타깝게도
Playlist
의 경우에는 합성으로 변경하더라도 가수별 노래 목록을 유지하기 위해Playlist
와PersonalPlaylist
를 함께 수정해야 하는 문제가 해결되지는 않음
- 그렇다 하더라도 여전히 상속보다는 합성을 이용하는 것이 좋은 이유는, 향후 Playlist의 내부 구현을 변경하더라도 파급효과를 최대한 PersonalPlaylist 내부로 캡슐화할 수 있기 때문임
- 대부분의 경우 구현에 대한 결합보다는 인터페이스에 대한 결합이 더 좋다는 사실을 기억하기
상속으로 인한 조합의 폭발적인 증가
- 가장 일반적인 상황은 작은 기능들을 조합해서 더 큰 기능을 수행하는 객체를 만들어야 하는 경우임.
- 부가 정책이 추가되는 조합 별로 클래스가 추가되어야 하므로 필요 이상으로 많은 수의 클래스가 생겨나는 것을
클래스 폭발
문제 or조합의 폭발
문제라고 부름 - 클래스 폭발 문제는 자식 클래스가 부모 클래스의 구현에 강하게 결합되도록 강요하는 상속의 근본적인 한계 때문에 발생하는 문제
합성 관계로 변경하기
- 여러 기능을 조합해야 하는 설계에 상속을 이용하면 모든 조합 가능한 경우별로 클래스를 추가해야 함 ⇒
클래스 폭발
문제 발생
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); } // 일반 요금제 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()); } } // 심야 할인 요금제 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()); } }
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); } }

부가 정책 적용하기

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); }
- Proxy 방식으로 동작하도록 만듦. 실제 객체의 동작 방식에 부가 기능을 추가하는 형태(데코레이터 패턴)로 클래스를 구성함
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 RegularPolicy(...));
새로운 정책 추가하기


- 위와 같이 클래스를 추가만 한 후 원하는 방식으로 조합해서 쓰면 되기에 클래스 폭발과 같은 문제가 일어날 일이 없음