본문 바로가기

IT Book Summary/ModernJavaInAction

Chapter 13 디폴트 메서드

인터페이스 메서드 추가 해야한다면 그것을 구현하는 모든 클래스도 수정해야 하는데

자바8 에서 새로 추가된 기능이 이 문제를 간단히 해결가능하게 한다.

인터페이스 내부 정적 메서드를 사용하거나 기본구현을 제공할 수 있도록 디폴트 메서드를 사용하는 것이다.

이렇게 하면 기존의 코드 구현을 바꾸도록 강요하지 않고 인터페이스를 바꿀 수 있다.

 

그렇다면 인터페이스와 추상클래스의 차이가 무엇인가?

디폴트 메서드는 주로 라이브러리 설계자 들이 사용한다.

이것은 다중상속 동작이라는 유연성을 제공하면서 프로그램 구성에 도움을 준다.

 

정적 메서드와 인터페이스

보통 자바는 인터페이스 그리고 인터페이스의 인스턴스를 활용할 수 있는 다양한 정적메서드를 정의하는 유틸리티 클래스를 활용한다.
 ex) Collections는 Collection 객체를 활용할 수 있는 유틸리티 클래스다.
자바 8에서는 인터페이스에 직접 정적 메서드를 선언할 수 있으므로 유틸리티 클래스를 없애고 직접 정적메서드를 구현할 수 있다.

 


13.1 변화하는 API 

 

ex) 자바 그리기 라이브러리. Resizable 인터페이스와 그것을 구현하는 Ellipse 클래스

 

1 - API 버전 1

 

// Resizable 인터페이스 초기버전
public interface Resizable extends Drawable {
    int getWidth();
    int getHeight();
    void setWidth(int width);
    void setHeight(int height);
    void setAbsoluteSize(int width, int height);
}

// 사용자 구현 Ellipse 클래스
public class Ellipse implements Resizable {
    ...
}

public class Game {
    public static void main (String...args) {
        List<Resizable> resizableShapes = Arrays.asList(new Square(), new Recrangle(), new Ellipse());
        Utils.paint(resizableShapes);
    }
}

public class Utils {
    public static vlid paint(List<Resizable> l) {
        l.forEach(r -> {
            r.setAbsoluteSize(42, 42);
            r.draw();
        });
    }
}

 

 

2 - API 버전 2

 

인터페이스에 메서드 추가

기존 Resizable  인터페이스에 setRelativeSize(int wFactor, int hFactor) 메서드가 추가됨.

 

인터페이스에 새로운 메서드를 추가하면 바이너리 호환성을 유지된다.

하지만 이때 Ellipse 객체가 인수로 전달되면 setRelativeSize 메서드를 정의하지 않았으므로 런타임 에러가 발생할 것이다.

공개된 API를 고치면 기존 버전과의 호환성 문제가 발생한다.

 

이런 문제를 해결하기 위해 디폴트 메서드를 이용할 수 있다.

인터페이스에서 자동으로 기본구현을 제공하므로 기존 코드를 고치지 않아도 된다.

 

바이너리 호환성
뭔가를 바꾼 이후에도 에러없이 기존 바이너리가 실행될 수 있는 상황
ex) 인터페이스에 메서드를 추가했을때 추가된 메서드를 호출하지 않는 한 문제가 일어나지 않는 것.

소스 호환성
코드를 고쳐도 기존 프로그램을 성공적으로 컴파일 할 수 있는것.

동장 호환성 
코드를 고쳐도 같은 입력값이 주어지면 프로그램이 같은 동작을 실행한다는 것

 


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

 

디폴트 메서드는 default 라는 키워드로 시작하고 메서드 바디를 포함한다.

 

// 추상메서드와 디폴트 메서드를 가지는 인터페이스
public interface Sized {
    int size();
    default boolean isEmpty() {
        return size() == 0;
    }
}

 

자바8 에서는 Collection 인터페이스의 stream 메서드처럼 많은 디폴트 메서드를 사용한다.

Predicate, Function, Comparator 등 함수형 인터페이스도 다양한 디폴트 메서드를 포함한다.

(함수형 인터페이스는 오직 하나의 추상메서드를 가지지만 디폴트 메서드는 추상메서드에 해당하지 않음)

 

추상클래스와 인터페이스의 차이

  1. 클래스는 하나의 추상클래스만 상속받을수 있지만 인터페이스를 여러개 구현가능
  2. 추상클래스는 필드를 공통 상태를 가질수 있음. 인터페이스는 가질 수 없음.

 


13.3 디폴트 메서드 활용 패턴

디폴트 메서드를 활용하는 두가지 방법

 

1 - 선택형 메서드

ex) 디폴트 메서드를 이용해 remove메서드에 기본구현 제공.

따라서 인터페이스를 구현하는 클래스가 빈구현을 제공할 필요가 없다.

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

 

2 - 동작 다중 상속

 

단일 상속과 다중상속

디폴트 메서드를 이용하면 클래스는 다중상속을 이용해 기존코드를 재사용 할 수 있다.

 

ex) ArrayList 클래스

 

