디폴트 메서드

기존의 자바에선 인터페이스(interface)를 구현하는 클래스는 인터페이스에서 정의하는 "모든" 메서드를 구현(implements)하거나 상위 클래스에서 구현을 상속받아야 합니다. 인터페이스를 사용함에 있어서 이런 특징은 별다른 문제가 없지만 라이브러리나 프레임워크 설계자 입장에서 인터페이스에 포함된 메서드를 추가하거나 변경하고 싶을 때 문제가 발생합니다. 인터페이스에 새로운 메서드를 추가한다는 것은 해당 인터페이스에 의존하고 있는 모든 클래스를 수정하는 문제(Expression problem)가 발생하기 때문입니다. 기존에 이런 문제점을 해결하기 위해서 Visitor pattern등을 사용합니다.

자바 8에선 이런 문제를 해결하기 위해서 인터페이스 내부에 기본 구현을 포함하는 인터페이스를 정의하는 두 가지 방법을 제공합니다. 첫 번째는 인터페이스 내부에 정적 메서드(static method)를 사용하는 것이고, 두 번째는 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드(default method)라는 기능을 사용하는 방법입니다. 즉, 자바 8에서는 메서드 구현을 포함하는 인터페이스를 정의할 수 있습니다. 결과적으로 기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 됩니다. 따라서 기존의 코드 구현을 바꾸도록 강요하지 않으면서도 인터페이스를 바꿀 수 있습니다.


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

// 디폴트 메서드 사용
List<Integer> numbers = Arrays.asList(3, 5, 1, 2, 6);
numbers.sort(Comparator.naturalOrder());
System.out.println(numbers);

기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴드 메서드를 상속합니다. 인터페이스 내부의 default 키워드는 해당 메서드가 디폴트 메서드임을 가리킵니다. 디폴트 메서드를 활용하면 API의 호환성을 유지하면서 라이브러리를 수정할 수 있습니다. 기본 구현을 그대로 상속하므로 인터페이스에 자유롭게 새로운 메서드를 추가 할 수 있습니다.

예제

아래와 같은 DrawableResizable 인터페이스가 존재합니다. 아래 예제에 크기를 조절 할 수 있는 setRelativeSize를 추가하고자 합니다.


// API v1
public interface Drawable {
  public void draw();
}

public interface Resizable extends Drawable {
  public int getWidth();
  public void setWidth(int width);
  public int getHeight();
  public void setHeight(int height);
  public void setAbsoluteSize(int width, int height);
  
}

public class { Square, Ellipse } implements Resizable {
  @Override
  public int getWidth() { return 0; }

  @Override
  public void setWidth(int width) { }

  @Override
  public int getHeight() { return 0; }

  @Override
  public void setHeight(int height) { }

  @Override
  public void setAbsoluteSize(int width, int height) { }

  @Override
  public void draw() { }
}

public void setRelativeSize(int wFactor, int hFactor);를 인터페이스에 추가하게 되면 { Square, Ellipse } 클래스의 구현도 수정해야 합니다. 그렇지만 공개된 API를 수정하게 되면 기존 버전과 호환성 문제가 발생됩니다. 이런 이유 때문에 공식 자바 컬렉션 API 같은 기존의 API는 고치기 어렵습니다. 이런 몇가지 문제는 디폴트 메서드를 사용하면 손쉽게 수정 할 수 있습니다.

자바 8에서 도입된 디폴트 메서드를 사용하면 아래 코드처럼 작성됩니다.


// API v2
public interface Resizable extends Drawable {
  public int getWidth();
  public void setWidth(int width);
  public int getHeight();
  public void setHeight(int height);
  public void setAbsoluteSize(int width, int height);
  // 디폴트 메서드
  default void setRelativeSize(int wFactor, int hFactor) {
      setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
  }  
}

자바 8에선 인터페이스는 자신을 구현하는 클래스에서 메서드를 구현하지 않을 수 있는 새로운 메서드 시그니처를 제공합니다. 인터페이스를 구현하는 클래스에서 구현하지 않은 메서드는 인터페이스에서 기본으로 제공합니다. default라는 키워드로 시작하며 다른 클래스에 선언된 메서드처럼 메서드 바디를 포함합니다. 인터페이스에 디폴트 메서드를 추가하면 소스 호환성이 유지됩니다. 함수형 인터페이스는 오직 하나의 추상 메서드를 포함하며 디폴트 메서드는 추상 메서드에 해당하지 않습니다.

디폴트 메서드 활용 패턴

