본문 바로가기
스터디

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

by eunoo 2023. 8. 25.
  • 디폴트 메서드란?
  • 진화하는 API가 호환성을 유지하는 방법
  • 디폴트 메서드의 활용 패턴
  • 해결 규칙

정적 메서드와 인터페이스

  • 자바는 인터페이스, 인터페이스의 인스턴스를 활용할 수 있는 정적 메서드를 제공하는 유틸리티 클래스를 제공한다. ex) Collections는 Collection객체를 활용할 수 있는 유틸리티 클래스이다.
  • 자바 8에서 인터페이스에 직접 정적 메서드를 작성할 수 있으므로 유틸리티 클래스를 없애도 되지만, 과거 버전과의 호환성때문에 유틸리티 클래스를 남겨두었다.

🍎 변화하는 API

  • 인터페이스에 새로운 메서드를 추가하면 바이너리 호환성은 유지된다.
    • 바이너리 호환성? 인터페이스에 새로 추가된 메서드를 호출하지만 않으면 새로운 메서드의 구현없이도 기존 클래스는 잘 동작한다.
    • 하지만 하나의 구현체에서 새로운 메서드를 구현하여 사용한다면, 다른 구현체에도 구현해야 한다.

호환성

  • 바이너리 호환성
  • 소스 호환성 : 코드를 고쳐도 기존 프로그램을 성공적으로 재컴파일할 수 있음. 인터페이스에 메서드를 추가하면 추가한 메서드를 구현하도록 클래스를 수정해야 하기 때문에 소스 호환성이 아니다.
  • 동작 호환성 : 코드를 바꿔도 같은 입력값이 주어지면 프로그램은 같은 동작을 실행함.

🍎 디폴트 메서드란

추상 클래스와 자바8의 인터페이스

  • 클래스는 다중상속 X, 인터페이스는 다중상속 O
  • 추상 클래스는 인스턴스 변수 O, 인터페이스는 인스턴스 변수를 가질 수 없다.

🍎 디폴트 메서드 활용하기

1. 선택형 메서드

  • 디폴트 메서드가 없던 시절 인터페이스의 구현체에서 필요없는 메서드는 빈 구현을 제공했다.
  • 디폴트 메서드의 등장으로 빈 구현 필요없이 선택적으로 메서드를 사용하면 된다.
interface Iterator<T> {
    boolean hasNext();
    T next();
    // 구현체에서 빈 구현을 제공하지 않고, 디폴트 메서드를 정의한다.
    default void remove() {
        throw new UnsupportedOperationException();
    }
}
 

2.동작 다중 상속

// 인터페이스의 다중 상속
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
 
  • 다중 상속은 여러 클래스에서 기능을 상속받는다. 디폴트 메서드를 사용하지 않아도 다중 상속을 활용할 순 있다. 하지만 디폴트 메서드를 다중 상속받는다면 어떤 이점이 있을까?
    • 중복되지 않는 최소한의 인터페이스를 유지한다면 동작을 쉽게 재사용하고 조합할 수 있다.

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

  • 어떤 모양은 회전X, 크기 조절 O
  • 어떤 모양은 회전O, 움직임O, 크기 조절X
public interface Rotatable {
    void setRotationAngle(int angleInDegrees);
    int getRotationAngle();

    default void rotateBy(int angleInDegrees) {
        setRotationAngle((getRotationAngle() + 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);
    }
}
 

인터페이스 조합 (다중 상속)

// 회전 O, 움직임 O, 크기조절 O
public class Monster implements Rotatable, Moveable, Resizable {
    // 모든 추상 메서드의 구현
}

...
Monster m = new Monster();
m.rotateBy(180); //180도 회전
m.moveVertically(10); //10만큼 이동
m.setAbsoluteSize(10, 10) // 사이즈 증가
 
// 회전 O, 움직임 O, 크기조절 X
public class Sun implements Rotatable, Moveable {
    // 모든 추상 메서드의 구현
}

...
Sun m = new Sun();
m.rotateBy(180);
m.moveVertically(10);
 

옳지 못한 상속

  • 한 개의 메서드를 위해 100개의 메서드가 정의되어 있는 클래스를 상속하는건 옳지 않다. 이럴 땐 델리게이션이 좋다? 멤버 변수에 클래스 선언해서 메서드 호출하기

🍎 디폴트 메서드의 충돌

해결 규칙 세 가지

  1. 클래스가 항상 이긴다. 인터페이스의 디폴트 메서드보다 클래스의 메서드가 이긴다.
  2. 클래스에 메서드 정의가 없으면 서브 인터페이스의 디폴트 메서드가 이긴다.
  3. 디폴트 메서드의 우선순위기 결정되지 않으면 명시적으로 오버라이딩해줘야 한다.
public interface A {
    default void hello(){ System.println.out("hello A~"); }
}

public class D implements A {}

public interface B extends A {
    @Override
    default void hello() { System.println.out("hello B~"); }
}

public class C extends D implements B, A {
    public static void main(String... args) {
        new C().hello(); // hello B~ 출력
    }
}
// 1번 규칙에 의해 클래스가 먼저이니 D의 hello가 우선이다.
// 하지만 2번 규칙에 의해 D에 메서드가 없으니 인터페이스가 이긴다.
// 컴파일러는 A와 B 중 선택하는데 A를 상속한 B가 이긴다.
 

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

  • 인터페이스 A와 B에 동일한 디폴트 메서드가 있다. A,B를 구현한 C는 명시적으로 디폴트 메서드를 오버라이딩해야 한다.
  • 만약 디폴트 메서드가 리턴 타입만 다르고 메서드명이 같으면 역시 충돌난다.

다이아몬드 문제

public interface A {
    default void hello() {
        System.println.out("Hello 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(); // B,C 모두 A의 디폴트 메서드이므로 A출력
    }
}

댓글