[도서] 자바 8 인 액션 (9장 디폴트 메서드)

dbswlekq·2023년 3월 21일

기존에 존재하던 인터페이스에 새로운 메서드를 추가하면 인터페이스를 구현하는 모든 클래스에 메서드 구현을 해줘야한다. 이런 문제를 해결하기 위해 자바 8부터 정적 메서드 사용과 디폴트 메서드 를 사용할 수 있게 되었다. 메서드 구현을 포함하는 인터페이스를 정의할 수 있게 된 것이다. 이 결과 기존 코드에서 추가된 메서드 구현을 강요하지 않고 메서드를 추가할 수 있게 되었다.

List 인터페이스에 sort메서드는 디폴트 메서드이다.

default void sort(Comparator<? super E> c) {
    Collections.sort(this, c);
}

이런식으로 default 키워드가 붙어있는 메서드가 디폴트 메서드이다. 디폴트 메서드를 이용하면 자바 API의 호환성을 유지하면서 라이브러리를 바꿀 수 있다.

디폴트 메서드

  • 자바 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를 사용할지 명시적으로 선택해서 에러를 해결해야 한다.

0개의 댓글