상속과 조합
업데이트:
상속의 목적은?
- 하위 클래스에서 상위 클래스의 속성이나 행위를
재사용하고 확장
하는 것!
상속의 단점은 무엇일까?
- 상위 클래스 변경의 어려움
- 클래스가 많아짐
- 기능 오용의 가능성
1. 상위 클래스 변경의 어려움
- Java의 ArrayList 클래스를 예로 들어보자.
-
ArrayList 클래스는 아래와 같은 클래스 구조를 갖는다.
- 이러한 계층 구도에서 만약 AbstractCollection 클래스에 변경이 있다고 가정해보자.
-
이럴경우, 다음과 같이 상위 클래스의 변경의 여파가 모든 하위 클래스로 전파될 것이다.
- 이처럼, 상속은 하위 타입이 상위 타입의 동작 구조에 매우 밀접하게 엮이는 경향이 있다.
- 둘 간의 결합도가 높아 서로를 강하게 의존하는 구조가 된다는 것이다.
- 그래서
상위 타입의 구현 변경이 하위 타입에 영향을 줄 가능성이 매우 높아진다.
- 결국, 캡슐화를 깨뜨릴 가능성이 높다.
캡슐화란?
- 내부 구현을 외부로부터 감추는 것.
- 내부 구현의 변경에 따른
외부 영향을 최소화 하기 위함이 목적.
- 내부 구현의 변경에 따른 외부 영향을 최소화하는 것이 캡슐화의 목적인데, 상속을 이용하면 이러한 목적을 이루지 못할 가능성이 크기 때문에 상속은 캡슐화를 깨뜨릴 가능성이 높은 것이다!
- 결국, 캡슐화를 깨뜨릴 가능성이 높다.
2. 클래스가 많아짐
-
해당 그림에서 보는 것과 같이 상속 구조가 깊어질 수록 클래스가 점점 많아진다.
3. 기능 오용의 가능성
-
다음과 같이 ArrayList를 상속받은 클래스가 있다고 가정해보자.
package test; import java.util.ArrayList; class Luggage { private int size; public Luggage(int size) { this.size = size; } public int getSize() { return size; } } class MyBag extends ArrayList<Luggage> { private int maxAvailableSize; private int currentSize; public MyBag(int maxAvailableSize) { this.maxAvailableSize = maxAvailableSize; } public void put(Luggage luggage) { if (!canPutLuggage(luggage)) { throw new RuntimeException("가방에 짐을 넣을 수 있는 공간이 충분하지 않습니다!!!"); } super.add(luggage); this.currentSize += luggage.getSize(); } public void extract(Luggage luggage) { super.remove(luggage); this.currentSize -= luggage.getSize(); } public boolean canPutLuggage(Luggage luggage) { return this.currentSize + luggage.getSize() <= this.maxAvailableSize; } } public class Test { public static void main(String[] args) { // 10만큼의 짐을 넣을 수 있는 가방 객체 생성 MyBag bag = new MyBag(10); Luggage luggage8 = new Luggage(8); Luggage luggage2 = new Luggage(2); Luggage luggage1 = new Luggage(1); if (bag.canPutLuggage(luggage8)) { System.out.println("크기 8짜리의 짐을 넣습니다."); bag.put(luggage8); // 정상적인 사용 } if (bag.canPutLuggage(luggage2)) { System.out.println("크기 2짜리의 짐을 넣습니다."); bag.add(luggage2); // 기능 오용, current size가 증가하지 않음 } if (bag.canPutLuggage(luggage1)) { System.out.println("크기 1짜리의 짐을 넣습니다."); bag.put(luggage1); // 원래 해당 if문을 실행해선 안되지만 실행되는 상황 } } }
-
ArrayList에 미리 구현돼있는 add 메서드를 오용하면서 원하는 결과를 얻지 못할 가능성이 생긴다.
조합
Composition
- 조합을 이용하면 위에서 보았던 상속의 단점을 해결할 수 있다.
- 조합은 다음과 같이 멤버필드로 다른 객체를 참조하는 방식이다.
... class MyBag { private int maxAvailableSize; private int currentSize; private List<Luggage> luggageList = new ArrayList<>(); public MyBag(int maxAvailableSize) { this.maxAvailableSize = maxAvailableSize; } public void put(Luggage luggage) { if (!canPutLuggage(luggage)) { throw new RuntimeException("가방에 짐을 넣을 수 있는 공간이 충분하지 않습니다!!!"); } luggageList.add(luggage); this.currentSize += luggage.getSize(); } public void extract(Luggage luggage) { luggageList.remove(luggage); this.currentSize -= luggage.getSize(); } public boolean canPutLuggage(Luggage luggage) { return this.currentSize + luggage.getSize() <= this.maxAvailableSize; } } ...
- 위와 같이, 조합으로 클래스를 작성한다면 그 객체의 내부 구현를 신경 써야 한다거나, 변화에 오류가 생기는 일이 없어질 것이다.
- 조합은 제공하는 기능을 사용하므로 기능의 내부 구현이 변경되더라도 영향을 받을 가능성이 줄어든다.
- 즉, 캡슐화를 깨뜨릴 가능성이 적다.
- 그리고 기능을 오용할 수 있는 문제도 해결할 수 있다.
- 위와 같이, 조합으로 클래스를 작성한다면 그 객체의 내부 구현를 신경 써야 한다거나, 변화에 오류가 생기는 일이 없어질 것이다.
정리
- 상속을 이용하면
상위 클래스의 기능 추가에 대해 유연하게 대처하기 힘들다.
- 또한, 별도의 클래스를 선언함으로써
클래스가 많아 질수 있으며
, 상위 클래스를 확장한 하위 클래스에서기능 오용
의 우려가 있다. - 그렇다면 언제 상속을 사용하고 언제 조합을 사용해야 할까?
- 두 클래스간의 관계가 완벽한 is-a 관계가 아니라면 일반적으로
상속보다는 조합
을 고려하는 것이 좋은 선택이다. - 그밖에 상속을 사용하기 좋을 때는…
- 상위 클래스와 하위클래스를 모두 같은 프로그래머가 통제하는 패키지일 경우.
- 확장할 목적으로 설계되었고 문서화도 잘 됐을 경우.
- 하위 클래스가 상위 클래스의 ‘진짜’ 하위 타입인 상황에서만 쓰자.
- 클래스간 관계가
is-a 관계
일때만 상속
- 클래스간 관계가
- 상속을 결정하는 질문
- 확장하는 클래스의 API에 아무런 결함이 없는가?
- 결함이 있다면 이 결함이 내 클래스의 API까지 전파되도 괜찮은가?
- 컴포지션으로는 이런 결함을 숨길 수 있지만, 상속은 아니다.
- 두 클래스간의 관계가 완벽한 is-a 관계가 아니라면 일반적으로
참고
- 객체지향 프로그래밍 입문
- effective java 3/E / Joshua Bloch / 인사이트
댓글남기기