Search
🙈

이펙티브 자바:: 아이템 20 <추상 클래스보다는 인터페이스를 우선하라>

Intro::

이펙티브 자바 정리본입니다.

결론

일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합합니다.
복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 고려해보면 좋습니다.
골격 구현은 가능한 한 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용할하도록 하는 것이 좋습니다.
인터페이스에 걸려 있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 흔하기 때문입니다.

인터페이스와 추상클래스

인터페이스
추상클래스
상속
선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급합니다.
추상 클래스의 하위 클래스가 되어야 합니다. → 계층화
확장
손쉽게 새로운 인터페이스를 구현해넣을 수 있습니다.
기존 클래스 위에 새로운 추상 클래스를 끼워넣기는 어렵다.
믹스인
적합합니다.
기존 클래스에 덧씌우기 어렵기에 합리적이지 않습니다.
계층구조
계층구조가 없는 타입 프레임워크를 만들 수 있습니다.
고도비만 계층클래스가 될 수 있다. 즉, 조합 폭발이 발생할 수 있다.
래퍼 클래스 관용구
인터페이스 기능을 향상시키는 안전하고 강력한 수단이 됩니다.
래퍼 클래스보다 활용도가 떨어지고 깨지기 쉽습니다.

인터페이스

디폴트 메서드

많은 인터페이스가 equals와 hashCode와 같은 Object의 메서드를 정의하고 있지만, 이들은 디폴트 메서드로 제공해서는 안됩니다.

인터페이스 코드 예시

