상속은 코드를 재사용할 수 있는 강력한 수단이지만 잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다.
상속은 캡슐화를 깨뜨릴 수 있다.
상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다는 말이다.
예를 한번 들어보도록 하겠다. 시나리오는 아래와 같다.
1. 우리에게는 HashSet 을 사용하는 프로그램이 있다.
2. HashSet은 처음 생성된 이후 원소가 몇 개 더해졌는지 알 수 있어야한다.
방법은 매우 간단하다.
몇개가 추가되었는지 알 수 있는 변수 1개와 추가되는 개수를 변수에 더해주기만 하면된다.
우리는 상속을 이용한다면 아래와 같이 작성할 수 있을것이다.
package item18;
import java.util.Collection;
import java.util.HashSet;
public class ExtendsHashSet<E> extends HashSet<E> {
private int addCount = 0;
public ExtendsHashSet() {
}
@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;
}
}
정상적으로 동작하는지 알아보기 위해 원소3개를 추가한 후 addCount를 출력해보겠다.
정상적으로 동작한다면 3이 찍혀야한다.
public final class main {
public static void main(String[] args) {
ExtendsHashSet extendsHashSet = new ExtendsHashSet();
extendsHashSet.addAll(Arrays.asList("1","2","3"));
System.out.println(extendsHashSet.getAddCount());
}
}
하지만 코드를 동작시켜본다면 6이 찍힌다.
그 원인은 HashSet의 addAll이 아래와 같이 구현 되어있기때문이다.
addAll을 부른객체의 add를 이용하여 원소를 추가하기때문에 중복으로 Count가 된것이다.
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
Iterator var3 = c.iterator();
while(var3.hasNext()) {
E e = var3.next();
if (this.add(e)) {
modified = true;
}
}
return modified;
}
이 경우에는 하위 클래스에서 addAll 메서드를 재정의 하지 않으면 문제를 고칠 수 있다.
하지만 상위 클래스인 HashSet의 자기자신의 add를 사용하는것이 아닌 다른 방식으로 변경된다면 ?
ExtendsHashSet의 addCount를 구하는과정에서 다르게 나오는 오류가 발생될 것이다.
누가 이 오류를 보고 "상위클래스가 변경됐나보다" 하고 단번에 생각하겠는가 ?
그렇기에 이러한 오류는 찾기 굉장히 어려운오류를 발생시킬 여지를 주게된다.
메소드 재정의가 문제라면 메소드를 새로 만들면 되지않을까? 라고 생각할 수 있다.
메소드 재정의보다 안전한것은 맞지만 상위 클래스에 하위클래스에서 새로만든 메소드와 이름이 같은 메소드가 추가된다면 어떻게 되겠는가 ?
그렇게 된다면 또 오류가 발생할 것이다.
이것 또한 찾기 어려운 오류를 발생시킬 여지가 있다.
그렇다면 어떻게 해야하는가?
기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하도록 하면된다.
기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션이라고 한다.
새 클래스의 인스턴스 메소드들은 기존 클래스의 대응하는 메소드를 호출해 그 결과를 반환한다.
이 방식을 forwarding이라고 하며 새 클래스의 메소드들을 forwarding method 라고 부른다.
예시는 아래와 같다.
전달클래스 ForwardingSet 클래스
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
@Override
public int size() {
return s.size();
}
@Override
public boolean isEmpty() {
return s.isEmpty();
}
@Override
public boolean contains(Object o) {
return s.contains(o);
}
@Override
public Iterator<E> iterator() {
return s.iterator();
}
@Override
public Object[] toArray() {
return s.toArray();
}
@Override
public <T> T[] toArray(T[] ts) {
return s.toArray(ts);
}
@Override
public boolean add(E e) {
return s.add(e);
}
@Override
public boolean remove(Object o) {
return s.remove(o);
}
@Override
public boolean containsAll(Collection<?> collection) {
return s.containsAll(collection);
}
@Override
public boolean addAll(Collection<? extends E> collection) {
return s.addAll(collection);
}
@Override
public boolean retainAll(Collection<?> collection) {
return s.retainAll(collection);
}
@Override
public boolean removeAll(Collection<?> collection) {
return s.removeAll(collection);
}
@Override
public void clear() {
s.clear();
}
}
래퍼 클래스
(다른 Set 인스턴스를 감싸고 있다는 뜻에서 이러한 클래스를 래퍼 클래스라고 부른다)
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedHashSet(Set<E> s) {
super(s);
}
public int getAddCount(){
return addCount;
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> collection) {
addCount+=collection.size();
return super.addAll(collection);
}
}
그러면 상속은 언제 써?
클래스 B가 클래스 A와 is-a 관계일 때만 클래스 A를 상속해야한다.
상속하고자 할때 B가 100% A 인가?를 생각해보자.
B가 A인지 확신할 수 없다면 B는 A를 상속해서는 안되고 A를 private 인스턴스로 두어야한다.
'개발 > JAVA' 카테고리의 다른 글
[effective java 3/E] 제네릭 <로 타입은 사용하지 말라> (0) | 2020.07.11 |
---|---|
[effective java 3/E] 추상 클래스보다는 인터페이스를 우선하라. (0) | 2020.07.08 |
[effective java 3/E] equals는 일반 규약을 지켜 재정의하라. (0) | 2020.07.05 |
[effective java 3/E] 자원을 직접 명시하지말고 의존 객체 주입을 사용하라 (2) | 2020.06.28 |
[effective java 3/E] private 생성자나 열거타입으로 싱글턴임을 보증하라 (0) | 2020.06.26 |
댓글