3 상속보단 조립
source: categories/study/oop_programming/opp_programming_3.md
3. 상속보단 조립
3.1 상속보단 조립
- 이 시간에는 상속보단 조립에 대해서 살펴보겠습니다.

- 위의 클래스 다이어그램은 스프링이라는 프레임워크의 초기버전에서 앱 요청을 처리하기 위해 사용한 클래스의 계층 구조를 보여주고 있습니다.
- 이 계층 구조에서 하위 타입의 클래스는 상위 타입의 기능을 확장하는 특징이 있습니다.
- 예를 들어,
AbstractController는 웹 요청 처리 위한 기본 기능을 제공하고 있고, - 하위에 있는
BaseCommandController는 웹 요청 처리 위한 기본 기능에 파라미터 처리 기능을 확장을 하고 있다. - 비슷하게
AbstractFormController클래스는 파라미터 처리 기능에 추가로 폼을 보여주고 전송하는 기능을 확장하고 있다. - 이렇게 상속은 상위 클래스의 기능을 재사용하고 확장하는 방식으로 많이 사용하고 있다.
상속을 통한 기능 재사용시 발생할 수 있는 단점
- 그런데 위와 같은 상속을 통한 기능 재사용은 몇가지 단점을 유발시킬 수 있다.
- 크게 3가지 문제를 발생시킨다.
- 상위 클래스 변경 어려움
- 클래스 증가
- 상속 오용
상속을 통한 재사용의 단점 1

- 상위 클래스 변경이 어려움
- 상위 클래스를 변경하게 되면 그 변경이 모든 하위 클래스에 영향을 줄 수가 있습니다.
- 즉, 상위 클래스를 조금만 잘못 변경해도 모든 하위 클래스가 비정상적으로 동작할 수 있게 되는 것이다.
- 게다가 상위 클래스 입장에서는 앞으로 어떤 하위 클래스가 추가될지 모른다.
- 그러다보니까 하위 클래스가 많아지면 많아질수록 상위 클래스를 변경하는게 매우 어렵게됩니다.
- 게다가 상위 클래스가 어떤식으로 동작하는지 어느정도 알고 있어야 하위클래스가 기능을 재사용할 수 있게되는데,
- 이는 결과적으로 상위 클래스는 하위 클래스에 대해서 캡슐화가 약해진다고도 볼 수 있는 것이다.
- 캡슐화가 약해지니깐 그만큼 상위 클래스 내부 내용을 변경하기 어려워지게 되는 것이다.
상속을 통한 재사용의 단점 2

