Skip to content

Latest commit

 

History

History
74 lines (49 loc) · 4.81 KB

File metadata and controls

74 lines (49 loc) · 4.81 KB

모던자바인액션 Chaptrt 13_디폴트 메서드

디폴트 메서드는 왜 인터페이스에 추가되었을까?

인터페이스를 구현하는 클래스는 인터페이스에서 정의하는 모든 메서드를 구현해야 한다. 따라서 인터페이스를 변경해야 하거나, 인터페이스에 메서드가 추가되면 해당 인터페이스를 구현했던 모든 클래스는 수정이 불가피하다. 자바 8에서는 이 문제를 해결하기 위해 기본 구현을 포함할 수 있도록 인터페이스 규칙을 추가했다. 기본 구현을 포함하는 인터페이스 메서드는 두 가지 방법으로 가능하다. 첫 번째는 인터페이스 내부의 정적 메서드를 사용하는 방법이다. 두 번째는 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드 기능을 사용하는 것이다.

디폴트 메서드란?

인터페이스를 구현하고 있는 클래스가 호환성을 유지하면서 API를 바꿀 수 있도록 해주는 기능이다. 이름에서 유추할 수 있는 것처럼 인터페이스에 기본 메서드로 구현할 수 있다. 일반적인 인터페이스의 메서드는 내부 로직을 포함하지 않는다. 그러나 디폴드 메서드는 인터페이스 안에 로직을 포함한 메서드를 정의할 수 있다. 인터페이스를 구현하는 모든 클래스는 디폴트 메서드의 구현도 상속을 받기 때문에 인터페이스에 정의된 형태 그대로 메서드를 사용할 수 있으며, 필요에 따라 인터페이스를 상속받은 클래스에서 Overriding하는 것도 가능하다.

추상 클래스와 인터페이는 어떤 차이가 있을까?

클래스의 상속 규칙은 이중 상속을 지원하지 않는다. 따라서 하나의 추상 클래스만 상속 받을 수 있다. 반면 인터페이스는 여러 개의 인터페이스를 구현할 수 있다. 또한 추상 클래스는 인스턴스 변수를 가질 수 있지만 인터페이스는 인스턴스 변수를 가질 수 없다.

디폴트 메서드의 해석 규칙

어떤 클래스가 같은 디폴트 메서드 시그니처를 포함하는 두 인터페이스를 구현하고 있다고 가정해보자. 해당 클래스에서 디폴트 메서드를 호출한다면 어떤 인터페이스의 디폴트 메서드가 호출될까? 디폴트 메서드의 해석 규칙은 크게 3가지 우선순위 원칙을 기반한다

세 가지 해석 규칙

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

1번 규칙과 2번 규칙은 가장 마지막에 구현된 디폴트 메서드를 호출하되, 인터페이스보다는 클래스에 우선순위가 있다는 것으로 간단히 정리할 수 있다. 3번의 경우는 명시적인 오버라이드가 필요한 경우다. 아래와 같은 예시 상황을 통해 살펴보자.

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

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

public class C implements B, A {
	void hello() {
		B.super.hello(); // 명시적으로 특정 인터페이스의 메서드를 선택해야 한다.
	}
}

반면, 아래 예시와 같이 메서드 호출 참조의 끝이 공통된 최상위 메서드를 바라보고 있다면 명시적 오버라이드가 필요하지 않다. 이를 다이아몬드 문제라고 한다. D는 B와 C 인터페이스를 구현한다. 그리고 B와 C는 공통적으로 A 인터페이스를 상속받는다. 따라서 D에서 호출한 hello()는 A의 디폴트 메서드 hello()를 호출하게 된다.

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(); //무엇이 출력될까?
	}
} 

물론 세 가지 해석 규칙에 의해 아래와 같은 상황에서는 다른 결과가 나올 수 있다. 참고해보자.

  • B에서 hello()를 오버라이드 했다면, 2번째 규칙에 의해 B인터페이스의 hello()가 호출되어 Hello from B가 출력될 것이다.
  • B와 C에서 모두 hello()를 오버라이드 했다면, 3번 규칙에 의해 명시적 오버라이드가 필요하다.