이번 주제 키워드
- 디폴트 메서드란?
- 진화되는 API가 호환성을 유지하는 방법
- 디폴트 메서드 활용 패턴
- 해결 규칙
인터페이스를 수정해야하는 경우
- 바이너리 호환성은 유지되지만 소스 호환성은 유지되지 않아 해당 인터페이스를 구현한 모든 클래스를 수정해야한다.
- 따라서 공개된 자바 API를 고치는 일은 굉장히 어려운 일이었다.
- 자바 8에서는 이러한 문제를 해결하는 두 가지 방법을 제공한다.
- 인터페이스 내부에
정적 메서드
를 사용하는 방법
- 인터페이스의 기본 구현을 제공할 수 있도록
디폴트 메서드
기능을 사용하는 방법
- 즉, 자바8부터 메서드 구현을 인터페이스를 정의할 수 있어서 기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 된다.
- 따라서 기존의 코드 구현을 바꾸도록 강요하지 않으면서도 인터페이스를 바꿀 수 있다.
호환성(참고)
- 바이너리 호환성
- 뭔가를 바꾼 이후에도 에러 없이 기존 바이너리가 실행될 수 있는 상황.
- ex) 인터페이스에 메서드를 추가했을 때 추가된 메서드를 호출하지만 않으면 문제가 일어나지 않는 경우
- 소스 호환성
- 코드를 고쳐도 기존 프로그램을 성공적으로 재컴파일할 수 있는 상황.
- ex) 마찬가지로 인터페이스에 메서드를 추가하는 경우는 소스 호환성이 아니다.
- 동작 호환성
- 코드를 바꾼 다음에도 같은 입력값이 주어지면 프로그램이 같은 동작을 실행하는 상황.
디폴트 메서드
- 자바 8에서는 호환성을 유지하면서 API를 바꿀 수 있도록 새로운 기능인 디폴트 메서드(default method)를 제공한다.
- 이제 인터페이스는 자신을 구현하는 클래스에서 메서드를 구현하지 않을 수 있는 새로운 메서드 시그니처를 제공한다.
- 인터페이스를 구현하는 클래스에서 구현하지 않은 메서드는 디폴트 메서드를 통해 인터페이스 자체에서 기본으로 제공한다.
- 디폴트 메서드는
default
라는 키워드로 시작하며 다른 클래스에 선언된 메서드처럼 메서드 바디를 포함한다.
이렇게 되면 이미 존재하는 추상 클래스와 자바 8의 인터페이스가 무엇이 다르냐고 물어볼 수 있다.
- 클래스는 하나의 추상 클래스만 상속받을 수 있지만 인터페이스를 여러 개 구현할 수 있다.
- 추상 클래스는 인스턴스 변수(필드)로
공통 상태
를 가질 수 있다. 하지만 인터페이스는 인스턴스 변수를 가질 수 없다.
디폴트 메서드 활용 패턴
- 디폴트 메서드를 이용하는 두 가지 방식은 선택형 메서드(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번 규칙 이외의 상황에서는
서브인터페이스
가 이긴다. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브인터페이스가 이긴다.
- 여전히 디폴트 메서드의 우선순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가
명시적으로 디폴트 메서드를 오버라이드
하고 호출해야 한다.
충돌 그리고 명시적인 문제해결
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를 사용할지 명시적으로 선택해서 에러를 해결해야 한다.