// 한개의 클래스 상속, 네개의 인터페이스 구현
public class ArrayList<E> extends AbstractList<E> 
        implements List<E>, RandomAccess, Cloneable, Serializable {
}

다중상속 형식

자바8 에서는 인터페이스가 구현을 포함할 수 있으므로 클래스는 여러 인터페이스에서 동작을 상속 받을수 있음.

 

기능이 중복되지 않는 최소의 인터페이스

게임에서 다양한 특성을 같는 여러 모양을 정의한다고 가정하자.

회전하는것, 움직이는것, 크기를 조절하는것 으로 기능을 나눠 인터페이스를 만든다.

 

public interface Rotatable {
    void setRotationAngle(int angleInDegrees);
    int getRotationAngle();
    default void rorateBy(int angleInDegrees) {
        setRotationAngle((getRotationAngel() + angleInDegrees)%360);
    }
}
// 구현해야할 다른 메서드에 따라 뼈대 알고리즘이 결정되는 템플릿디자인패턴과 비슷

public 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 interface Resizable {
    int getWidth();
    int getHeight();
    void setWidth(int width);
    void setHeight(int height);
    void setAbsoluteSize(int width, int height);
    default void setRelativeSize(int wFactor, int hFactor) {
        setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
    }
}

 

인터페이스 조합

이들 인터페이스를 조합해서 게임에 필요한 다양한 클래스를 구현한다.

 

// 움직이고 회전하고 크기 조절할 수 있는 Monster 클래스
public class Monster implements Rotatable, Moveable, Resizable {
    // 모든 추상메서드 구현을 해야하지만 디폴트메서드는 구현 x
    ...
}

// 움직이고 회전할 수 있는 Sun 클래스
puvlic class Sun implements Moveable, Rotatable {
    ...
}

 

Monster m = new Monster();

m.rotateBy(180);

m.moveVertically(10);

 

한개의 메서드를 재사용하려고 100개의 메서드와 필드가 정의된 클래스를 상속받는것은 좋은생각이 아니다.
이때 멤버변수를 이용해서 클래스에 필요한 메서드를 직접 호출하는 메서드를 작성하는것이 좋다.
예를들어 String 클래스는 final로 선언되어있는데, 이렇게 String의 핵심 기능을 바꾸지 못하게 제한할 수 있다.

 


13.4 해석 규칙

 

같은 시그니처를 갖는 디폴트 메서드를 상속받는다면?

실제로는 자주 일어나지 않지만 따라야 하는 규칙이 있다.

 

1 - 알아야 할 세 가지 해결 규칙

  1. 클래스나 슈퍼클래스가 정의한 메서드가 항상 이긴다. 
  2. 서브 인터페이스가 이긴다. 디폴트 메서드를 정의하는 가장 하위의 인터페이스.
  3. 여전히 디폴트메서드의 우선순위가 결정되지 않았다면
    상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.

 

2 - 디폴트 메서드를 제공하는 서브인터페이스가 이긴다.

디폴트 메서드를 제공하는 가장 하위의 서브인터페이스가 이긴다.

 

3 - 충돌 그리고 명시적인 문제해결

클래스와 메서드 관계로 디폴트 메서드를 선택할 수 없는 상황에서는

개발자가 직접 클래스에 사용하려는 매서드를 명시적으로 선택해야 한다.

 

public class C implements B, A {
    void hello() {
        B.super.hello(); // 명시적 선택
    }
}

 

4 - 다이아몬드 문제

 

public interface A {
    default void hello() {
        System.out.println("Hello from A");
    }
}
public interface B extends A {}
public interface C extends A {}
public class D implements B, C {
    public static void main(String... args) {
        new D().hello();
    }
}

 

실제로 선택할 수 있는 메서드 선언은 하나 뿐이므로  답은 'Hello from A'

 

만약 C가 추상메서드 hello()를 선언한다면?

-> C는 A를 상속받으므로 C의 추상메서드 hello가 A의 디폴트메서드 hello보다 우선권을 가지므로

컴파일 에러가 발생. 클래스 D 가 어떤것을 사용할지 명시적으로 선택해야 한다.


  • 자바 8 인터페이스는 구현코드를 포함하는 디폴트메서드, 정적메서드를 정의할 수 있음.
  • 디폴트메서드는 default 키워드로 시작하며 바디를 가짐.
  • 공개된 인터페이스에 추상메서드를 추가하면 호환성이 깨짐
  • 설계자가 API를 디폴트 메서드로 바꾸어 기존버전과 호환성을 유지
  • 선택형 메서드와 동작다중상속에도 디폴트 메서드를 사용
  • 같은 시그니처를 갖는 여러 디폴트 메서드를 상속해 생기는 충돌을 해결하는 규칙이 존재.
  • 클래스 > 슈퍼클래스 > 서브 인터페이스 
  • 상속관계로 충돌문제를 해결할 수 없을때 디폴트메서드를 사용하는 클래스에서 메서드를 오버라이드해서
    어떤 디폴트 메서드를 호출할지 명시적으로 해결해야 함.