[이펙티브 자바] 아이템18 | 상속보다는 조합(컴포지션)을 사용하라

제롬·2022년 3월 4일
0

이펙티브자바

목록 보기
18/25

상속의 위험성

상위 클래스와 하위 클래스가 모두 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안전한 방법이다. 확장할 목적으로 설계되거나 문서화도 잘 된 클래스도 마찬가지로 안전하다.

하지만, 다른 패키지의 구체 클래스를 상속하는 일은 위험성을 가지고 있다.
(지금 말하는 상속은 클래스가 다른 클래스를 확장하는 구현 상속을 말한다. 클래스가 인터페이스를 구현하거나 인터페이스가 다른 인터페이스를 확장하는 인터페이스 상속과는 무관하다.)

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

캡슐화 : 타인의 외부에서의 조작에 대비해 외부에서 특정 속성이나 메서드를 사용하지 못하도록 숨겨놓은 것.

상위 클래스의 구현이 하위 클래스에게 노출되는 상속은 캡슐화를 깨뜨린다.

캡슐화가 깨짐으로써 하위 클래스는 상위 클래스에 강하게 결합, 의존하게 되고 강한 결합, 의존은 변화에 유연하게 대처하기가 힘들다.

[lottoNumbers를 필드로 갖는 Lotto 클래스]

public class Lotto {
    protected List<Integer> lottoNumbers;

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

    public boolean contains(Integer integer) {
        return this.lottoNumbers.contains(integer);
    }
}
...

[Lotto 클래스를 상속하며 당첨 로또 번호를 갖는 WinningLotto 클래스]

public class WinningLotto extends Lotto{
    private final BonusBall bonusBall;

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

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

위 두 클래스 중 부모 클래스 Lotto의 필드인 List<Integer>int[] lottoNumbers로 바뀐다면 Lotto클래스를 상속한 WinningLotto는 강한 영향을 받는다.

[필드가 변경된 Lotto 클래스]

public class Lotto {
    protected int[] lottoNumbers;

    public Lotto(final int[] lottoNumbers) {
        this.lottoNumbers = lottoNumbers;
    }

    public boolean contains(Integer integer) {
        return Arrays.stream(lottoNumbers)
            .anyMatch(lottoNumber -> Objects.equals(lottoNumber, integer));
    }
}
...

[Lotto 클래스를 상속한 WinningLotto 클래스 오류 발생]

public class WinningLotto extends Lotto{
    private final BonusBall bonusBall;

    public WinningLotto(final List<Integer> lottoNumbers, final BonusBall bonusBall) {
        super(lottoNumbers);
        this.bonusBall = bonusBall;
    } // 오류 발생

    public long compare(Lotto lotto){
        return lottoNumbers.stream()
                .filter(lotto::contains)
                .count();
    } // 오류 발생
}

이처럼 상속은 하위 클래스가 상위 클래스에 강하게 의존, 결합하기 때문에 변화에 유연하게 대처하기 어렵다.

또한, 상위 클래스의 매개변수나 메서드 이름의 변화는 하위 클래스 전체의 변화를 야기할수도 있다. 이를 해결하는 방법은 하위 클래스는 상위 클래스의 변화에 맞춰 계속해서 수정하는 것이다.

상속 구조가 깊으면 깊을수록 이런 문제는 심화된다.

상속 대신 조합(Composition)을 사용하자.

조합: 기존 클래스가 새로운 클래스의 구성요소로 사용된다. 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 한다.

기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 조합(Composition)이라고 한다.

[조합을 사용한 WinningLotto 클래스]

public class WinningLotto{
    private final Lotto lotto
    private final BonusBall bonusBall;
}

위 코드처럼 WinningLotto 클래스에서 필드로 Lotto 클래스를 갖는것이 조합이다. WinningLotto 클래스는 Lotto 클래스의 메서드를 호출해 그 결과를 반환하는 방식으로 작동한다.

이 방식을 전달(forwarding) 이라고 하며, WinningLotto 클래스의 메서드들을 전달 메서드라고 부른다.

조합을 사용했을때 장점

  1. 기존 클래스(Lotto)에서 받는 영향이 적어진다.
  2. 메서드를 호출하는 방식으로 사용하기때문에 캡슐화를 깨뜨리지 않는다.

조합을 사용한 WinningLotto 클래스는 메서드를 호출하는 방식이기 때문에 Lotto 클래스의 필드인 List<Integer> lottoNumbersint[] lottoNumbers로 바뀌어도 영향을 받지 않는다.

즉, 상속의 문제점에서 벗어날 수 있는 방법이다.

조합을 사용할 때 주의할 점

조합은 콜백 프레임워크와 사용하기에 적합하지 않는다는 것을 기억하고 사용시 주의해야 한다.

콜백 프레임워크: 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출(콜백) 때 사용하도록 하는 것.

[결론]

상속은 캡슐화를 깨뜨리고 상위 클래스에 의존하게 되므로 변화에 유연하지 못하다. 따라서 상속보다는 조합을 사용하자. 하지만, 상속이 적절히 사용된다면 조합보다 강력하고 개발하기 편하다. 단, 다음과 같은 조건을 만족할 경우에만 해당된다.

  1. is-a 관계를 만족할 때
  2. Api 에 아무런 결함이 없거나 있더라도 하위 클래스에 전파돼도 괜찮은 경우일 때

클래스 A와 이를 상속하는 클래스 B가 있을때 is-a 관계를 고려한다면 "B가 정말 A 인가" 를 고려해야 한다. 예를 들어, 교통수단 클래스와 교통수단을 상속하는 자동차가 있다고 가정하면 "자동차는 교통수단입니다." 조건을 만족해야 한다.

public class 자동차 extends 교통수단 {
    protected void 움직이다() {
    	...
    }

    protected void 연료를채우다() {
    	...
    }
}

자동차가 교통수단이라는 사실은 변할가능성이 거의 없고 자동차가 움직이고 연료를 채우는 행위 역시 변할 가능성이 적다면 is-a 관계에서 상위 클래스가 변경될 가능성이 적다고 할 수 있다. 따라서 이런경우에는 상속을 사용해도 좋다.

만약 그렇지 않다면 A(교통수단)클래스를 B(자동차)클래스의 private 인스턴스로 두면된다. 즉, A클래스는 B클래스의 필수구성요소가 아니라 구현방법중에 하나일 뿐이다.

물론 위 관계에 포함된다고해도 무조건적으로 상속을 사용해도 된다는것은 아니다. 다만, 상속을 사용함에 있어 단순히 재사용만을 가능한 수단으로만 고려할 것이 아니라 확장 또한 반드시 고려해서 사용해야 한다는 것이다.

다만, 애매할 경우 상속보다는 조합을 사용하는것을 추천한다.

[Reference]
Tecoble
Effective Java/item18

0개의 댓글