HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
📖
공부한 책
/
오브젝트
오브젝트
/
11. 합성과 유연한 설계

11. 합성과 유연한 설계

상속과 합성의 차이상속을 합성으로 변경하기불필요한 인터페이스 상속 문제 : 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()); } }
기본 정책과 부가 정책을 포괄하는 RatePolicy 인터페이스와 일반 요금제, 심야 할인 요금제를 구현하는 클래스
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
notion image

부가 정책 적용하기

notion image
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(...));

새로운 정책 추가하기

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