Search
🙉

이펙티브 자바:: 아이템 21 <인터페이스는 구현하는 쪽을 생각해 설계하라>

Intro::

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

결론

생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하는 것은 어려운 일입니다.
디폴트 메서드는 (컴파일이 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있습니다.
기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 합니다. 물론 새로운 인터페이스를 만드는 경우라면 표준적인 메서드 구현을 제공하는 것이 아주 유용한 수단이 될 수 있습니다.
디폴트 메서드는 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아닙니다.
디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야합니다.
인터페이스를 릴리스한 후라도 결함을 수정하는 게 가능한 경우도 있겠지만, 절대 그 가능성에 기대서는 안됩니다.
자바 7 까지의 세상에서는 모든 클래스가 현재의 인터페이스에 새로운 메서드가 추가될 일은 영원히 없다 고 가정하고 작성되었습니다. 자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었습니다. 자바 라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분의 상황에서 잘 작동하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하는 것은 어려운 일입니다.

코드 예시

// java 8의 Collection 인터페이스에 추가된 디폴트 메서드 default boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); boolean removed = false; final Iterator<E> each = iterator(); while (each.hasNext()) { if (filter.test(each.next())) { each.remove(); removed = true; } } return removed; }
Java
복사
위의 코드보다 더 범용적으로 구현하기도 어렵겠지만, 그렇다고 해서 현존하는 모든 Collection 구현체와 잘 어우러지는 것은 아닙니다. 대표적인 예로 org.apache.commons.collections4.collection.SynchronizedCollection입니다.

SynchronizedCollection

해당 클래스는 java.util.Collections.synchronizedCollection 정적 팩터리 메서드가 반환하는 클래스와 비슷합니다.
// java.util.Collections.synchronizedCollection static class SynchronizedCollection<E> implements Collection<E>, Serializable { @java.io.Serial private static final long serialVersionUID = 3053995032091335093L; @SuppressWarnings("serial") // Conditionally serializable final Collection<E> c; // Backing Collection @SuppressWarnings("serial") // Conditionally serializable final Object mutex; // Object on which to synchronize SynchronizedCollection(Collection<E> c) { this.c = Objects.requireNonNull(c); mutex = this; } SynchronizedCollection(Collection<E> c, Object mutex) { this.c = Objects.requireNonNull(c); this.mutex = Objects.requireNonNull(mutex); } ... @Override public boolean removeIf(Predicate<? super E> filter) { synchronized (mutex) {return c.removeIf(filter); } ... }
Java
복사
위의 코드에서는 removeIf가 재정의되어있는 상황이지만, 만약 인터페이스의 removeIf 디폴트 메서드가 추가된 상황이고 인터페이스 구현체가 해당 메서드를 재정의 하지 않은 경우라면 락 객체를 사용할 수 없게됩니다. 따라서 SynchronizedCollection 인스턴스를 여러 스레드가 공유하는 환경에서 한 스레드가 removeIf를 호출하면 예기치 못한 결과로 이어질 수 있습니다.
자바 플랫폼 라이브러리에서도 이런 문제를 예방하기 위해 일련의 조치를 취했습니다. 예를 들어 구현한 인터페이스의 디폴트 메서드를 재정의하고, 다른 메서드에서는 디폴트 메서드를 호출하기 전에 필요한 작업을 수행하도록 했습니다. 예컨대 Collections.synchronizedCollection이 반환하는 package-private 클래스들은 removeIf를 재정의하고, 이를 호출하는 다른 메서드들은 디폴트 구현을 호출하기 전에 동기화를 하도록 했습니다(위의 예시에서 removeIf 메서드).

References::

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