디폴트 메서드 이용하는 대표적인 두 가지 방식은 1) 선택형 메서드
, 2) 동작 다중 상속 입니다. 선택형 메서드는 디폴트 메서드를 이용하면 기본 구현을 제공할 수 있으므로 인터페이스를 구현하는 클래스에서 빈 구현을 제공할 필요가 없습니다. 동작 다중 상속은 자바에서 클래스는 한 개의 다른 클래스만 상속할 수 있지만 인터페이스는 여러 개 구현할 수 있습니다. 대표적인 예로 ArrayList는 한 개의 클래스를 상속 받고, 여섯 개의 인터페이스를 구현하며, 결과적으로 AbstractList, List, RandomAccess, Cloneable, Serializable, Iterable, Collection 하위 형식이 됩니다. 따라서 디폴트 메서드를 사용하지 않아도 다중 상속이 가능합니다. 자바 8에서는 인터페이스가 구현을 포함할 수 있으므로 클래스는 여러 인터페이스에서 동작(구현 코드)를 상속 받을 수 있습니다. 중복되지 않는 최소한의 인터페이스를 유지한다면 쉽게 재사용하고 조합 가능한 인터페이스를 구현 가능합니다.

디폴트 메서드를 구현 클래스에서 메서드를 정의하지 않는다면 디폴트 메서드 덕분에 인터페이스를 직접 고칠 수 있고 구현하는 모든 클래스도 자동으로 변경한 코드를 상속 받습니다. 상속으로 코드 재사용 문제를 모드 해결할 수 있는 것은 아닙니다. 한 개의 메서드를 재사용하려고 100개의 메서드와 필드가 정의되어 있는 클래스를 상속 받는 것은 좋지 않은 생각입니다. 이 경우 델리게이션(delegation), 즉 멤버 변수를 이용해서 클래스에서 필요한 메서드를 직접 호출하는 메서드를 작성하는 것이 좋습니다.

final로 선언된 클래스는 다른 클래스가 해당 클래스를 상속받지 못하게 함으로써 원래 동작이 바뀌지 않길 원하기 때문입니다. 디폴트 메서드에도 이 규칙을 적용 할 수 있습니다. 필요한 기능만 포함하도록 인터페이스를 최소한으로 유지한다면 필요한 기능만 선택할 수 있으므로 쉽게 기능 조립 가능합니다.

해석 규칙

자바 8에는 디폴트 메서드가 추가되었으므로 같은 시그니처를 갖는 디폴트 메서드를 상속받는 상황 발생 가능합니다. 따라서 이런 상황을 올바로 이해하기 위해서 알아야 할 세 가지 해결 규칙이 있습니다. 1) 클래스가 항상 우선합니다. 클래스나 슈퍼클래스에서 정의한 메서드가 디플트 메서드보다 우선권을 갖습니다. 2) 1번 규칙 이외의 상황에서 서브인터페이스가 우선권을 가집니다. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브 인터페이스가 우선권을 가집니다. 즉, B가 A를 상속 받는다면 B가 A보다 우선권이 높습니다. 3) 여전히 디폴트 메서드의 우선순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 합니다.


public class Ambiguous {

  public static void main(String... args) {
      new C().hello();
  }

  static interface A {
      public default void hello() {
          System.out.println("Hello from A");
      }
  }

  static interface B {
      public default void hello() {
          System.out.println("Hello from B");
      }
  }

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

아래 예제에서 C 클래스는 어떤 메서드의 정의를 사용할까요? 2번 규칙에서는 서브 인터페이스가 이긴다고 설명했습니다. 즉, B가 A를 상속받았으므로 컴파일러는 B의 hello를 선택합니다. 따라서 프로그램은 Hello form B를 출력합니다.

클래스 E는 어떤 메서드가 호출될까요? 1번 규칙은 클래스의 메서드 구현이 우선이라고 하였습니다. 클래스 Dhello를 오버라이드 하지 않았고 단순히 인터페이스 A를 구현했으니 DA의 디폴트 메서드 구현을 상속받습니다. 2번 규칙에서 클래스나 슈퍼클래스에 메서드 정의가 없을 때는 디폴트 메서드를 정의하는 서브인터페이스가 선택됩니다. 따라서 컴파일러는 AB 중 하나를 선택하게 됩니다. BA를 상속받는 관계이므로 Hello form B가 출력됩니다.


public class MostSpecific {

    public static void main(String... args) {
        new C().hello();
        new E().hello();
        new G().hello();
    }

    static interface A {
        public default void hello() {
            System.out.println("Hello from A");
        }
    }

    static interface B extends A {
        public default void hello() {
            System.out.println("Hello from B");
        }
    }

    static class C implements B, A {
    }

    static class D implements A {
    }

    static class E extends D implements B, A {
    }

    static class F implements B, A {
        public void hello() {
            System.out.println("Hello from F");
        }
    }

    static class G extends F implements B, A {
    }

}

클래스와 메서드 관계로 디폴트 메서드를 선택할 수 없는 상황에서는 X.super.m(...) 형태의 새로운 문법을 사용해서 명시적으로 해결해야 합니다.

참고