티스토리 뷰

728x90
[이펙티브 자바 3/E] 아이템 21 : 인터페이스는 구현하는 쪽을 생각해 설계하라

 

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

Java7까지만 해도 "현재 인터페이스에 새로운 메서드가 추가될 일은 영원히 없다"고 가정하고 작성됐다고 하니 말 다했다. 

 

만약 요구사항이 추가되면서 인터페이스에 메서드를 추가해야하는 상황이 온다면, 해당 인터페이스의 구현체 클래스를 모두 변경해야줘야한다. 이게 작은 프로그램이라면 문제가 없겠지만 해당 인터페이스를 상속 받은 클래스가 무수히 많다면.... 변경 사항이 걷잡을 수 없이 많아질 것이다. 

 

이러한 문제를 해결하기 위해 Java8 부터는 기존 인터페이스에 메서드를 추가할 수 있는 "디폴트 메서드"가 추가되었다. 

 

핵심

디폴트 메서드는 완전한 것이 아니다. 기존 클래스에 여러 문제를 야기할 수 있다. 

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

 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다. 

 인터페이스를 설계할 때는 추후 변경사항이 발생하지 않도록 릴리즈 전 많은 테스트 과정을 거쳐야한다.  

    - 수 많은 개발자가 각기 다른 방식으로 인터페이스를 구현할 것이니, 최소한 세 가지의 다른 방식으로 구현해봐야 한다. 

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

 

디폴트 메서드(Defautl Method)

디폴트 메서드는 인터페이스에 있는 구현 메서드를 의미한다. 

 

✅ 메서드 앞에 deault 에약어가 붙는다. 

 구현부 {}가 있어야 한다. 

 

다음은 List 인터페이스의 디폴트 메서드와 추상 메서드이다. 

// 디폴트 메서드
default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

// 추상 메서드
void clear();

 

디폴트 메서드.. 마냥 좋은 것이 아니다

기존 추상 메서드와 다르게 인터페이스 안에서 디폴트 구현을 지정할 수 있기 때문에

디폴트 메서드를 재정의하지 않은 모든 클래스에서는 디폴트 구현을 사용하며, 재정의해 사용할 수도 있다. 

하지만, 디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의 없이 무작정 "삽입"될 뿐이므로 주의해야한다. 

 

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

 

Java8의 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;
    }
}

위의 removeIf() 메서드도  범용적으로 잘 구현되었지만, 현존하는 모든 Collection 구현체와 잘 어우러지는 것은 아니다.

org.apache.commons.collections4.collection.SynchronizedCollection은 모든 메서드에서 주어진 락 객체로 동기화한 후 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스인데(한마디로 멀티 스레드 환경에서 안정성을 보장해준다), removeIf() 메서드를 재정의하고 있지 않다. removeIf() 메서드는 동기화에 관해 아무것도 모르므로 락 객체를 사용할 수 없게 된다. 멀티 스레드 환경에서 실행하다 보면 예기치 못한 결과를 만날 수 있게 되는 것이다. (최신 버전에서는 해당 이슈가 고쳐졌다함)

 

자바 플랫폼 라이브러리에서는 이런 문제를 예방하기 위해 구현한 인터페이스의 디폴트 메서드를 재정의하고, 다른 메서드에서는 디폴트 메서드를 호출하기 전에 필요한 작업을 수행하도록 하는 조치를 취했다. 

 

하지만 그렇다고 해서 모든 오류를 잡아낼 수 있는 것이 아니며,

컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으키는 등 다양함 문제를 야기할 수 있다. 🥲

 

Ref.

https://asfirstalways.tistory.com/353

https://k3068.tistory.com/73

728x90
댓글
공지사항
최근에 올라온 글