본문 바로가기

Java

[Effective Java] 아이템18. 상속보다는 컴포지션을 사용하라

[상속의 문제점] 하위클래스의 캡슐화를 깨뜨린다.

1. 상위 클래스의 메서드를 재정의할 때 재정의한 자신의 메서드를 사용하여 하위 클래스가 오작동할 수 있다.

예시) 

public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet(){}

    public InstrumentedHashSet(int initCap, float loadFactor){
        super(initCap, loadFactor);
    }
    
	//재정의
    @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;
    }
}

public class TestItem18 {
    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(List.of("1", "2", "3"));//addAll

        System.out.println(s.getAddCount()); // 3이 아닌 6이 반환됨..!
    }
}

3을 기대했지만 6이 출력되는데, 그 원인은 HashSet의 addAll메서드가 내부적으로 자신의 add메서드를 사용하기 때문이다.

자세히 설명하면,

InstrumentedHashSet의 addAll로 addCount += c.size()로 인해 3을 더한 뒤, hashSet의 addAll을 호출한다.

hashSet의 addAll은 내부적으로 add를 사용하는데 이 때 InstrumentedHashSet에서 재정의한 add메서드를 부른다.

 

2. 상위클래스에 새로운 메서드가 추가되어 하위클래스에서는 허용되지 않은 원소를 상위클래스를 통해 넣을 수 있다.

 

만약에 다음 릴리즈에서 상위 클래스에 원소를 추가하는 메서드들이 생겼다고 해보자.

새로생긴 메서드 때문에 하위 클래스에서 허용되지 않은 원소들을 상위클래스를 통해 추가할 수 있게 된다.

즉, 하위 클래스에서 재정의 하지 않은, 새로운 메서드를 사용해 허용되지 않은 원소를 추가할 수 있게 된다!!

 

 

3. 하위 클래스에 새로운 메서드를 추가했을 때 상위클래스와 같은 시그니처를 가진 메서드가 새로 생길 수 있다.

반환 타입이 다르다면 컴파일조차 안된다. 

반환 타입이 같다면 상위 클래스의 새 메서드를 재정의하게 된다. 

 

 


상속보다는 컴포지션을 사용하라.

상속으로 인해 하위 클래스의 캡슐화가 깨지는 문제를 해결하기 위해서 컴포지션을 사용한다.

컴포지션이란 기존 클래스가 새로운 클래스의 구성요소로 쓰이는 설계이다.

 

- 사용방법

새로운 클래스를 만들고, private 필드로 기존 클래스의 인스턴스를 참조한다.

새로운 클래스에서 기존 클래스의 메서드들을 호출하는 것을 전달(forwarding)이라 하며,

이러한 새 클래스의 메서드들전달 메서드(forwarding method)라 한다.

 
그 결과 새로운 클래스는 기존 클래스 내부 구현 방식에 영향을 받지 않고,
심지어 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향을 받지 않는다.
 
 
1) InstrumentedSet: 래퍼 클래스, set 인스턴스를 감싸고 있다
public class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

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

 

// 전달 클래스
public class ForwardingSet<E> implements Set<E> {

    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s;}

    public void clear() { s.clear(); }
    public boolean contains(Object o) {return s.contains(o); }
    public boolean isEmpty() { return s.isEmpty(); }
    public int size() { return s.size(); }
    public Iterator<E> iterator() { return s. iterator(); }
    public boolean add(E e) { return s.add(e); }
    public boolean remove(Object o) {return s.remove(o); }
    public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
    public boolean removeAll(Collection<?> c) { return s.removeAll(c); }
    public boolean retainAll(Collection<?> c) { return s.retainAll(c); }
    public Object[] toArray() { return s.toArray(); }
    public <T> T[] toArray(T[] a) { return s.toArray(a); }
    @Override public boolean equals(Object o) { return s.equals(o); }
    @Override public int hashCode() {return s.hashCode(); }
    @Override public String toString() { return s.toString(); }

}

 

 

상속은 언제 해야할까?

하위 클래스가 상위 클래스의 '진짜' 하위 타입인 상황에서만 사용한다.
클래스 B가 클래스 A와 is-a 관계일때만 클래스 A를 상속한다. "B가 정말 A인가"

has-a관계라면 컴포지션을 이용