자바를 하는 사람은 상속(계승)을 모를 수가 없죠.
코드의 재사용을 돕는 강력한 도구입니다.
하지만 상속을 적절하게 사용하지 못한 소프트웨어는 깨지기 쉬운데요. 그래서 상속에는 그에 걸맞는 문서가 있는 것이 안전합니다.
이펙티브 자바에서는 먼저 상속의 단점을 나열하고 있습니다.
먼저 메소드와 달리 상속은 캡슐화 원칙을 위반합니다. 상위 클래스를 상속한 하위클래스는 상위클래스에 의존적입니다.
상위 클래스가 수정되면, 하위 클래스의 코드도 필연적으로 수정되어야 하죠.
캡슐화의 조건을 깨뜨리고 있습니다.
예제 코드로 한번 살펴봅시다.
package dao;
import java.util.Collection;
import java.util.HashSet;
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 += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
HashSet을 상속하는 클래스입니다.
이 클래스 코드를 아래와 같이 구현해서 실행해 봅시다.
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
결과는 6이 나올 것입니다.
3이 기대되어야 할 것입니다.
왜 그럴까요?
그림을 보면서 살펴봅시다.
실행의 과정은 이런식입니다.
여기서 중요한 것은 두번째와 마지막 단계입니다.
HashSet의 addAll 메소드는 add 메소드를 통해 구현이 됩니다.
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
HashSet의 내부모습입니다.
객체의 갯수만큼 add를 호출하여 추가하는 형식이죠.
이러한 사실은 HashSet의 명세에 실려있지 않다고 이펙티브 자바에서는 말하고 있습니다.
InstrumentedHashSet의 addAll 메소드를 삭제하면 정상작동하겠지요.
하지만 상위 클래스의 addAll을 사용한다는 것 자체가 의존성을 띤다는 것을 의미합니다.
캡슐화와는 멀어지게 됩니다.
이러한 문제를 막기 위해서 기존 메소드를 Override 하는 대신 새로운 메소드를 만들면 된다고
생각할 수 있을 수 있습니다.
하지만, 만약 새로운 릴리즈가 되어 메소드가 추가되었을 때
재수없게도 메소드의 시그니처(이름 + 매개변수)가 하위클래스와 같을 때
반환 타입만 다르다면 그 하위클래스는 컴파일 에러가 나게 됩니다.
Override 실패로 뜨겠죠?
이처럼 상속을 사용하는 사용자가 클래스의 내부 구조를 모르는 경우
릴리즈되어 클래스가 조금이라도 변경이 바뀌는 경우
명세가 자세하지 못한 경우 등의 상황에서 상속은 깨지기 쉬운 약한 클래스가 될 수밖에 없습니다.
이런 문제를 해결할 방법이 하나 있습니다.
이전에도 소개했던 상속 대신 구성을 사용하는 방법입니다.
EFJ에서 소개한 코드를 한번 봅시다.
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
public void clear() {
s.clear();
}
public boolean contains(Object o) {
return s.contains(o);
}
public boolean isEmpty() {
return s.isEmpty();
}
public int size() {
return s.size();
}
public Iterator<E> iterator() {
return s.iterator();
}
public boolean add(E e) {
return s.add(e);
}
public boolean remove(Object o) {
return s.remove(o);
}
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
public Object[] toArray() {
return s.toArray();
}
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean equals(Object o) {
return s.equals(o);
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public String toString() {
return s.toString();
}
}
위 코드는 Set을 둘러싸고 있기에 포장(Wrapper) 클래스, 아래코드가 전달(forwarding) 클래스입니다.
이전 포스트에서 데코레이터 패턴을 다룬 적이 있었는데, 구성은 그 패턴을 말하기도 합니다.
InstrumentedSet 클래스는 Set 인터페이스를 구현하며, Set 객체를 매개변수르 받는 생성자를 하나 가집니다.
본래 상속을 이용한 접ㄱ른법은 한 클래스에만 ㅈ거용이 가능하고, 상위 클래스마다 별도의 생성자를 구현해줘야하지만
포장클래스 기법을 사용하면 어떤 Set도 원하는 대로 수정이 가능하고, 이미 있는 생성자도 그대로 사용할 수 있습니다.
Set<Date> s = new InstrumentedSet<Sate>(new TreeSet<Date>(cmp));
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));
.
구성 패턴은 단점이 별로 없지만, 콜백 패턴에서는 사용하기 어렵습니다.
콜백은 자신의 주소를 다른 객체에 넘기고 나중에 호출받는 형태의 패턴인데
문제는 객체는 자신이 포장되어야하는 객체라는 사실을 알 수 없다는 것입니다.
때문에 콜백 사용할 때는 구성은 사용해서는 안됩니다.
'프로그래밍 > Java' 카테고리의 다른 글
이펙티브 자바 규칙 17 - 계승을 위한 문서를 갖추거나, 그럴 수 없다면 금지하자 (0) | 2018.02.15 |
---|---|
이펙티브 자바 규칙 15 - 변경 가능성을 최소화하라 (0) | 2018.02.12 |
디자인 패턴 - 프록시 패턴(Proxy pattern) (0) | 2018.02.12 |