- 클래스가 증가할 수 있다는 것이다.
- 왼쪽에 있는 그림처럼 처음에
Storage클래스하고 그 클래스를 상속받은CompressedStorage,EncryptedStorage가 있었다고 해보자 CompressedStorage는 저장소 기능을 제공하는 스토리지를 상속받아서 거기에 압축 기능을 추가한 것이고EncryptedStorage는 암호화 기능을 추가한 것이다.
- 그런데 캐시 기능을 제공하는 스토리지가 필요해졌다.
- 그러면 스토리지에 캐시기능을 확장한
CacheableStorage라는 그런 클래스를 만들어볼 수 있겠죠?
- 여기에 압축과 암호화 기능을 함께 제공하는 스토리지가 필요해졌다면?
- 그럼 또는 캐시 기능과 암호화 기능을 제공하는 스토리지가 필요해졌다면?
- 이 경우에 위 그림에 보시는 것처럼
CacheableEncryptedStorage를CacheableStorage를 상속받아서 구현할 수도 있겠죠? - 또는
EncryptedStorage를 상속을 받아서 구현을 할 수도 있을 것이다. - 비슷하게 암호화 기능과 압축 기능을 구현하는 그런 스토리지는
CompressedStorage또는EncryptedStorage를 상속받게해서 구현할 수 있을 것이다.
- 만약에 압축, 암호, 캐시 기능, 이렇게 3가지를 모두 제공하는 클래스, 그런 스토리지가 필요하다면, 그땐 어떻게 해야될까요?
- 압축, 암호, 캐시 스토리지 모두를 상속을 받아서 특정 클래스를 구현해야될까?
- 아니면
CachableEncryptedStorage(캐시, 암호) 스토리지를 상속받고CompressedStorage를 상속받아서 구현해야될까? - 이렇게 무언가 새로운 조합이 생길 때마다 하위 클래스가 증가하게 되고,
- 심지어 그 하위 클래스가 증가하는 것도 어떤 클래스를 상속받아서 구현을 해야될지 애매한 상황이 발생하기도 한다.
상속을 통한 재사용의 단점 3
- 상속 자체를 오용할 수 있다는 것이다.
public class Container extends ArrayList<Luggage> {
private int maxSize;
private int currentSize;
public Container(int maxSize) {
this.maxSize = maxSize;
}
public void put(Luggage lug) throws NotEnoughSpaceException {
if (!canContain(lug)) throw new NotEnoughSpaceException();
super.add(lug);
currentSize += lug.size();
}
public void extract(Luggage lug) {
super.remove(lug);
this.currentSize -= lug.size();
}
public boolean canContain(Luggage lug) {
return maxSize >= currentSize + lug.size();
}
}
- 지금 위에서 보시는
Container클래스는Luggage(수하물) 목록을 관리하는 클래스이다. Container클래스는 목록 관리 기능을 직접 구현하지 않고ArrayList를 상속 받아서 구현하고 있다.put메소드를 보면 상위 클래스(super)의add메소드를 이용해서 수하물을 목록에 추가하고 있고,extract메소드를 보면 상위 클래스(super)의remove메소드를 이용해서 수하물을 목록에서 제거하고 있다.
- 이
put메소드는Container가 최대 크기를 넘겨서Luggage를 보관하지 않도록 하려고currentSize를 사용해 관리하고 있고, canContain메소드를 보시면 새로운Luggage를 넣을 수 있는지 판단하기 위해서currentSize값을 사용하고 있다.
- 지금 이 코드만 보시면 크게 문제가 없는 것처럼 보인다. 잘 만든 것 같다.
- 실제로 잘 만든게 맞을까?

- 올바른 사용법을 왼쪽 위에 표시했다.
Container클래스에 정의된canContain,put메소드를 사용해서 수하물을 추가하는 코드이다.- 그런데 이클립스나 인텔리제이 같은 개발 도구에서
.을 누르면Container에 정의되어 있는 메소드만 나오는 것이 아니고, Container클래스가 상속받은ArrayList에 정의된 메소드 목록도 표시된단 말이죠?- 그래서 지금 여기서는
add메소드가 첫번째로 목록에 나온다. (위의 사진 참고) add메소드는 이름만 보면 마치Container에Luggage를 추가하는 기능처럼 보이잖아?
- 그래서 위와 같이
Container의Luggage추가에 대한 올바른 사용법을 제대로 숙지하지 않으면 - 위의 오른쪽 아래처럼
add메소드를 사용해서 수하물을 추가하는 코드를 작성할 수 있다. add메소드를 사용하면currentSize가 올바르게 계산되지 않잖아?- 그러다보니까
Luggage를 최대 크기보다 넘치게 추가할 수 있는 그런 논리적인 오류가 발생할 수 있게된다.
- 그러면 이는
add메소드를 잘못 사용한 사람의 문제일까? - 아니다.
- 사실 이건 잘못 사용할 가능성이 높은
Container클래스를 구현한 사람이 더 잘못한겁니다.
- 그럼 재사용 방법으로 상속을 사용할 때 발생하는 단점은 어떻게 없앨 수 있을까?
- 그것은 바로 상속 대신에 조립을 사용하는 방식을 사용하는 것이다.
상속의 단점 해결 방법 -> 조립
- 조립은 기능을 재사용하고 싶은 클래스가 있으면, 그 클래스의 객체를 필드나 필요한 시점에 생성해서 사용하는 것이다.
- 조립(Composition)
- 여러 객체를 묶어서 더 복잡한 기능을 제공
- 보통 필드로 다른 객체를 참조하는 방식으로 조립 또는 객체를 필요 시점에 생성/구함
- 예를 들어, 암호화 기능이 필요하다면, 아래 보시는 코드처럼
- 암호화 기능을 제공하는
Encryptor클래스를 상속 받아서 구현하는 것이 아니고 필드나 메소드에서Encryptor객체를 생성하고 재사용하는 것이다. - 즉, 다양한 기능을 제공하는 객체를 조립해서 더 복잡한 기능을 제공하는 것이다.
public class FlowController {
private Encryptor encryptor = new Encryptor(); // 필드로 조립
public void process() {
// ...
byte[] encryptedData = encryptor.encrypt(data);
// ...
}
}
조립을 통한 기능 재사용

