-
Item 20. 추상 클래스보다는 인터페이스를 우선하라Book/Effective Java 3E 2022. 10. 29. 08:46반응형
자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스, 이렇게 두가지입니다. 자바8부터 인터페이스도 디폴트 메서드(default method)를 제공할 수 있게 되어, 이제는 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있습니다.
인터페이스는 믹스인(mixin) 정의에 안성맞춤입니다. 믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 '주된 타입' 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 줍니다. 예컨대 Comparable은 자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스입니다. 추상 클래스로는 믹스인을 정의할 수 없습니다. 자바는 단일 상속만 지원하니, 클래스 계층구조에는 믹스인을 삽입하기에 합리적인 위치가 없기 때문입니다.
인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있습니다. 예를 들어 가수(Singer) 인터페이스와 작곡가(Songwriter) 인터페이스가 있다고 하면, 가수 클래스가 Singer와 Songwriter 모두를 구현해도 전혀 문제되지 않습니다. 같은 구조를 클래스로 만들려면 가능한 조합 전부를 각각의 클래스로 정의한 고도비만 계층구조가 만들어질 것입니다. 속성이 n개라면 지원해야 할 조합의 수는 2의 n승개가 됩니다. 흔히 조합 폭발(combinatorial explosion)이라 부르는 현상입니다.
한편, 인터페이스와 추상 골격 구현(skeletal implementation) 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법도 있습니다. 인터페이스로는 타입을 정의하고, 필요하면 디폴트 메서드 몇 개도 함께 제공합니다. 그리고 골격 구현 클래스는 나머지 메서드들까지 구현합니다. 바로 템플릿 메서드 패턴입니다.[Gamma95] 관례상 인터페이스 이름이 Interface라면 그 골격 구현 클래스의 이름은 AbstractInterface로 짓습니다. 좋은 예로, 컬렉션 프레임워크의 AbstractCollection, AbstractSet, AbstractMap 각각이 핵심 컬렉션 인터페이스의 골격 구현입니다.
다음 코드는 완벽히 동작하는 List구현체를 반환하는 정적 팩터리 메서드로, AbstractList 골격 구현으로 활용했습니다.
package effectivejava.chapter4.item20; import java.util.*; // 코드 20-1 골격 구현을 사용해 완성한 구체 클래스 public class IntArrays { static List<Integer> intArrayAsList(int[] a) { Objects.requireNonNull(a); // 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다. // 더 낮은 버전을 사용한다면 <Integer>로 수정하자. return new AbstractList<>() { @Override public Integer get(int i) { return a[i]; // 오토박싱 } @Override public Integer set(int i, Integer val) { int oldVal = a[i]; a[i] = val; // 오토언박싱 return oldVal; // 오토박싱 } @Override public int size() { return a.length; } }; } public static void main(String[] args) { int[] a = new int[10]; for (int i = 0; i < a.length; i++) a[i] = i; List<Integer> list = intArrayAsList(a); Collections.shuffle(list); System.out.println(list); } }
골격 구현 작성은 인터페이스에서 다른 메서드들의 구현에 사용되는 기반 메서드들을 선정합니다. 이 기반 메서드들은 골격 구현에서는 추상 메서드가 될 것입니다. 그 다음으로, 기반 메서드들을 사용해 직접 구현할 수 있는 메서드를 모두 디폴트 메서드로 제공합니다. 단, equals와 hashCode 같은 Object의 메서드는 디폴트 메서드로 제공하면 안 된다는 사실을 항상 유념해야 합니다.
간단한 예로 Map.Entry 인터페이스를 보면 getKey, getValue는 확실히 기반 메서드이며, 선택적으로 setValue도 포함할 수 있습니다. Object 메서드들은 디폴트 메서드로 제공해서는 안 되므로, 해당 메서드들은 모두 골격 구현 클래스에 구현합니다. toString도 기반 메서드를 사용해 구현하였습니다.
package effectivejava.chapter4.item20; import java.util.*; // 코드 20-2 골격 구현 클래스 public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> { // 변경 가능한 엔트리는 이 메서드를 반드시 재정의해야 한다. @Override public V setValue(V value) { throw new UnsupportedOperationException(); } // Map.Entry.equals의 일반 규약을 구현한다. @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry) o; return Objects.equals(e.getKey(), getKey()) && Objects.equals(e.getValue(), getValue()); } // Map.Entry.hashCode의 일반 규약을 구현한다. @Override public int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } @Override public String toString() { return getKey() + "=" + getValue(); } }
골격 구현은 기본적으로 상속해서 사용하는 걸 가정하므로 Item 19에서 이야기한 설계 및 문서화 지침을 모두 따라야 합니다.
일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합합니다.
복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해보자.
골격 구현한 '가능한 한' 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋다.
'가능한 한'이라고 한 이유는, 인터페이스에 걸려 있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문이다.
[참고 정보]
이펙티브 자바 Effective Java 3/E 도서 [조슈아 블로크 저]
반응형'Book > Effective Java 3E' 카테고리의 다른 글
Item 22. 인터페이스는 타입을 정의하는 용도로만 사용하라 (0) 2022.11.02 Item 21. 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) 2022.10.29 Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) 2022.10.26 Item 18. 상속보다는 컴포지션을 사용하라 (0) 2022.10.22 Item 17. 변경 가능성을 최소화하라 (0) 2022.10.22