해당 포스트는 "Effective Java" 책의 내용을 요약한 것이다.
※ extends 대신 composition을 사용해라
: 상속을 제대로 사용하지 못하면 소프트웨어는 많이 불안정해진다. 상속은 단일 패키지 안에서만 해야 안전하다. 하위 클래스는 상위 클래스에 많이 의존하게 된다. 상위 클래스의 코드가 수정되면 하위 클래스의 코드도 수정해야하는 경우가 많다. 상속은 중요하게 캡슐화를 위반하기 때문이다. 구체적인 사례는 다음과 같다. 다음 코드는 HashSet을 상속해 삽입된 요소의 수를 추적하는 클래스이다.
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; } }
//메인 함수 InstrumentedHashSet<String> s = new InstrumentedHashSet<>(); s.addAll(Arrays.asList("Snap","Crackle","Pop")); System.out.println(s.getAddCount());
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> { // Set 객체 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 new Object[0]; } 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(); } }
InstrumentedSet 클래스는 계승 대신 구성을 사용하는 포장(wrapper) 클래스이다. 다른 Set 객체를 포장하고 있기 때문이다. ForwardingSet 클래스는 재사용 가능한 전달(forwarding) 클래스이다. 위와 같이 구현하면 안정적이고 유연성도 좋다. 또한 Set 인터페이스가 있어 더 안정적이다. InstrumentedSet 클래스는 어떤 Set 객체를 인자로 받아 필요한 기능을 갖춘 다른 Set 객체로 변환할 수 있다. 이러한 접근법은 계승의 경우 한 클랫에만 적용이 가능하고 별도의 생성자를 만들어야 한다. 하지만 포장 클래스를 사용하면 어떤 Set 구현도 원하는 대로 수정할 수 있고 이미 있는 생성자도 그대로 사용할 수 있다. 다음과 같이 말이다.
Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp)); Set<E> s1 = new InstrumentedSet<E>(new HashSet<E>(ca));
또한 이미 사용 중인 객체에 일시적으로 원하는 기능을 넣을 수도 있다.
static void walk(Set<Dogs> dogs){ InstrumentedSet<Dog> iDogs = new InstrumentedSet<Dog>(dogs); .... // 이 메서드 안에서는 dogs가 아닌 iDogs를 사용한다 }
상속은 "IS-A" 관계가 확실할 때만 사용해야 한다. 그렇지 않으면 구성을 사용해야 한다. 구성 대신 상속을 사용할 때 계승할 클래스에 문제가 있는 지 확인해야 하고 그렇다면 그 문제들이 계속 새 API의 일부가 되어도 상관 없는지 확인해야 한다. 상위 클래스의 문제는 하위 클래스에 전파된다. 반면 구성은 그런 약점을 감추는 새로운 API를 만들 수 있다.
계승은 캡슐화 원칙을 깨 IS-A 관계가 확실할 때만 사용해야 한다. IS-A 관계가 성립하더라도 하위 클래스가 상위 클래스와 다른 패키지에 있거나 상위 클래스가 상속을 고려해 만든 클래스가 아니라면 구성을 사용하는 게 좋다. 또한 포장 클래스 구현에 인터페이스까지 있다면 더 좋다.
※ extends을 위한 설계와 문서를 갖추거나 ,그럴 수 없다면 상속을 금지하라
: 재정의 가능 메서드(public이나 protected로 선언된 모든 메서드)에 대해서 내부적으로 어떻게 사용하는 지(내부 동작 원리) 반드시 문서에 남겨야 한다. 다음은 java.util.AbstractCollection remove 메서드의 주석이다.
This implementation iterates over the collection looking for the specified element. If it finds the element, it removes the element from the collection using the iterator's remove method.
위 주석을 보면 iterate 메서드를 재정의하면 remove가 영향을 받는 다는 걸 알 수 있다. 반면 해당 포스트의 첫 번째 예제에서 HashSet의 하위 클래스를 만드는데 add를 재정의하면 addAll에 무슨 일이 생길지 알 수 없다. 따라서, 하위 클래스를 안전하게 만들기 위해서 문서에 반드시 구현 세부사항을 기술해야 한다.
계승을 허용하라면 반드시 따라야할 규칙 사항이 있다. 생성자는 재정의 가능 메서드를 호출해서는 안 된다. 상위 클래스의 생성자는 하위 클래스의 생성자보다 먼저 실행이 되 상위 클래스에서 하위 클래스의 재정의 메서드가 호출된다. 이는 심각한 오류를 유발한다. 클래스가 Serializable 인터페이스를 사용할 경우 readObject에서 재정의 가능한 메서드를 호출 하지 않도록 해야 한다. 또한 클래스 내부적으로 재정의 가능 메서드를 사용하는 경우를 완전히 제거해야 한다. 그러면 클래스는 계승해도 안전한 클래스가 된다. 메서드를 재정의해도 다른 메서드에는 영향이 없기 때문이다. 만약 재정의 가능 메서드를 내부적으로 사용하는 코드는 다음과 같이 해라. 재정의 가능 메서드의 내부 코드를 private로 선언된 도움 메서드 안으로 옮기고 각각의 재정의 가능 메서드가 해당 메서드를 호출하게 하라. 그런 다음 재정의 가능 메서드를 호출하는 내부 코드는 전부 해당 private 도움 메서드 호출로 바꾸면 된다.
※ 추상 클래스 대신 인터페이스를 사용하라
: 인터페이스는 다양한 구현이 가능한 자료형을 정의할 수 있고 포장 클래스를 통해 안전하면서도 강력한 기능 개선이 가능하다. 추상 클래스는 상속 이외에 수단이 없다. 물론 인터페이스는 함수 구현을 하지 못하는 단점을 가지고 있다. 이런 단점을 해결하는 방안으로 추상 골격 구현 클래스가 있다. 추상 골격 구현 클래스에 대한 설명은 해당 문장 url에 있다. 추상 골격 구현 클래스는 인터페이스와 추상 클래스의 장점을 결합해 놓은 것이다. 골격 구현 클래스를 API에 포함시킬 수 있다면 제공하는 게 좋다.
인터페이스보다 추상 클래스로 정의하면 좋은 점은 추상 클래스가 더 발전시키기 쉽다는 거다. 만약 API에 새로운 메서드를 추가하고 싶다면 추상 클래스는 기본적인 구현 메서드를 담으면 된다. 하지만 인터페이스의 경우 메서드를 추가하면 해당 API를 구현한 다른 클라이언트 코드들은 오류를 발생시킬 것이다. 따라서 유연하고 강력한 API를 만들 때는 인터페이스를 사용하는 게 좋고, 개선이 쉬운 API를 만들 때는 추상클래스를 만드는 게 좋다. 또한 인터페이스가 공개되고 수정이 거의 불가능하기 때문에 public 인터페이스는 조심해서 사용해야 한다.
참고로 인터페이스는 구현한 클래스의 기능을 담당하는 것이다. 인터페이스 안에 상수만 정의해 놓는 상수 인터페이스는 절대 사용해서는 안 된다. 사용해야 한다면 가장 관련된 클래스 내부에 public static final로 선언해 놓아야 한다.
'자바 > 자바 성능' 카테고리의 다른 글
제네릭 주의할 점(무인자 자료형, 무점검 경고, 제네릭 자료형/메서드) (0) | 2017.07.08 |
---|---|
함수 객체 (0) | 2017.07.07 |
변경 불가능 클래스 작성법 (0) | 2017.07.07 |
자바의 접근 권한과 효율적인 사용 방법 (0) | 2017.07.07 |
equals를 어떻게 재정의해야 하는가 (0) | 2017.07.06 |