public interface TestInterface { /** * public static final이 필드값의 기본이어서 생략 가능 */ public static final int one = 1; int two = 2; default void defaultFunction() { System.out.println("default function"); } /** * static void publicStaticFunction()와 동일 */ public static void publicStaticFunction() { System.out.println("publicStaticFunction"); } private static void privateStaticFunction() { System.out.println("privateStaticFunction"); } }
Java
복사
public class TestImpl implements TestInterface { public static void main(String[] args) { TestInterface t = new TestImpl(); t.defaultFunction(); TestInterface.publicStaticFunction(); // private access // TestInterface.privateStaticFunction(); } }
Java
복사

추상 골격 구현 클래스

인터페이스와 추상클래스의 장점을 모두 취할 수 있습니다.
인터페이스로는 타입을 정의합니다.
필요하면 디폴트 메서드도 제공합니다.
골격 구현 클래스에서 나머지 메서드들까지 구현합니다.
이를 템플릿 메서드 패턴이라고 합니다.
결론적으로 골격 구현 클래스의 경우에 인터페이스를 구현한 추상 클래스이기때문에 추상 클래스의 단점인 계층화에 대한 단점을 보완할 수 있고 실질적으로 메서드들도 정의 할 수 있기 때문에 유연한 클래스 구조를 가질 수 있습니다.
// 코드 20-1 골격 구현을 사용해 완성한 구체 클래스 (133쪽) 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]; // 오토박싱(아이템 6) } @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); } }
Java
복사
// 코드 20-2 골격 구현 클래스 (134-135쪽) 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(); } }
Java
복사

질문

인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고, 각 메서드 호출을 내부 클래스의 인스턴스에 전달해 골격 구현 클래스를 우회적으로 사용할 수 있다는게 정확히 이해가 안간다.

상속보다는 컴포지션을 사용한 방식이라고 이해하면 됩니다. 이러한 방식을 시뮬레이트한 다중 상속(simulated multiple inheritance)라 합니다.
interface A { void method1(); void method2(); } abstract class AbstractA implements A { @Override public void method1() { System.out.println("AbstractA method1"); } // method2는 서브클래스에서 구현해야 함. } public class MyClass implements A { // Private 내부 클래스가 골격 구현을 상속받음 private class PrivateInnerClass extends AbstractA { @Override public void method2() { System.out.println("PrivateInnerClass method2"); } } // 내부 클래스의 인스턴스 생성 private final PrivateInnerClass delegate = new PrivateInnerClass(); @Override public void method1() { // method1 호출을 내부 클래스 인스턴스에 위임 delegate.method1(); } @Override public void method2() { // method2 호출을 내부 클래스 인스턴스에 위임 delegate.method2(); } } public class Main { public static void main(String[] args) { A instance = new MyClass(); instance.method1(); // Prints "AbstractA method1" instance.method2(); // Prints "PrivateInnerClass method2" } }
Java
복사

인터페이스에서 디폴트 메서드를 다 정의할 수 있는데 굳이 골격 추상 클래스로도 만들어 줘야하는 이유가 있을까???

1. 상태를 가지는 기능:
인터페이스의 디폴트 메서드는 상태(필드)를 가질 수 없습니다. 모든 메서드는 인스턴스 변수가 없는 상태로 동작해야 합니다. 하지만 골격 추상 클래스는 인스턴스 변수를 가질 수 있기 때문에, 상태를 필요로 하는 메서드들을 구현할 수 있습니다. 예를 들어, 내부적으로 데이터를 저장하거나 관리해야 하는 경우 골격 추상 클래스가 더 유용합니다. 때문에 골격 추상 클래스의 기반 메서드로 제공을 합니다.
2. 다중 상속 문제 해결:
인터페이스는 다중 상속이 가능하지만, 여러 인터페이스에서 동일한 디폴트 메서드가 존재할 경우 충돌이 발생할 수 있습니다. 이러한 경우 서브클래스에서 직접 해결해줘야 합니다. 반면, 추상 클래스는 단일 상속만 가능하므로 이러한 충돌 문제가 발생하지 않으며, 코드 관리가 더 쉬워질 수 있습니다.
3. 코드 재사용 및 편리함:
골격 구현을 사용하면, 공통적으로 사용되는 코드를 한 곳에 모아 관리할 수 있어 코드 재사용성을 높일 수 있습니다. 골격 구현 클래스는 인터페이스에 비해 구현이 더 복잡하거나 메서드 간에 많은 상호작용이 필요한 경우 특히 유용합니다. 또한, 골격 구현을 사용하는 서브클래스는 필요한 메서드만 오버라이드하면 되기 때문에, 디폴트 메서드만으로 해결할 수 없는 복잡한 로직을 구현할 때 편리합니다.
4. 구현 세부사항의 은닉:
추상 클래스는 특정 메서드의 구현 세부사항을 서브클래스에게 감출 수 있습니다. 추상 클래스에서 보호된(protected) 메서드를 사용하여 서브클래스에 공개하고 싶지 않은 구현 세부사항을 숨길 수 있습니다. 반면, 인터페이스의 디폴트 메서드는 기본적으로 public이기 때문에 이러한 세부사항을 감추기가 어렵습니다.
// 인터페이스 정의 public interface MyCollection<E> { void add(E element); boolean addAll(Collection<? extends E> c); int size(); boolean isEmpty(); } // 골격 추상클래스 정의 public abstract class AbstractMyCollection<E> implements MyCollection<E> { // 수정 횟수를 추적하는 필드 protected int modCount = 0; @Override public boolean addAll(Collection<? extends E> c) { boolean modified = false; for (E element : c) { if (add(element)) { modified = true; modCount++; // 상태를 변경할 때마다 수정 횟수 증가 } } return modified; } @Override public boolean isEmpty() { return size() == 0; } } // 리스트 구현 public class MyArrayList<E> extends AbstractMyCollection<E> { private List<E> list = new ArrayList<>(); @Override public void add(E element) { list.add(element); modCount++; // 요소가 추가될 때마다 수정 횟수 증가 } @Override public int size() { return list.size(); } } // 집합 구현 public class MyHashSet<E> extends AbstractMyCollection<E> { private Set<E> set = new HashSet<>(); @Override public void add(E element) { if (set.add(element)) { modCount++; // 요소가 추가될 때마다 수정 횟수 증가 } } @Override public int size() { return set.size(); } }
Java
복사

References::

이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)