Study/이펙티브 자바 / / 2023. 9. 23. 14:56

[Effective Java 3E] 인터페이스는 구현하는 쪽을 생각해 설계하라

💥 개요

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

인터페이스에 메서드를 추가하면 대부분 컴파일 오류가 발생하는데 추가된 메서드가 우연히 기존 구현체에 이미 존재할 가능성은 아주 낮기 때문입니다. 자바 8부터 default method가 추가 되어 인터페이스에 메서드를 추가할 수 있게 되었지만 위험이 완전히 사라진 것은 아닙니다.

 

🔍 default method

디폴트 메서드(default method)를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 됩니다. 이처럼 기존 인터페이스에 메서드를 추가할 수 있게 되었지만, 모든 기존 구현체가 매끄럽게 연동되라는 보장은 없습니다. 디폴트 메서드는 구현 클래스에 대해 아무것도 모른채 합의없이 무작정 '삽입' 될 뿐입니다.

 

🎈자바 8에서 추가된 디폴트 메서드

핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가 되었습니다. 주로 람다를 활용하기 위해서인데 자바 라이브러리의 디폴트 메서드는 코드 품질이 높고, 범용적이라 대부분 잘 작동합니다. 하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법 입니다.

 

자바 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;
    }

자바 8의 Collection 인터페이스의 removeIf 메서드는 주어진 불리언 함수가 true를 반환하는 모든 원소를 제거합니다.

이 코드는 범용적이지만, 모든 Collection 구현체와 잘 어우러지는것은 아닙니다. 

org.apache.commons.collections4.collection.SynchronizedCollection은 대표적인 예시입니다.

이 클래스는 java.util의 Collections.synchronizedCollection 정적 팩터리 메서드가 반환하는 클래스와 비슷합니다. 아파치 버전은 클라이언트가 제공한 락을 거는 능력을 추가로 제공합니다. 즉, 모든 클래스에서 주어진 락 객체로 동기화 후 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스 입니다.

이팩티브 자바 3판이 나온 시점엔 removeIf를 재정의하지 않았고 이 클래스를 자바 8과 함께 사용하면 모든 메서드 호출을 알아서 동기화해주지 못합니다. 따라서 해당 클래스의 인스턴스를 여러 스레드가 공유하는 환경에서 한 스레드가 removeIf를 호출하면 ConcurrentModificationException이 발생하거나 예기치 못한 오류가 발생하게 됩니다.

Java 17 기준으로 재정의 되어있음

 

 

⚠️ 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다.

자바 8은 컬렉션 인터페이스에 꽤 많은 디폴트 메서드를 추가했고, 그 결과 많은 기존 코드가 영향을 받았습니다. 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니라면 피해야합니다.

추가하려는 디폴트 메서드가 기존 구현체와 충돌하지 않는지 심사숙고 해야합니다. 하지만 새로운 인터페이스를 만들 때 더 쉽게 구현할 수 있도록 해주는 유용한 수단입니다.

디폴트 메서드는 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아님을 명시해야 합니다.(이렇게 변경하면 기존 클라이언트를 망가뜨림)

핵심은 디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 합니다.

디폴트 메서드로 기존 인터페이스에 새로운 메서드를 추가하면 커다란 위험도 딸려옵니다. 새로운 인터페이스라면 반드시 테스트를 거치고, 다른 방식으로 최소 3가지는 구현해봐야 합니다. 또한 클라이언트도 여러개 만들어야 합니다. 이런 작업을 통해 인터페이스를 릴리스하기 전에, 바로잡을 기회가 남았을 때 결함을 찾을 수 있습니다.

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유