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

콜트·2021년 7월 30일
0
post-thumbnail

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

디폴트 메서드

  • 자바 8 이전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.
    • 인터페이스에 메서드를 추가하면 보통은 컴파일 오류가 나는데, 추가된 메서드가 기존 구현체에 존재할 가능성이 아주 낮기 때문이다.
  • 자바 8 이후부터 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드가 추가되었다.
  • 디폴트 메서드를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.
    • 디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의 없이 무작정 '삽입'될 뿐이므로 주의해야 한다.
  • 자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었다. 이는 주로 람다를 활용하기 위해서다.
  • 자바 라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분의 상황에서 잘 작동하지만, 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하는 것은 어렵다.

자바 8의 Collection 인터페이스에 추가된 디폴트 메서드

public interface Collection<E> extends Iterable<E> {
    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 메서드로, 주어진 boolean 함수(predicate; 프레디키트)가 true를 반환하는 모든 원소를 제거한다.
  • 범용적으로 잘 구현되었지만, 현존하는 모든 Collection 구현체와 잘 어우러지는 것은 아니므로 주의해야 한다.
  • 자바 플랫폼 라이브러리에서는 이런 문제를 예방하기 위해 다음과 같은 조치를 취했다.
    • 구현한 인터페이스의 디폴트 메서드를 재정의하고, 다른 메서드에서는 디폴트 메서드를 호출하기 전에 필요한 작업을 수행하도록 했다.

주의사항

  • 디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다.
  • 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다.
  • 반면, 새로운 인터페이스를 만드는 경우라면 표준적인 메서드 구현을 제공하는 데 아주 유용한 수단이며, 그 인터페이스를 더 쉽게 구현해 활용할 수 있게끔 해준다.
  • 디폴트 메서드는 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아님을 명심해야 한다.

추가 정리

  • 메서드 시그니처는 메서드의 이름과 해당 메서드에 필요한 매개변수들의 조합을 말한다.
  • 반환 타입은 메서드 시그니처에 포함되지 않는다.
public class MethodSignature {

    // 메서드 시그니처 -> sum(int, int)
    int sum(int x, int y) {
        return x + y;
    }

//     메서드 시그니처 -> sum(int, int)
//    int sum(int x, int y) { // 컴파일 에러 발생 - 메서드 시그니처가 동일한 메서드가 이미 존재
//        return x + y;
//    }

    // 메서드 시그니처 -> sum2(int, int)
    int sum2(int x, int y) {
        return x + y;
    }

    // 메서드 시그니처 -> sum(int, int, int)
    int sum(int x, int y, int z) { // sum(int, int) 를 오버로딩
        return x + y + z;
    }
}
  • 위 코드에서 첫 번째 sum 메서드의 시그니처는 sum(int, int)이다.
  • 마지막 sum 메서드의 시그니처는 sum(int, int, int)이다.
  • 자바 컴파일러는 이러한 메서드 시그니처를 통해 메서드를 식별한다. 이는 오버로딩을 할 때 중요하다.

핵심 정리

  • 디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 한다.
  • 디폴트 메서드로 기존 인터페이스에 새로운 메서드를 추가하려면 기존 구현체들과 충돌하지는 않을지 반드시 고려해야 한다.
  • 새로운 인터페이스라면 릴리스 전에 반드시 테스트를 거쳐야 한다.
    • 수 많은 개발자가 각기 다른 방식으로 인터페이스를 구현할 것이니, 최소한 세 가지의 다른 방식으로는 구현해봐야 한다.
    • 또한 각 인터페이스의 인스턴스를 다양한 작업에 활용하는 클라이언트도 여러 개 만들어봐야 한다.
  • 인터페이스를 릴리스한 후라도 결함을 수정하는 게 가능한 경우도 있겠지만, 절대 그 가능성에 기대서는 안 된다.

참고자료

profile
개발 블로그이지만 꼭 개발 이야기만 쓰라는 법은 없으니, 그냥 쓰고 싶은 내용이면 뭐든 쓰려고 합니다. 코드는 깃허브에다 작성할 수도 있으니까요.

0개의 댓글