인터페이스의 세상은 2개로 나누어진다.
"현재의 인터페이스에 새로운 메서드가 추가될 일은 영원히 없다"고 가정하고 모든 클래스를 작성하던 자바7까지의 세상과 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드가 제공된 세상이다.
📌 자바 8 이후의 인터페이스
default method가 추가되면서 새로운 인터페이스를 구현하지 않아서 발생하는 컴파일 에러를 잡을 수 있게 되었다.
하지만 default method에도 위험은 따른다.
자바 8의 Collection 인터페이스에 추가된 removeIf 메서드를 예로 생각해보자
이 메서드는 주어진 불리언 함수(predicate)가 true를 반환하는 모든 원소를 제거한다.
- 디폴트 구현은 반복자를 이용해 순회하면서 각 원소를 인수로 넣고 predicate를 호출한다.
- predicate가 true를 반환하면 반복자의 remove 메서드를 호출하여 그 원소를 제거한다.
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;
}
그렇다면 이 removeIf 메서드가 정말로 하위 호환성을 지켜줄 것인가?
apache commons-collections의 4.3 버전 이하의 구현체를 보면 removeIf 메서드가 존재하지 않는다.
즉 자바 1.8 이전에 만들어진 라이브러리인데, SynchronizedCollection 클래스가 정상 작동을 할 수 있을까?
그걸 진짜 테스트 하신 분들이 있네...방법 찾아보다가 감탄했습니다.
참고로 4.4 버전부터는 SynchronizedCollection에서 removeIf()를 다음과 같이 재정의하고 있어서 사용 가능하다.
public boolean removeIf(Predicate<? super E> filter) {
synchronized(this.lock) {
return this.decorated().removeIf(filter);
}
}
4.3 이하에서는 removeIf를 재정의 하지 않아서 디폴트 구현을 물려받아 모든 메서드 호출을 알아서 동기화해주지 못한다.
removeIf의 구현은 동기화에 대해 아무것도 모르므로 락 객체를 사용할 수 없으며,
SynchronizedCollection 인스턴스를 여러 스레드가 공유하는 환경에서 removeIf를 호출하면 ConcurrentModificationException이 발생하거나 예상치 못한 결과로 이어질 수 있다.
💡 디폴트 메서드는 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다.
기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피하자.
반면, 새로운 인터페이스를 만드는 경우라면 표준적인 메서드 구현을 제공하는데 아주 유용한 수단이 된다.
또한 그 인터페이스를 더 쉽게 구현해 활용할 수 있게끔 해준다. (Item 20)
디폴트 메서드는 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아님을 명심하자.
💡 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 한다.
새로운 인터페이스라면 릴리즈 전에 반드시 테스트를 거쳐야 한다.
적어도 서로 다른 방식으로 세 가지는 구현해보자.
각 인터페이스의 인스턴스를 다양한 작업에 활용하는 클라이언트도 여러 개 만들어 봐야 한다.
인터페이스를 릴리스한 후라도 결함을 수정하는 게 가능한 경우도 있겠지만, 절대 그 가능성에 기대지 마라.