[OOP] 상속보다는 조합을 사용하자
상속을 왜 사용하는가?
먼저 제목 내용을 살펴보기 전에 상속을 왜 사용하는지에 대해 생각해 볼 필요가 있다.
우리는 상속을 왜 사용하는가? 먼저 대부분의 이유는 다음과 크게 다르지 않다고 생각한다.
- 코드를 재사용하여 중복을 줄인다
- 변화에 대해 유연해지고 확장성이 증가한다.
- 개발 시간이 줄어든다.
하지만 위 내용은 상속을 적절히 사용했을 경우에만 해당한다.
상속을 잘못 사용하게되면 변화에 유연하지 않고 에러를 내기 쉽다.
상속의 단점
상속은 상위 클래스의 구현이 하위 클래스에 노출되기 때문에 캡슐화를 깨트린다.
캡슐화가 깨짐으로써 하위 클래스가 상위 클래스에 강하게 결합, 의존하게되고 강한 결합과 의존은 변화에 유연하게 대처하기 어려워진다.
예제를 통해서 살펴보자.
다음 예제는 로또 번호와 당첨 번호를 가지고 있는 Lotto와 WinningNumber 클래스이다.
public class Lotto {
protected List<Integer> lottoNumbers;
public Lotto(List<Integer> lottoNumbers) {
this.lottoNumbers = new ArrayList<>(lottoNumbers);
}
public boolean contains(Integer number) {
return this.lottoNumbers.contains(number);
}
}
public class WinningNumber extends Lotto {
private final BonusNumber bonusNumber;
public WinningNumber(List<Integer> lottoNumbers, BonusNumber bonusNumber) {
super(lottoNumbers);
this.bonusNumber = bonusNumber;
}
public long compare(Lotto lotto) {
return lottoNumbers.stream()
.filter(lotto::contains)
.count();
}
}
크게 문제가 보이지 않는다.
하지만 여기서 요구사항 변경으로 인해 다음과 같이 로또번호를 담은 List<Integer> 형태가 int[] 로 바뀌었다고 가정해보자.
public class Lotto {
protected int[] lottoNumbers;
public Lotto(int[] lottoNumbers) {
this.lottoNumbers = lottoNumbers;
}
public boolean contains(Integer number) {
return Arrays.stream(lottoNumbers)
.anyMatch(lottoNumber -> lottoNumber == number);
}
}
이렇게 되면 부모와 강한 의존을 맺은 WinningNumber 클래스는 강한 영향을 받는다.
즉, Lotto 클래스를 상속한 하위 클래스는 모두 깨지게 되는 것이다. 이렇게 되면 일일이 하위 클래스를 전부 수정해주어야 한다. 또, 상위 클래스 메서드 명과 매개변수 변경은 하위 클래스 전체의 변경을 불러일으키키도 한다.
이렇게 상속은 하위 클래스와 상위 클래스가 강하게 의존, 결합하기 때문에 변화에 유연하게 대처하기 어려워진다.
조합을 사용하자
조합은 기존 클래스가 새로운 클래스의 구성요소로 쓰이는 것을 말한다. 즉, 새로운 클래스를 만들어 필드로 기존 클래스의 인스턴스를 참조하는 것이다.
위에서 살펴봤던 WinningNumber 클래스가 Lotto를 상속하는 것이 아닌 조합을 사용하면 다음과 같이 변경할 수 있다.
public class WinningNumber {
private Lotto lotto;
private BonusNumber bonusNumber;
}
이처럼 WinningNumber 클래스(새로운 클래스)에서 인스턴스 변수로 Lotto 클래스(기존 클래스)를 가지는 것이 조합이다.
이렇게 구성하면 WinningNumber 클래스는 Lotto 클래스의 메서드를 호출하는 방식으로 동작하게 된다.
장점
- 메서드를 호출하는 방식으로 동작하기 때문에 캡슐화를 깨트리지 않는다.
- Lotto 클래스 같은 기존 클래스의 변화에 영향이 적어지며, 안전하다.
- 메서드 호출 방식이기 때문에 Lotto 클래스의 인스턴스 변수인 lottoNumbers의 타입이 List<Integer>에서 int[]로 바뀌어도 영향을 받지 않게 된다.
위 내용은 상속의 문제점 들에서 벗어날 방법이다.
자기 자신에 대한 참조를 다른 객체에 넘겨 나중에 필요할 때 콜백하도록 요청하는 콜백 프레임워크와 사용하기에는 적합하지 않다.
마무리
조합을 사용하는 것이 무조건적으로 좋다는 것이 아니다.
상속을 적절히 사용하면 조합보다 더 좋은 코드를 작성할 수 있다.
단, 다음 조건을 만족시키며 사용해야 한다.
- 확장을 고려하고 설계한 확실한 is-a 관계일 때
- API에 아무런 결함이 없는 경우, 결함이 있다면 하위 클래스까지 전파되어도 괜찮은 경우
'Software Engineering > OOP' 카테고리의 다른 글
[OOP] 무분별한 Getter/Setter를 지양하라 (0) | 2023.03.16 |
---|---|
[OOP] 원시 타입을 포장하라 (0) | 2023.03.15 |
[OOP] instanceof의 사용을 지양하라 (0) | 2023.03.14 |
댓글
이 글 공유하기
다른 글
-
[OOP] 무분별한 Getter/Setter를 지양하라
[OOP] 무분별한 Getter/Setter를 지양하라
2023.03.16 -
[OOP] 원시 타입을 포장하라
[OOP] 원시 타입을 포장하라
2023.03.15 -
[OOP] instanceof의 사용을 지양하라
[OOP] instanceof의 사용을 지양하라
2023.03.14