기능의 재사용면에 있어서 상속보다 조합(Composition)
조합(Composition) 이란?
- 다른객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 메서드를 호출하는 기법이다.
- 해당 인스턴스의 내부 구현이 바뀌더라도 영향을 받지 않는다.
- 또한, 다른객체의 인스턴스이므로 인터페이스를 이용하면 Type을 바꿀 수 있다.
상속 | 컴포지션 |
IS-A | HAS-A |
부모 클래스 확장 | 기존 클래스가 새로운 클래스의 구성요소가 되는 것 (기존에 존재하는 객체를 멤버변수로 사용) |
둘 이상의 클래스를 확장할 수 없다 (C++은 제외지만 다중상속 위험함) | 여러 다른 클래스와 관계 가질 수 있다. |
정적 바인딩이며 런타임에 변경할 수 없다. | 동적 바인딩이며 변경 사항에 유연 |
상속을 받게되면 한번에 메모리에 올라간다. 극단적인 예시 (메모리가 부족하여 객체 자체가 생성이 안될수 도 있다.) |
멤버 클래스를 포인터로 들고 있기 때문에 메모리가 랜덤으로 배치된다. 극단적인 예시 (잘 사용 하다가 메모리가 부족한 멤버 클래스만 생성이 안될수 도 있다. |
상속은 변경의 유연함이라는 측면에서 단점을 가짐
1. 상위 클래스 변경이 어려움
상위 클래스 변경 시, 하위 클래스 까지 영향을 미침
상속을 통해 기능을 재사용하다보면 하위 클래스가 상위 클래스의 내부 구현에 직접적으로 의존하는 상황이 벌어질 수 있습니다. 예를 들어 상위 클래스에 정의된 필드에 직접 접근하거나 상위 클래스의 메서드의 실행 순서를 보고 그에 맞게 하위 클래스를 구현하는 식으로요. 상위 클래스의 private 메서드를 protected로 바꾸기도 하구요. 이런 코드가 증가할수록 상위 클래스의 캡슐화는 약해지게 됩니다.
ex)
public class InheritSet<E> extends HashSet<E> {
private int addCount = 0;
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
HashSet을 상속받은 InhefitSet 클래스의 객체를 사용하면서 원소가 몇 개 더해졌는지 알기 위한 addCount field를 추가하고, add(), addAll() 메소드를 사용할 때 마다 개수를 증가 시키기 위해 메소드를 재정의 하였다.
@DisplayName("add All method를 통해 count를 4증가 시켰으니 4가 나오기를 기대한다...")
@Test
public void inheritSetTest() {
InheritSet<String>stringInheritSet = new InheritSet<>();
stringInheritSet.addAll(Arrays.asList("한방", "두방", "세방", "네방"));
assertEquals(8, stringInheritSet.getAddCount());
}
이 테스트 코드를 실행하면 getAddCount() 메소드는 4를 반환할 것이라고 생각 하겠지만 8을 반환한다.
HashSet은 다음과 같은 상속구조를 가진다.
예제 코드에서 호출하는 addAll() 메소드는 AbstractCollection 클래스의 메소드인데 addAll() 메소드는 내부적으로 add() 메소드를 사용하는 방식으로 구현 되어있다.
InheritSet 의 addAll() 메소드를 호출하면 addCount에 원소의 size인 4를 더하고 super.addAll()를 호출하는데, addAll() 메소드는 for문에서 재정의 한 InheritSet 의 add() 메소드를 호출하여 addCount는 4번 중복되어 더해져 8이라는 값을 갖게 되었다.
결국 InheritSet에서 재정의 한 addAll() 메소드는 상위 클래스의 내부 구현 방식을 모른 채 재정의 하였기 때문에 생각과는 다른 결과를 맞이하게 되었다.
{InheritSet 의 AddAll (4) -> IngeritSet 의 Add (4) -> Super 의 Add}
Composition / Forwarding 으로 고친 ex)
// 1
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> set) {
super(set);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
// 2
public class ForwardingSet<E> implements Set<E> {
private final Set<E> set;
public ForwardingSet(Set<E> set) {this.set = set;}
@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 add(E e) {return set.add(e);}
@Override
public boolean remove(Object o) {return set.remove(o);}
@Override
public boolean containsAll(Collection<?> c) {return set.containsAll(c);}
@Override
public boolean addAll(Collection<? extends E> c) {return set.addAll(c);}
@Override
public boolean retainAll(Collection<?> c) {return set.retainAll(c);}
@Override
public boolean removeAll(Collection<?> c) {return set.removeAll(c);}
@Override
public void clear() {set.clear();}
@Override
public boolean equals(Object o) {return set.equals(o);}
@Override
public int hashCode() {return set.hashCode();}
}
@Test
public void compositionSetTest() {
InstrumentedSet<String>instrumentedSet = new InstrumentedSet<>(new HashSet<>());
instrumentedSet.addAll(Arrays.asList("한방", "두방", "세방", "네방"));
assertEquals(4, instrumentedSet.getAddCount());
// test passed. actual 4
}
{InheritSet 의 AddAll (4) -> Super 의 Add}
새로 만든 InstrumentedSet 클래스는 ForwardingSet클래스를 확장하여 add와 addAll메소드는 ForwardingSet의 메소드를 이용하게 되는데, ForwardingSet은 Set 인터페이스를 구현한 구현 클래스 이다.
테스트 코드에서 ForwardingSet 클래스는 생성자로 주입받은 HashSet을 private final 필드로 참조하게 되는데 이것을 Composition이라고 할 수 있다.
ForwardingSet 클래스는 Set 인터페이스의 모든 메소드를 재정의 하고 있는데, 재정의 내용은 간단하다. 기존 Set 인터페이스 메소드의 결과를 그냥 반환할 뿐이다. 이 메소드들은 전달 메소드이다.
결국 새로 만든 InstrumentedSet 클래스는 addCount에 숫자를 증가 시키는 기능의 확장을 일궈내면서 상위 클래스에 기능변경에 영향을 받지 않으며, Forwarding 클래스의 전달 메소드를 통해 기존 클래스(HashSet)의 똑같은 기능을 그대로 사용할 수 있게 되었다.
InstrumentedSet 클래스는 HashSet의 모든 기능은 정의한 Set 인터페이스를 활용해 설계되어 견고하면서도 유연한 클래스가 되었다.
위 구조는 Set의 기능을 덧씌워 새로운 Set으로 만들었기 때문에 HashSet, TreeSet등 다양한 Set구현체를 사용할 수 있다.
2. 클래스가 불필요하게 증가 (이 악물고 상속만 사용할 때)
파일 보관소를 구현한 Storage 클래스가 있다고 할 때, 제품 출시 이후 보관소의 용량을 아낄 수 있는 방법을 제공해달라는 요구가 발생하였다. 이 요구를 수용키위해 압축 기능을 추가한 CompressedStorage 클래스를 만들었다. 또한 보안 요구가 추가되어 EncryptedStorage 클래스를 추가하였다. 그런데, 보안과 압축 기능을 모두 가진 방법을 제공해달라는 추가 요구가 발생하였다. 기존의 CompressedStorage와 EncryptedStorage가 있음에도 불구하고 동시에 해당 기능을 수행하기 위해서는 CompressedEncryptedStorage라는 새로운 클래스를 생성해야한다. 이와 더불어 이러한 요구가 늘어나게 된다면, 클래스는 불필요하게 많이 증가하게 된다.
요약
상속의 단점으로 기존의 클래스를 확장한는 것이 아니라,
새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게하는 컴포지션 방식을 사용
이러한 방식을 forwarding(전달) 한다 라고 하며, 새로운 클래스는 기존의 클래스의 구현방식을 벗어나고, 기존 클래스에 새로운 메소드가 추가되더라도 전혀 영향을 받지 않는다.
상속을 사용할 때 재사용이라는 관점이 아니라 기능의 확장 이라는 관점에서 적용해야함