13장: 디폴트 메서드

디폴트 메서드의 필요성

  • 인터페이스에 새로운 메서드를 추가하려면, 해당 인터페이스를 구현하는 모든 클래스에서 새 메서드를 구현해야 한다.

  • 따라서 인터페이스를 구현하는 클래스가 매우 많을 경우에는 인터페이스를 변경하기 어렵다.

  • 자바 8에서는 인터페이스 내부에 정적 메서드를 사용하거나 디폴트 메서드를 제공하도록 지원한다.

  • 이에 따라 기존 인터페이스 구현체는 자동으로 새로운 메서드를 상속받는다.

  • 주로 라이브러리 설계자들이 하위 호환성을 유지하면서 라이브러리를 변경하는 용도로 사용한다.

  • Java 의 List 클래스에도 정렬을 위해 디폴트 메서드를 추가해 사용하고 있다.

public interface List<E> extends SequencedCollection<E> {
    // ...
    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);
        }
    }
    // ...
}
  • 인터페이스에 새로운 메서드를 추가해도 바이너리 호환성은 유지된다. 즉, 새로 추가된 메서드를 호출하지만 않으면 클래스에서 메서드를 구현하지 않아도 기존 클래스가 잘 동작한다.

바이너리 호환성: 코드를 변경한 후 에러 없이 기존 바이너리가 실행될 수 있다.

소스 호환성: 코드를 변경해도 기존 프로그램을 다시 컴파일할 수 있다.

동작 호환성: 코드를 변경해도 같은 입력에 대해 같은 동작을 수행한다.

  • 하지만 전체 애플리케이션을 재빌드할 때에 컴파일 에러가 발생할 것이다.

디폴트 메서드란

  • 인터페이스에서 호환성을 유지하면서 API를 바꿀 수 있도록 제공하는 메서드

  • 인터페이스의 구현 클래스에서 반드시 구현하지 않아도 되는 메서드이다.

  • 만약 구현 클래스에서 디폴트 메서드를 오버라이드하지 않으면 인터페이스에서 정의된대로 수행된다.

public interface Sized { 
  int size();
  default boolean isEmpty() {
    return size() == 0;
  }
}

추상 클래스와 차이점

  • 클래스는 하나의 추상 클래스만 상속받을 수 있지만 인터페이스는 여러 개를 구현할 수 있다.

  • 추상 클래스는 인스턴스 필드를 공통적으로 가질 수 있지만 인터페이스는 인스턴스 필드를 가질 수 없다.

활용 패턴

선택형 메서드

  • 인터페이스를 구현하는 클래스에서 필요 없는 메서드를 일단 오버라이드하고 비워두는 대신, 디폴트 메서드에 기본 구현을 제공할 수 있다.

  • 아래와 같이 remove가 필요 없는 클래스에서는 오버라이드하지 않고 기본 동작을 수행하도록 하고, remove가 필요한 클래스에서는 오버라이드하도록 한다.

interface Iterator<T> {
  boolean hasNext();
  T next();
  default void remove() {
    throw new UnsupportedOperationException();
  }
}

동작 다중 상속

  • 기능이 중복되지 않도록 최소의 인터페이스로 분리시켜 다중 상속을 받을 수 있도록 한다.

  • 예를 들어 어떤 도형에 대해 회전, 크기 조절, 이동 의 특성을 가질 수 있다고 하면, 각각의 인터페이스를 생성할 수 있다.

  • 그리고 각 인터페이스에서는 회전하기, 크기조절하기, 이동하기 작업을 수행하는 디폴트 메서드를 만들어두면 한 번의 구현으로도 여러 구현 클래스에서 사용할 수 있고, 로직을 변경해야 할 때에도 구현 클래스에 영향 없이 변경할 수 있다.

  • 아래는 회전, 이동을 인터페이스로 정의하고, 인터페이스를 구현하는 클래스에 대한 코드이다. Monster 객체를 정의해 rotateBy나 moveHorizontally, moveVertically 메서드를 호출할 수 있다.

interface Rotatable {
  void setAngle(int angle);
  int getAngle();
  default void rotateBy(int angle) {
    setAngle((getAngle() + angle) % 360);
  }
}

interface Moveable {
  int getX();
  int getY();
  void setX(int x);
  void setY(int y);

  default void moveHorizontally(int distance) {
    setX(getX() + distance);
  }
  
  default void moveVertically(int distance) {
    setY(getY() + distance);
  }
}

public class Monster implements Rotatable, Moveable {
  // ...
}

해석 규칙

  • 여러 인터페이스를 구현하다보면 같은 시그니처를 갖는 디폴트 메서드를 상속받는 상황이 발생할 수 있다.

  • 이런 상황을 잘 처리하기 위해 세 가지 해결 규칙을 따라야 한다.

3가지 규칙

  • 클래스, 부모 클래스의 메서드가 디폴트 메서드보다 우선으로 처리된다.

    • 추상 클래스와 인터페이스를 모두 구현하는 클래스이고, 같은 메서드 시그니처를 가진 추상 메서드와 디폴트 메서드가 존재한다면 클래스에서 반드시 추상 메서드를 구현해야 한다. 왜냐하면 인터페이스의 디폴트 메서드보다 부모 클래스의 메서드가 우선이기 때문이다.

  • 상속 관계를 갖는 두 인터페이스를 구현할 경우, 자식 인터페이스의 디폴트 메서드가 우선으로 처리된다.

  • 위 두 상황이 아니라면 구현 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.

  • 아래와 같이 A,B 인터페이스에 hello라는 디폴트 메서드가 존재하고 C가 두 인터페이스를 구현한다면 명시적으로 어떤 디폴트 메서드를 호출할 지 오버라이드해주어야 한다.

public class C implements A, B {
    void hello() {
        B.super.hello();
    }
}
  • 디폴트 메서드의 시그니처가 완전히 동일하지 않아도 어떤 디폴트 메서드를 호출해야 하는지 알기 어려운 경우 오버라이드해주어야 한다.

public interface A {
    default Number get() {
        return 10;
    }
}

public interface B {
    default Integer get() {
        return 20;
    }
}

public class C implements A, B {
    void get() {
        B.super.get();
    }
}

Last updated