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"); } }
package item20; public class TestImpl implements TestInterface { public static void main(String[] args) { TestInterface t = new TestImpl(); t.defaultFunction(); TestInterface.publicStaticFunction(); // private access // TestInterface.privateStaticFunction(); } }
추상 골격 구현 클래스
인터페이스와 추상클래스의 장점을 모두 취할 수 있습니다.
- 인터페이스로는 타입을 정의합니다.
- 필요하면 디폴트 메서드도 제공합니다.
- 골격 구현 클래스에서 나머지 메서드들까지 구현합니다.
이를 템플릿 메서드 패턴이라고 합니다.
결론적으로 골격 구현 클래스의 경우에 인터페이스를 구현한 추상 클래스이기때문에 추상 클래스의 단점인 계층화에 대한 단점을 보완할 수 있고 실질적으로 메서드들도 정의 할 수 있기 때문에 유연한 클래스 구조를 가질 수 있습니다.
// 코드 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); } }
// 코드 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(); } }
질문
인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 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" } }
인터페이스에서 디폴트 메서드를 다 정의할 수 있는데 굳이 골격 추상 클래스로도 만들어 줘야하는 이유가 있을까???
1. 상태를 가지는 기능:
인터페이스의 디폴트 메서드는 상태(필드)를 가질 수 없습니다. 모든 메서드는 인스턴스 변수가 없는 상태로 동작해야 합니다. 하지만 골격 추상 클래스는 인스턴스 변수를 가질 수 있기 때문에, 상태를 필요로 하는 메서드들을 구현할 수 있습니다. 예를 들어, 내부적으로 데이터를 저장하거나 관리해야 하는 경우 골격 추상 클래스가 더 유용합니다. 때문에 골격 추상 클래스의 기반 메서드로 제공을 합니다.
2. 다중 상속 문제 해결:
인터페이스는 다중 상속이 가능하지만, 여러 인터페이스에서 동일한 디폴트 메서드가 존재할 경우 충돌이 발생할 수 있습니다. 이러한 경우 서브클래스에서 직접 해결해줘야 합니다. 반면, 추상 클래스는 단일 상속만 가능하므로 이러한 충돌 문제가 발생하지 않으며, 코드 관리가 더 쉬워질 수 있습니다.
3. 코드 재사용 및 편리함:
골격 구현을 사용하면, 공통적으로 사용되는 코드를 한 곳에 모아 관리할 수 있어 코드 재사용성을 높일 수 있습니다. 골격 구현 클래스는 인터페이스에 비해 구현이 더 복잡하거나 메서드 간에 많은 상호작용이 필요한 경우 특히 유용합니다. 또한, 골격 구현을 사용하는 서브클래스는 필요한 메서드만 오버라이드하면 되기 때문에, 디폴트 메서드만으로 해결할 수 없는 복잡한 로직을 구현할 때 편리합니다.
4. 구현 세부사항의 은닉:
추상 클래스는 특정 메서드의 구현 세부사항을 서브클래스에게 감출 수 있습니다. 추상 클래스에서 보호된(protected) 메서드를 사용하여 서브클래스에 공개하고 싶지 않은 구현 세부사항을 숨길 수 있습니다. 반면, 인터페이스의 디폴트 메서드는 기본적으로 public이기 때문에 이러한 세부사항을 감추기가 어렵습니다.
References::
이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)
Loading Comments...