모던 자바 인 액션 - 13장 디폴트 메서드

Modern Java In Action 정리 - 디폴트 메서드

  • 디폴트 메서드란 무엇인가 ?
  • 진화하는 API가 호환성을 유지하는 방법
  • 디폴트 메서드의 활용패턴
  • 해결 규칙

모던 자바 인 액션 책을 보고 정리한 글입니다.

변화하는 API

  • API를 변경하는것이 왜 어려운지에 대해 예제를 통해 알아보자.

API 버전 1

  • Resizable 인터페이스 초기 버전은 다음과 같은 메서드를 지원한다.
1
2
3
4
5
6
7
public interface Resizable extends Drawable {
  int getWidth();
  int getHeight();
  void setWidth(int width);
  void setHeight(int height);
  void setAbsoluteSize(int width, int height);
}

API 버전 2

  • 요구사항의 변화로 Resizable 에 메서드가 추가되었다고 가정하자.
1
2
3
4
5
6
7
8
public interface Resizable extends Drawable {
  int getWidth();
  int getHeight();
  void setWidth(int width);
  void setHeight(int height);
  void setAbsoluteSize(int width, int height);
  void setRelativeSize(int wFactor, int hFactor);
}
  • 사용자가 겪는 문제
  • Resizable을 고치면 Resizable을 구현하는 모든 클래스는 추가된 메소드 또는 변경된 메소드를 구현해야 한다.
  • 인터페이스에 새로운 메서드를 추가하면 바이너리 호환성은 유지되지만 컴파일 단계에서 에러가 발생할 것이다
  • 공개된 API를 고치면 기존 버전과의 호환성 문제가 발생했다. 디폴트 메서드가 나온 이후로는 이러한 문제들을 해결할 수 있게 되었다.

디폴트 메서드란 무엇인가 ?

  • Java8에서는 호환성을 유지하면서 API를 바꿀수 있도록 새로운 기능인 디폴트 메서드를 제공한다.
1
2
3
4
5
6
public interface Sized { 
  int size();
  default boolean isEmpty() {
    return size() == 0;
  }
}
  • @FuntionalInterface는 오직 하나의 추상 메서드를 포함한다. 디폴트 메서드는 추상 메서드에 해당하지 않는 사실을 기억하자.

디폴트 메서드 활용 패턴

  • 디폴트 메서드를 이용하는 두가지 방식, Optional Method와 Multiple Inheritance of Behavior 를 알아보자.

선택형 메서드

  • 인터페이스를 구현하는 클래스에서 비어있는 메서드를 확인한 적이 있을것이다. 인터페이스에 정의된 메서드이지만 실제로 사용하지 않는 메서드라 빈 메서드를 정의해야 하는 상황이다.
  • 디폴트 메서드를 이용하면 이러한 메서드에 기본 구현을 제공해줄 수 있다. Iterator 인터페이스를 살펴보자.
1
2
3
4
5
6
7
interface Iterator<T> {
  boolean hasNext();
  T next();
  default void remove() {
    throw new UnsupportedOperationException();
  }
}

동작 다중 상속

  • 디폴트 메서드를 이용하면 기존에는 불가능했던 동작 다중 상속 기능도 구현할 수있다.
  • 자바에서는 한개의 클래스 상속만 가능하지만 인터페이스는 여러 개를 구현할 수 있도록 되어 있다.
1
2
3
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAeccess, Cloneable, Serialiizable {

}
  • 다중 상속 형식
  • ArrayList는 AbstractList, List, RandomAeccess, Cloneable, Serialiizable의 서브 형식이 된다.
  • Java8 에서는 인터페이스가 구현을 포함할 수 있으므로 클래스는 여러 인터페이스에서 동작을 상속받을 수 있다. 중복되지 않는 최소한의 인터페이스를 유지한다면 재사용과 조합을 쉽게 할 수 있다.
  • 기능이 중복되지 않는 최소의 인터페이스
  • 기존의 코드를 재사용하며 새로운 기능을 제공할때 디폴트 메서드를 활용할 수 있다.
1
2
3
4
5
6
7
interface Rotatable {
  void setAngle(int angle);
  int getAngle();
  default void rotateBy(int angle) {
    setAngle((getAngle() + angle) % 360);
  }
}
  • 위 인터페이스는 템플릿 메서드 패턴과 비슷해보인다. 템플릿 메서드 패턴은 추상 클래스를 상속 받아 구현하게 되어 다중상속이 불가능 하다는 차이점이 있다.
  • 인터페이스 조합
  • 여러 인터페이스를 조합하여 기존 기능들을 재사용할 수 있다.
1
2
3
public class Monster implements Rotatable, Moveable, Resizable {

}
  • Monster 클래스는 3가지 인터페이스의 디폴트 메서드를 자동으로 상속 받는다. 움직일수 있고, 회전가능하며 크기도 조절할 수 있는 클래스가 되었다.
    반면 움직일수 있고, 회전 가능하지만 크기는 조절할 수 없는 클래스를 만드려면 2가지 인터페이스만 구현하면 될것이다.
  • 인터페이스에 디폴트 구현을 포함하면 또 다른 장점이 생긴다. 특정 요구사항이 변경되었을 경우 해당하는 인터페이스의 디폴트 메서드만 변경하면 상속 받는 모든 클래스에 요구사항이 반영된다.

해석 규칙

  • Java 8에는 디폴트 메서드가 추가되어 같은 시그니처를 갖는 디폴트 메서드를 상속받는 상황이 생길 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface A {
  default void hello {
    print A
  }
}

public interface B extends A {
  default void hello {
    print B
  }
}

public class C implements B, A {
  public static void main(String... args) {
    new C().hello();
  }
}

알아야 할 세 가지 해결 규칙

  1. 클래스가 항상 이긴다. 클래스나 슈퍼 클래스에서 정의한 메서드가 디폴트 메서드보다 우선권을 갖는다.
  2. 1번 규칙 이외의 상황에서는 서브 인터페이스가 이긴다. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브 인터페이스가 이긴다. 즉 B가 A를 상속받는다면 B가 A를 이긴다.
  3. 디폴트 메서드의 우선순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.

정리

  • Java 8의 인터페이스는 구현 코드를 포함하는 디폴트 메서드, 정적 메서드를 정의할 수 있다.
  • 디폴트 메서드의 정의는 defualt 키워드로 시작하며 일반 메서드처럼 바디를 갖는다.
  • 공개된 인터페이스에 추상 메서드를 추가하면 소스 호환성이 깨진다.
  • 디폴트 메서드를 사용하면 소스 호환성을 유지할 수 있다.
  • 선택형 메서드와 동작 다중 상속에도 디폴트 메서드를 사용할 수 있다.
  • 클래스가 같은 시그니처를 갖는 여러 디폴트 메서드를 상속하며 생기는 충돌 문제를 해결하는 규칙이 있다.