- 조립을 통해 기능을 재사용하면, 앞에서 봤던 클래스가 증식하는 문제가 사라지게된다.
- 특정 기능을 재사용하고 싶으면, 오른쪽 그림에서 보는 것처럼, 해당 기능을 제공하는 클래스 객체를 만들어서 사용하면 되는 것이다.
- 예를 들어, 서명 기능을 제공하는 기능이 추가로 필요하다면,
- 서명 기능을 제공하는 클래스를 만드는 것이다.
Signer이렇게 서명 기능을 제공하는 클래스를 만들고, 그 클래스에 객체를 조립해서 재사용 하는 것이다.
- 조립하는 방식을 사용하면
Storage클래스에 하위 클래스가 없잖아? - 그러니까
Storage클래스의 내부를 변경하는 것도 상대적으로 더 수월해진다.
조립을 통한 기능 재사용
public class Container extends ArrayList<Luggage> {
private int maxSize;
private int currentSize;
// ...
public void put(Luggage lug) {
if (!canContain(lug))
throw new NotEnoughSpaceException();
super.add(lug);
currentSize += lug.size();
}
// ...
}
- 위 코드가 아래처럼..
public class Container {
private int maxSize;
private int currentSize;
private List<Luggage> luggages = new ArrayList();
// ...
public void put(Luggage lug) {
if (!canContain(lug))
throw new NotEnoughSpaceException();
luggages.add(lug);
}
// ...
}
- 위와 같이하면 상속을 오용하는 문제도 줄어든다.
- 위에 조립으로 변경한 코드를 보시면 더 이상
ArrayList의add메소드를 제공하지 않는다. - 그래서 앞에서 봤던 불필요한 기능까지 상속하면서 발생했던 문제가 해결된다.
상속보다는 조립(Composition over inheritance)
- 상속하기에 앞서 조립으로 풀 수 없는지 검토
- 진짜 하위 타입인 경우에만 상속 사용
- 이렇게 기능 재사용을 위해서 상속을 사용하지 않고 조립을 사용하는 것이 보통 장점이 더 많다.
- 그래서 상속 보다는 조립을 사용하라는 규칙이 있는 것이다.
- 지금 코드를 재사용하려고 상속을 고민하고 있다면, 그러지마시고 조립으로 맞출 수 없는지 검토를 해야됩니다.
- 진짜로 하위 타입인 경우에만 상속으로 구현을 해야된다.
- 예를 들어, 앞에서 봤던
Container클래스의 경우Container클래스는ArrayList의 한 종류가 아니잖아? - 단지, 목록을 관리하는 기능이 필요해서
ArrayList를 상속을 했단말야? - 이렇게 진짜로 하위 타입이 아닌데, 기능 재사용 때문에 상속을 오용하면 문제가 발생한다.
- 여러분이 상속을 고려하고 있다면, 어떤 기능을 구현하기 위해서 상속을 고려하고 있다면, 꼭 조립하는 방법으로 해결할 수 없는지, 먼저 검토해보시기 바랍니다.