728x90

이번 주제 키워드

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

인터페이스를 수정해야하는 경우

  • 바이너리 호환성은 유지되지만 소스 호환성은 유지되지 않아 해당 인터페이스를 구현한 모든 클래스를 수정해야한다.
    • 따라서 공개된 자바 API를 고치는 일은 굉장히 어려운 일이었다.
  • 자바 8에서는 이러한 문제를 해결하는 두 가지 방법을 제공한다.
    1. 인터페이스 내부에 정적 메서드를 사용하는 방법
    2. 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드 기능을 사용하는 방법
  • 즉, 자바8부터 메서드 구현을 인터페이스를 정의할 수 있어서 기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 된다.
  • 따라서 기존의 코드 구현을 바꾸도록 강요하지 않으면서도 인터페이스를 바꿀 수 있다.

호환성(참고)

  • 바이너리 호환성
    • 뭔가를 바꾼 이후에도 에러 없이 기존 바이너리가 실행될 수 있는 상황.
    • ex) 인터페이스에 메서드를 추가했을 때 추가된 메서드를 호출하지만 않으면 문제가 일어나지 않는 경우
  • 소스 호환성
    • 코드를 고쳐도 기존 프로그램을 성공적으로 재컴파일할 수 있는 상황.
    • ex) 마찬가지로 인터페이스에 메서드를 추가하는 경우는 소스 호환성이 아니다.
  • 동작 호환성
    • 코드를 바꾼 다음에도 같은 입력값이 주어지면 프로그램이 같은 동작을 실행하는 상황.

디폴트 메서드

  • 자바 8에서는 호환성을 유지하면서 API를 바꿀 수 있도록 새로운 기능인 디폴트 메서드(default method)를 제공한다.
  • 이제 인터페이스는 자신을 구현하는 클래스에서 메서드를 구현하지 않을 수 있는 새로운 메서드 시그니처를 제공한다.
  • 인터페이스를 구현하는 클래스에서 구현하지 않은 메서드는 디폴트 메서드를 통해 인터페이스 자체에서 기본으로 제공한다.
  • 디폴트 메서드는 default라는 키워드로 시작하며 다른 클래스에 선언된 메서드처럼 메서드 바디를 포함한다.

이렇게 되면 이미 존재하는 추상 클래스와 자바 8의 인터페이스가 무엇이 다르냐고 물어볼 수 있다.

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

디폴트 메서드 활용 패턴

  • 디폴트 메서드를 이용하는 두 가지 방식은 선택형 메서드(optional method)와 동작 다중 상속(multiple inheritance of behavoir)이다.

선택형 메서드

  • 이전의 인터페이스를 구현하는 클래스는 사용하지 않는 메서드에 대해 비어있는 메서드까지 필수적으로 구현해주어야 했다.
  • 하지만 디폴트 메서드를 이용하면 메서드의 기본 구현을 인터페이스로부터 제공받기 때문에 빈 구현을 제공할 필요가 없다.
  • 이를 통해 불필요한 코드의 양을 줄일 수 있다.
  • Iterator 인터페이스의 remove 메서드
default void remove() {
    throw new UnsupportedOperationException("remove");
}

동작 다중 상속

  • 인터페이스는 한 클래스에서 여러 개 구현할 수 있으므로 디폴트 메서드가 없더라도 다중 상속 을 활용 할 수 있다.
  • 거기에 추가로 구현을 포함하는 디폴트 메서드를 통해 동작다중 상속 을 활용할 수 있다.
  • ex) Rotatable, Moveable, Resizable 인터페이스의 조합을 통해 게임에 필요한 다양한 클래스들 구현 가능
    • 디폴트 메서드를 활용하는 구조가 마치 템플릿 메서드 패턴과 비슷해 보인다.
    • 디폴트 메서드 덕분에 인터페이스의 직접 수정도 가능하며 이를 구현한 클래스들을 오버라이딩하지 않은 이상 자동으로 상속받으니 문제 없다.
public interface Rotatable {
    void setRotationAngle(int angleInDegrees);
    int getRotationAngle();
    default void rotateBy(int angleInDegrees) { //디폴트 메서드
        setRotationAngle((getRotationAngle() + angleInDegrees) % 360);
    }
}

고민해볼 상황

  • 만약 같은 시그니처의 디폴트 메서드를 포함하는 여러 인터페이스를 구현하는 상황이라면?
  • 어떤 인터페이스의 디폴트 메서드를 사용하는 것일까!!!

해석 규칙

  • 드물지만 같은 시그니처를 갖는 디폴트 메서드를 상속받는 상황이 생길 수 있다.
  • 다중 상속을 허용하는 언어의 다이아몬드 상속 문제와 같다.
  • 어떤 메서드를 실행할까?
public interface A {
    default void hello() {
        System.out.println("Hello From A");
    }
}

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

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

세가지 규칙

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

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

  • 1,2 규칙으론 해결할 수 없는 경우
public interface A {
    default void hello() { ... }
}

public interface B {
    default void hello() { ... }
}

public class C implements B, A { }
  • A와 B 인터페이스 간의 상속관계도 없어 디폴트 메서드의 우선순위가 결정되지 않았다.
  • 따라서 자바 컴파일러는 어떤 메서드를 호출해야 할지 알수 없으므로 에러를 발생시킨다.
  • 충돌해결을 위해서는 아래처럼 개발자가 직접 클래스 C에서 사용하려는 메서드를 명시적으로 선택해야 한다.
public class C implements B, A {
    void hello() {
        B.super.hello();
    }
}

다이아몬드 문제

public interface A {
    default void hello() { ... }
}

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();
    }
}
  • 다이어그램의 모양이 다이아몬드를 닮아 다이아몬드 문제라 부른다.
  • D가 구현하는 B와 C 중 선택할 수 있는 메서드는 오직 A의 디폴트 메서드 뿐이다. D는 A의 hello를 호출한다.
  • 만약 B에 같은 디폴트 메서드 hello가 있었다면 가장 하위의 인터페이스인 B의 hello가 호출될 것이다.
  • B와 C가 모두 디폴트 메서드를 정의했다면 디폴트 메서드 우선순위로 인해 에러가 발생하고 명시적인 호출이 필요하게된다.

만약 C에서 디폴트 메서드가 아닌 추상메서드 hello를 추가하면 어떻게 될까?

public interface C extends A {
    void hello();
}
  • C는 A를 상속받으므로 C의 추상 메서드 hello가 A의 디폴트 메서드 hello보다 우선권을 갖는다.
  • 따라서 B와 C중 선택하지 못하며 컴파일에러가 발생하며 어떤 hello를 사용할지 명시적으로 선택해서 에러를 해결해야 한다.
728x90

+ Recent posts