JAVA/이펙티브자바

이펙티브자바 Item18. 상속보다는 컴포지션을 사용하라

ghyeong 2024. 10. 7. 23:33
반응형

 

  • 상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다
  • 상속은 캡슐화를 깨트리게 된다
    • 내부구현이 노출된다
  • 하위 클래스가 상위 클래스에 강하게 의존 및 결합이 되는 설계이다

메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.

캡슐화 : 객체의 속성(data fields)과 행위(메서드, methods)를 하나로 묶고, 실제 구현 내용 일부를 외부에 감추어 은닉한다.

  • 문제 1. 상위 클래스가 어떻게 구현되느냐에 따라 하위클래스의 동작에 이상이 생길 수 있다
  • 문제 2. 다음 릴리스에서 상위 클래스에 새로운 메서드를 추가한다면?
    • 하위 클래스에서 재정의하지 못한 그 새로운 메서드를 사용해 허용되지 않은 원소를 추가할 수 있다

문제 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++;
		return super.addAll(c);
	}

	public int getAddCount() {
		return addCount;
	}
}
InstrumentHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("틱", "탁탁", "탱");

// 3을 반환하기를 원하지만
// 6을 반환한다
  • HashSet의 addAll은 각 원소를 add메서드를 호출해 추가하는데, 이 때 불리는 add는 InstrumentedHashSet에서 재정의한 메서드이다
반응형

위의 두가지 문제 모두 메서드 재정의가 원인이다

상속대신 컴포지션을 사용하자

컴포지션(조합)의 정의는 기존 클래스가 새로운 클래스의 구성요소로 사용되는 것을 말한다.

상속

public class Lotto {
    protected List<Integer> lottoNumbers;

    public Lotto(List<Integer> lottoNumbers) {
        this.lottoNumbers = new ArrayList<>(lottoNumbers);
    }

    public boolean contains(Integer lottoNumber) {
        return this.lottoNumbers.contains(lottoNumber);
    }
    ...
}
public class WinningLotto extends Lotto {
    private final LottoNumber bonusBall;

    public WinningLotto(List<Integer> lottoNumbers, LottoNumber bonusBall) {
        super(lottoNumbers);
        this.lottoNumber = lottoNumber;
    }

    public long calculateMatchCount(Lotto otherLotto) {
        return lottoNumbers.stream()
            .filter(otherLotto::contains)
            .count();
    }
    ...
}

조합

public class WinnginLotto {
    private Lotto lotto;
    private LottoNumber bonusBall;

    public long containsLotto(Lotto otherLotto) {
        return lotto.calculateMatchCount(otherLotto);
    }
    ...
}

WinningLotto 클래스에서 인스턴스 변수로 Lotto 클래스를 가지고 있다는 것을 알 수 있게 된다.

이처럼, WinningLotto 클래스에서 인스턴스 변수로 Lotto 클래스를 가지는 것이 조합

WinningLotto에서 Lotto 클래스를 사용하고 싶으면 Lotto 클래스의 메서드를 호출하는 방식으로 사용하게 된다.


정리

상위 클래스에 의존하게 되어 종속적이고 변화에 유연하지 못한 상속보다는 조합을 사용하자

컴포지션을 써야 할 상황에서 상속을 사용하는 건 내부구현을 불필요하게 노출하는 꼴이다.

하지만 조합이 상속보다 무조건 좋다는 것은 아니다.

상속은 확실한 is - a 관계일 경우 사용하는 것을 추천

  • API에 아무런 결합이 없는 경우, 결함이 있다면 하위 클래스까지 전파돼도 괜찮은 경우

상속은 코드 재사용의 개념이 아니다.

  • 상속은 반드시 확장이라는 관점에서 사용해야 한다
반응형