티스토리 뷰

상속의 단점을 피하면서 코드를 재사용하기 위해 합성을 사용할 수 있다. 합성은 구현에 의존하지 않고, 퍼블릭 인터페이스에 의존한다. 

상속은 부모 클래스에 구현된 코드를 재사용하지만, 합성은 포함된 객체의 퍼블릭 인터페이스를 재사용한다. 이는 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경시키고, 클래스 사이의 결합도를 낮추는 효과를 준다.

상속을 합성으로 변경하기


10장에서 코드 재사용을 위해 상속을 남용했을 때 생길 수 있는 문제로 3가지 예시를 알아봤었다.

 

  • 불필요한 인터페이스 상속 문제
  • 메서드 오버라이딩의 오작용 문제
  • 부모 클래스와 자식 클래스의 동시 수정 문제

상속 관계를 합성으로 바꿔서 이 세 가지 문제를 해결할 수 있다.

불필요한 인터페이스 상속 문제

java.util.Properties

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는 Hashtable의 내부 구현에 대해 모른다. 오직, getter, setter 메서드를 통해서만 Hashtable과 협력할 수 있다. 또한 String 타입의 키와 값만 허용하도록 정의함으로써 때문에 Properties의 규칙을 어길 위험도 사라진다.

책에서는 Stack 클래스에 대한 예제도 있는데 이는 생략한다. 

메서드 오버라이딩의 오작용 문제

InstrumentedHashSet

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 <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의 오퍼레이션을 오버라이딩한 인스턴스 메서드에서 내부의 HashSet 인스턴스에게 동일한 메서드 호출을 그대로 전달한다는 것을 알 수 있다. 이를 포워딩(forwarding)이라 부르고 동일한 메서드를 호출하기 위해 추가된 메서드를 포워딩 메서드(forwarding method)라고 부른다.

InstrumentedHashSet이 제공하는 모든 오퍼레이션들이 Set 인터페이스에 정의되어 있기 때문에 Set인터페이스를 실체화하면서 내부에 HashSet 인스턴스를 합성하면 HashSet에 대한 구현 결합도를 제거하면서도 퍼블릭 인터페이스를 그대로 유지할 수 있다.

부모 클래스와 자식 클래스의 동시 수정 문제

PersonalPlaylist

public class PersonalPlaylist {
	
    private Playlist playlist = new Playlist();
    
    public void append(Song song) {
    	playlist.append(song);
    }
    
    public void remove(Song song) {
    	playlist.getTracks().remove(song);
        playlist.getSingers().remove(song.getSinger());
    }
}

이 경우에는 합성을 하더라도 부모클래스와 자식 클래스의 동시 수정 문제가 해결되진 않지만, 향후 Playlist 내부 구현이 변경되더라도 파급효과를 최대한 PersonalPlaylist 내부로 캡슐화 할 수 있기에 상속보다는 합성을 사용하는 것이 더 좋은 경우라고 볼 수 있다.

 

상속으로 인한 조합의 폭발적인 증가


상속으로 인해 결합도가 높아지면 코드를 수정하는 데 필요한 작업의 양이 과도하게 늘어나는 경향이 있다. 일반적으로 다음과 같은 두가지 문제가 발생한다.

  • 하나의 기능을 추가하거나 수정하기 위해 불필요하게 많은 수의 클래스를 추가하거나 수정 해야한다.
  • 단일 상속만 지원하는 언어네서는 상속으로 인해 오히려 중복 코드의 양이 늘어날 수 있다.

이 부분은 예제가 많아서 다 정리하기가 힘들다.. 책을 참고하기 바란다...

요약하면, 변경되는 요구사항들을 상속으로 구현하면 부모 클래스와 자식클래스 사이의 결합도가 커진다. 또한  단일 상속을 지원하는 언어들은 상속으로 인해 발생하는 중복 코드를 제거하기가 쉽지 않다. 상속을 남용하다보면 하나의 기능을 추가하기 위해 필요 이상으로 많은 클래스를 추가하게 되고, 이 경우를 가리켜 클래스 폭발(class explosion) 문제 또는 조합의 폭발(combinational explosion) 문제라고 부른다.

아래는 상속을 통한 요금제 예제의 클래스 다이어그램이다.

 

합성 관계로 변경하기


합성은 컴파일타임 관계를 런타임 관계로 변경함으로써 이 문제를 해결한다. 합성을 사용하면 구현이 아닌 퍼블릭 인터페이스에 대해서만 의존할 수 있기 때문에 런타임에 객체의 관계를 변경할 수 있다. 8장에서 컴파일 타임 의존성과 런타임 의존성의 거리가 멀수록 설계가 유연해진다고 했다. 그러나, 상속은 컴파일 타임의 의존성과 런타임의 의존성을 동일하게 만든다.

부가 정책 적용하기

부가 정책은 RatePolicy 인터페이스를 구현해야 하고, 내부에 또 다른 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);
}

TaxablePolicy

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));
    }
}

RateDiscountable

public class RateDiscountablePolicy extends AdditionalRatePolicy {
	
    private Money discountAmount;
    
    public TaxablePolicy(Money discountAmount, RatePolicy next) {
    	super(next);
        this.discountAmount = discountAmount;
    }
    
    @Override
    protected Money afterCalculated(Money fee) {
    	return fee.minus(discountAmount);
    }
}

다이어그램으로 보면 아래와 같다.

기본 정책과 부가 정책을 쉽게 합성할 수 있는 상황이 됐다. 또한 새로운 정책을 추가하기 위해서 클래스 하나만 추가한 후 원하는 방식으로 조합을 하면 된다. 상속의 경우와 비교해 간단해진 것을 알 수 있다.

믹스인


객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법을 믹스인(mixin)이라 한다. 합성이 실행 시점에 객체를 조합하는 재사용 방법이라면 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 재사용 방법이다.

 

느낀점


이번 장만 봤을 때, 상속은 거의 사용하면 안되는 것처럼 느껴졌다. 하지만 책에서도 언급하듯이, 상속을 구현 상속과 인터페이스 상속 두가지로 나눠진다. 그리고 이번 장에서는 구현 상속일 경우에 생기는 문제점에 대해서 주로 다룬 것이다. 평소에 상속을 잘 쓰지도 않았지만, 잘 고민해보고 써야겠다는 생각이 든다.

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday