DEV ℧ Developer Diary

[EffectiveJava] item21 - 인터페이스는 구현하는 쪽을 생각해 설계하라

인터페이스는 구현하는 쪽을 생각해 설계하라

디폴트 메서드

자바 8전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.

하지만 디폴트 메서드가 생긴 이후에 기존 인터페이스에 메서드를 추가할 수 있게 되었다.

디폴트 메서드를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.

디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의없이 무작정 ‘삽입’될 뿐이다.

람다의 디폴트 메서드

자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었다. 주로 람다를 활용하기 위해서다. 자바 라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분 상황에서 잘 작동한다.

하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.

아래는 디폴트 메서드의 안좋은 예시이다.

Collection 인터페이스에 추가된 removeIf의 메서드인데, boolean 함수가 true로 반환하는 모든 원소를 제거하는 메서드이다.

default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean result = false;
    for (Iterator<E> it = iterator(); it.hasNext();){
        if(filter.test(it.next())){
            it.remove();
            result = true;
        }
    }
    return result;
}

이 코드는 org.apache.commons.collections4.collection.SynchronizedCollection에는 removeIf가 재정의 되어있지 않다.

해당 클래스는 아파치 커먼즈 라이브러리의 일부이며, java.util의 Collections.synchronizedCollection 정적 팩터리 메서드가 반환하는 클래스와 비슷하다.

아파치 버전은 클라이언트가 제공한 객체로 락을 거는 능력을 추가로 제공한다.

아파치의 SynchronizedCollection는 지금도 관리가 되고 있지만 이펙티브 자바 3판이 나오는 시점에서는 해당 removeIf를 재정의하고 있지 않아 구현 로직이 어긋나고 만다.

위에서 구현된 removeIf는 동기화에 관해 아무것도 정의되어 있지 않으므로 락객체를 사용할 수 없다.

따라서 SynchronizedCollection 인스턴스를 멀티 스레드 환경에서 한 스레드가 removeIf를 호출하면 ConcurrentModificationException이 발생하거나 의도치 않은 결과로 이어질 수있다.

디폴트 메서드의 주의할점

디폴트 메서드는 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다. 그러므로, 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다.

또한 기존 구현체들과 충돌하지 않는지도 정확하게 파악해야 한다.

설계시 주의할점

인터페이스를 설계 할 때는 여전히 세심한 주의를 기울여야 한다. 디폴트 메서드로 기존 인터페이스에 새로운 메서드를 추가하면 커다란 위험이 딸려온다.

새로운 인터페이스라면 릴리즈전에 반드시 테스트를 거쳐야한다.

수많은 개발자가 나름의 방식으로 인터페이스를 구현할 것이니, 각자 디폴트 메서드를 넣기 전에 서로 다른 방식으로 최소한 세가지는 구현해 봐야한다.

인스턴스를 다양한 작업에 활용하는 클라이언트도 여러개 만들어봐야한다.

새 인터페이스가 의도한 용도에 잘 부합하는지 확인하는 길은 이렇게 험난하다.

인터페이스를 릴리스한 후라도 결함을 수정하는 게 가능할 수 는 있지만, 절대 그 가능성에 기대어선 안된다.