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

    들어가기 전

    이 글은 인프런 백기선님의 이펙티브 자바 강의를 복습하며 작성한 글입니다. 

     


    핵심 정리

    • 기존 인터페이스에 디폴트 메서드 구현을 추가하는 것은 위험한 일이다.
      • 디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의 없이 무작정 '삽입' 될 뿐이다.
      • 디폴트 메서드는 기존 구현체에 런타임 오류를 일으킬 수 있다.
    • 인터페이스를 설계할 때는 세심한 주의를 기울여야 한다.
      • 서로 다른 방식으로 최소한 세 가지는 구현을 해보자.

    아래에서 자세한 설명을 살펴보고자 한다.


    기존 인터페이스에 디폴트 메서드 구현을 추가하는 것은 위험한 일임.

    인터페이스에 default 메서드를 구현하면, 이 인터페이스를 구현한 모든 클래스에 해당 default 기능을 강제적으로 삽입하게 되고 이로 인해 문제가 발생할 수 있다. 대표적인 예는 Collection에 있는 removeIf 메서드다.

    // Collection.java
    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;
    }

    위는 Collection 인터페이스의 removeIf default 메서드를 보여준다. removeIf 자체는 굉장히 편리한 기능이다. 하지만 Collection을 구현한 클래스 입장에서 removeIf는 위험한 기능이 될 수도 있다. 

     

    예를 들어 Collection 인터페이스를 구현한 SyncronizedCollection에게는 큰 위험이 될 수 있다. SyncronizedCollection은 '한번에 한 쓰레드만 해당 오퍼레이션'을 실행해야한다. 이걸 보장하기 위해 synchronized 키워드를 사용해서 동기화를 보장한다. 

    static class SynchronizedCollection<E> implements Collection<E>, Serializable {
    
        ...
        public int size() {
            synchronized (mutex) {return c.size();}
        }
        ...
    }
    
    ...
    
    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;
    }

    이 때 Collection에 구현된 removeIf을 보자. removeIf은 내부적으로 어떠한 동기화 처리도 되어 있지 않은 것을 볼 수 있다. 그렇지만 default 메서드이기 때문에 SyncronizedCollection 인스턴스에서 호출해서 사용할 수 있는 메서드가 된다. 만약 누군가가 이 사실을 모르고 SyncronizedCollection을 호출한다면 ConcurrentModificationException이 발생할 수 있다. 

    public class HaveToOverride implements Collection {
    
        // 위험한 default 메서드를 재정의해서, 사용하지 못하도록 한다. 
        @Override
        public boolean removeIf(Predicate filter) {
            throw new UnsupportedOperationException();
        }
    }

    다시 한번 정리하면, 특정 클래스 관점에서 바라본 default 메서드는 특정 클래스에 해가 될 수 있다. 만약 어쩔 수 없이 default 메서드가 추가되었다면, 의도치 않은 동작을 막기 위해 해당 메서드를 오버라이딩해야한다. 

     


    Default 메서드는 기존 구현체에 런타임 에러를 발생시킬 수 있음. 

    기존 구현체가 상속을 한 상태에서 인터페이스를 구현하면, 특정 시점에 런타임 에러를 발생 시킬 수 있다. 예를 들어 아래 SuperClass, SubClass, MarkerInterface가 존재한다. 

    public class SuperClass {
        private void hello() {
            System.out.println("hello class");
        }
    }
    
    • SuperClass는 구현체다. 이 때 hello()라는 메서드를 가지고, 이 메서드는 비공개 메서드다.
    public interface MarkerInterface {
    
        default void hello() {
            System.out.println("hello interface");
        }
    }
    • MarkerInterface는 default 메서드 hello()를 가진다. 이 메서드는 인스턴스에서 접근 가능하다.
    public class SubClass extends SuperClass implements MarkerInterface{
        public static void main(String[] args) {
            SubClass subClass = new SubClass();
            subClass.hello();
        }
    }
    

    SubClass는 SuperClass를 상속받고, MarkerInterface를 구현한다. 

    • 이 때, SuperClass의 hello()는 비공개이기 때문에 접근할 수 없어야 한다. 
    • 이 때, MarkerInterface의 default 메서드인 hello()는 공개되었기 때문에 접근할 수 있다. 

    따라서 SubClass가 hello()를 호출하면, 컴파일 시점에는 에러가 발생하지 않는다. 그렇지만 호출하는 시점에는 아래와 같은 에러가 발생한다. 

    Exception in thread "main" java.lang.IllegalAccessError: class com.example.effectivejava1.chapter21.SubClass tried to access private method com.example.effectivejava1.chapter21.SuperClass.hello()V (com.example.effectivejava1.chapter21.SubClass and com.example.effectivejava1.chapter21.SuperClass are in unnamed module of loader 'app')
    	at com.example.effectivejava1.chapter21.SubClass.main(SubClass.java:7)

    이것은 자바의 메서드 접근 규칙에 따라 발생하는 버그로 볼 수 있다.

    자바는 인터페이스보다 클래스가, 상속한 클래스가 우선순위를 가진다. 

    위의 경우 상속한 클래스의 hello() 자체는 접근할 수 없고, 보여지는 메서드는 default 메서드 hello() 일 것이다. 하지만 자바의 메서드 접근 규칙은 클래스가 메서드를 가지고 있다면 먼저 접근한다. 위에서 SubClass는 SuperClass를 상속했기 때문에 인터페이스의 메서드보다 클래스의 메서드로 먼저 접근한다. 

    그런데 접근하고보니 접근한 메서드는 private이었기 때문에 위와 같은 에러가 발생한다. 


    정리

    • 인터페이스의 default 메서드는 유용한 기능이다.
    • 인터페이스를 구현한 클래스 관점에서 본다면 default 메서드는 강제적으로 삽입되는 기능이다. 그리고 이 기능이 클래스에 에러를 발생시킬 수 있다. 
    • default 메서드가 추가되었고, 만약 구현체에서 사용하고 싶지 않다면 default 메서드를 오버라이딩 해서 사용하지 못하도록 한다. 

     

    댓글

    Designed by JB FACTORY