인터페이스는 구현하는 쪽을 생각해 설계하라.
자바 8 이전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.
인터페이스에 메서드를 추가하면 보통은 컴파일 오류가 난다. 당연하게도 해당 인터페이스를 구현하는 클래스에 추가한 메서드가 있을 확률이 적기 때문이다.
그래서 등장한 것이 디폴트 메서드이다. 인터페이스 구현 클래스에서 재정의하지 않아도 이 디폴트 메서드의 구현을 쓸 수 있다.
그런데 자바 8 이전 버전에서도 인터페이스는 계속 쓰여 왔다. 그 당시에는 인터페이스에는 무조건 추상 메서드만 있을 것이라는 전제 하에 인터페이스를 구현해왔고, 그 상태로 디폴트 메서드를 구현한다면 무작정 삽입될 뿐이다.
자바 8 이후 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었다. 주로 람다를 활용하기 위함이다.
이렇게 무작정 추가된 디폴트 메서드들이 과연 아무런 문제가 없을까?
자바 라이브러리의 디폴트 메서드들은 코드 품질이 좋고 범용적이기 때문에 대부분의 경우 잘 작동하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하는 것은 어렵다.
removeIf
자바 8에서 Collection에 추가된 removeIf라는 디폴트 메서드를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
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;
}
- 매개변수로 주어지는 Predicate => boolean 함수가 참을 반환하는 모든 원소를 제거하는 메서드이다.
- 이터레이터를 이용해 순회하면서 각 원소를 인수로 넣어 predicate를 호출하고, 그 결과가 true라면 이터레이터의 remove 메서드를 호출해 제거하는 방식이다.
범용적이고 잘 구현되어 있는 것 같다.
하지만 한 가지 문제가 있다.
removeIf의 문제
만약 멀티 쓰레드 환경이라면?
removeIf()의 구현은 동기화에 관해 아무것도 모른다.
따라서 락 객체를 사용할 수 없다.
멀티 쓰레드 환경에서 removeIf()를 호출하면 ConcurrentModificationException이 발생하거나 다른 예기치 못한 결과로 이어질 수 있다.
예를 들면 한 쓰레드가 어떤 요소를 제거하는 도중 다른 쓰레드가 요소를 추가하거나 제거하면, 순회 중인 요소가 변경되어 예외가 발생하는 것이다.
락 객체
동시에 같은 값에 접근해 값을 변경하면 예상치 못한 값이 나오게 된다.
락 객체를 이용하면 락 객체를 획득한 쓰레드만 해당 자원에 접근이 가능한 구조인 것이다.
removeIf()의 해결
결론부터 말하자면 removeIf() 메서드를 재정의하면 된다.
Collections.SynchronizedCollection 클래스는 모든 메서드에서 주어진 락 객체로 동기화한 후 내부 컬렉션 객체에 기능을 위임하는 일종의 래퍼 클래스이다.
그런데 이 도서가 쓰여진 시점에는 removeIf() 메서드를 재정의하지 않고 있다. 만약 자바 8과 함께 사용된다면 removeIf() 호출 시 모든 메서드 호출을 알아서 동기화해주지 않는다는 것이다.
자바 플랫폼에 속하지 않는 제 3의 기존 컬렉션 구현체들(Apache SynchronizedCollection 등)은 이런 언어 차원의 인터페이스 변화에 발맞춰 수정될 기회가 없었고, 그 중 일부는 여전히 수정되지 않고 있다.
따라서 아래와 같이 재정의해볼 수 있다.
1
2
3
4
5
6
@Override
public boolean removeIf(final Predicate<? super E> filter) {
synchronized(lock) {
return decorated().removeIf(filter);
}
}
또 다른 주의점
- 디폴트 메서드는 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다.
- 흔한 일은 아니지만 가능성이 있다.
- 따라서 디폴트 메서드는 반드시 필요한 경우가 아니라면 피해야 한다.
- 새로운 인터페이스라면 릴리스 전에 반드시 테스트를 거쳐야 한다.
- 인터페이스를 릴리스한 후라도 결함을 수정하는게 가능한 경우도 있겠지만, 그 가능성에 기대선 안된다.
정리
인터페이스에 새로운 메서드를 추가하기 위해 디폴트 메서드라는 기능이 추가되었다.
하지만 디폴트 메서드는 예기치 못한 결함들이 발생할 수 있는 원인이 된다. 단순히 인터페이스의 구현만 생각하면 문제가 없지만, 기존에 구현하고 있던 클래스들에서 문제가 발생할 수 있다.
따라서 인터페이스를 설계할 때는 늘 구현 클래스를 생각해야하고, 만약 새로운 인터페이스나 디폴트 메서드를 추가했을 경우 많은 테스트를 거쳐야 한다.