상속은 코드를 재사용 할 수 있도록 돕는 강력한 도구이지만, 항상 최선이라 할 수 없다.
제대로 구현하지 못한 경우 문제를 유발할 가능성이 크기 때문이다.
그리고 같은 패키지 내부에 존재하는 클래스를 상속받아야한다.
상속은 일반적으로 캡슐화 원칙을 위반한다.
그 이유는 상위클래스의 일부 개념이 변경될 경우, 하위클래스는 그대로 영향을 받을 수 밖에 없기 때문이다.
이를 예방하기 위해서는 문서를 제대로 구현해 놓아야 한다.
문제가 야기될 수 있는 상황은 다음과 같다.
1. 기존의 재정의 하여 사용하였을 경우.
-> 문제가 될 수 있는 상황을 예를 들면, HashSet을 상속하고 add관련 메서드를 재정의하여 객체를 추가한 횟수를 확인하는 로직을 추가하는 로직을 만들어보자
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class Effective16<E> extends HashSet<E> { private int addCount = 0; @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 getCount() { return addCount; } } | cs |
다음과 같이 HashSet<E>을 상속받아 기존의 add 관련 함수를 재정의 한다음 다음과 같이 호출하였을 때, 출력되는 count값은 어떻게 될까?
1 2 3 4 5 6 7 | public static void main(String args[]) { Effective16<String> set = new Effective16<>(); set.addAll(Arrays.asList("babo", "cjung")); System.out.println(set.getCount()); } | cs |
위의 로직을 보면 당연히 count가 2가 나올 것 같지만 4가 나온다.
그 이유는 HashSet에서 제공하는 addAll은 내부적으로 all을 사용하기 때문에 재정의한 all에 접근하여 카운트가 더 증가되는 것이다.
2. 다른 이름을 가진 새로운 메서드를 사용한다.
-> 1번의 방법보다 조금더 안전하긴 하지만 만약 상위 HashSet 클래스에 동일한 메서드나 반환형만 다른 메서드 타입이 생성될 경우 문제를 야기 할 수 있기 때문에 문제가 될 수 있다.
해결 방법
기존 클래스를 상속하는 대신 새로운 클래스에 기존 클래스 객체를 참조하는 private필드를 하나 두는 composition이라는 기법을 사용한다. (Has a 기법)
이렇게 가지고 있는 필드에 접근하여 필요한 메서드만 호출하여 사용하는 방법이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) { this.s = s; } @Override public boolean add(E arg0) { return s.add(arg0); } @Override public boolean addAll(Collection<? extends E> arg0) { return s.addAll(arg0); } } public class Effective16<E> extends ForwardingSet<E> { public Effective16(Set<E> s) { super(s); } private int addCount = 0; @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 getCount() { return addCount; } } | cs |
정리하면
상속은 캡슐화의 원칙을 침해 하므로 문제를 발생시킬 소지가 있다는 것을 잊지말아야한다. 그러므로 상속은 하위 클래스가 상위 클래스의 하위 자료형이 확실하고, IS-A관계가 성립되는 경우에만 사용한다.
그렇기에 해당 기능을 사용해야 하는 경우에는 has a 관계를 사용하면 안되는지 다시 한번 확인 해 보는 것이 좋다.
출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙16 인용.
'JAVA > Effective Java' 카테고리의 다른 글
클래스와 인터페이스 - 규칙 18 추상 클래스 대신 인터페이스를 사용하라. (0) | 2018.05.29 |
---|---|
클래스와 인터페이스 - 규칙 17 계승을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 계승을 금지하라. (0) | 2018.05.29 |
클래스와 인터페이스 - 규칙 15 변경 가능성을 최소화하라 (0) | 2018.05.29 |
클래스와 인터페이스 - 규칙 14 public 클래스 안에는 public 필드를 두지 말고 접근자 메서드를 사용하라 (0) | 2018.05.29 |
클래스와 인터페이스 - 규칙 13 클래스와 멤버의 접근권한은 최소화하라. (0) | 2018.05.29 |