Published on

Chapter 13. 디폴트 메소드

Chapter 13. 디폴트 메소드

인터페이스에 어떤 기능을 추가하는 일은 쉬운일은 아닙니다.

해당 인터페이스를 구현하는 구현체들을 모두 자신이 제어 하고 있다면 해당 구현체들에 변경된 인터페이스를 추가 구현하면 되지만, 자신이 제어하고 있지 않은 경우 인터페이스를 변경하면 구현체들과 호환이 되지 않아 문제가 발생합니다.

Java 8에서는 이와 같은 문제를 해결하기 위해 2가지 방법을 제공합니다.

  • 정적 메소드
  • 디폴트 메소드

💡 정적 메소드와 인터페이스

보통 자바에서는 인터페이스 그리고 인터페이스의 인스턴스를 활용할 수 있는 다양한 정적 메소드를 정의하는 유틸리티 클래스를 활용합니다.

CollectionsCollection 객체를 활용할 수 있는 유틸리티 클래스 입니다.

Java 8부터는 인터페이스 내부에 정적 메소드를 사용하는것이 가능해졌기 때문에 유틸리티 클래스를 사용하지 않아도 되지만, 과거 버전과의 호환성을 위해 Java API에서는 유틸리티 클래스를 남겨두었습니다.

13.1 변화하는 API

예를 들어, 자바 그리기 라이브러리 설계자가 되었다고 가정하고 라이브러리를 만들었습니다.

Resizable 인터페이스는 모양의 크기를 조절하는데 필요한 메소드들을 정의합니다.

  • getWidth
  • setWidth
  • getHeight
  • setHeight
  • setAbsoluteSize
  • 등등

시간이 지나 해당 기능외에도 더 많은 기능이 필요하다는 것을 알게 되어 추가하려고 합니다.

13.1.1 API 버전 1

public interface Resizable {
	int getWidth();

	void setWidth(int width);

	int getHeight();

	void setHeight(int height);

	void setAbsoluteSize(int width, int height);
}

💡 사용자 구현

public class Ellipse implements Resizable {
	...
}

13.1.2 API 버전 2

setRelativeSize메소드를 추가해달라는 요청이 들어와서 Resizable 인터페이스에 추가하였습니다.

💡 사용자가 겪는 문제

첫 번째로 Resizable 인터페이스를 수정하면 Resizable를 구현하는 구현체는 모두 해당 메소드를 구현해야합니다.

이떄, 인터페이스에 새로운 메소드르 추가만 하면 바이너리 호환성은 유지 됩니다. 하지만 사용할려고 하면 AbstractMethodError 예외가 발생합니다.

두 번째로 사용가자 구현체를 포함하는 모든 애플리케이션을 재빌드할 때 컴파일 에러가 발생하게 됩니다.

💡 바이너리 호환성, 소스 호환성, 동작 호환성

  • 뭔가를 바꾼 이후에도 에러 없이 기존 바이너리가 실행될 수 있는 상황을 바이너리 호환성이라 함
  • 코드를 고친 후, 재컴파일 할 수 있으면 소스 호환성이라 함
  • 코드를 바꾼 후에도 같은 입력값이 주어지면 기존과 동일하게 동작하는지를 동작 호환성이라 함

13.2 디폴트 메소드란 무엇인가?

Java 8에서는 호환성을 유지하면서 API를 변경할 수 있도록 디폴트 메소드를 제공합니다.

이제 인터페이스는 구현체에서 구현하지 않아도 되는 새로운 메소드 시그니처(= 디폴트 메소드)를 제공합니다.

디폴트 메소드 시그니처는 간단합니다. default키워드를 반환타입 보다 앞에 작성해주면 됩니다.

이제 인터페이스 자체적으로도 구현을 가질 수 있게 되었습니다.

여기서 2가지 의문이 들었습니다.

  • 추상 클래스와 차이점은?
  • 상속은 하나만 가능하지만 인터페이스는 여러개를 구현할 수 있다면, 다중 상속이 가능하지 않나?

이에 대한 대답은 아래에 하나씩 나오게 됩니다.

💡 추상 클래스와 Java 8의 인터페이스

Java 8 부터는 인터페이스도 바디를 가지는 메소드를 정의할 수 있으므로 이 둘의 차이가 크게 안느껴질수도 있습니다.

  • 클래스는 하나의 추상 클래스만 상속받을 수 있지만, 인터페이스는 여러 개를 구현할 수 있음
  • 추상 클래스는 인스턴스 변수로 공통 상태를 가질 수 있지만, 인터페이스는 인스턴스 변수를 가질 수 없음

13.3 디폴트 메소드 활용 패턴

13.3.1 선택현 메소드

인터페이스를 구현하는 클래스에서 메소드의 내용이 비어있는 상황을 본적이 있을 것이라고 합니다. (저는 하나하나 다 까보진 않아서 없음)

예를 들어 Iterator 인터페이스remove 메소드 같은 경우 잘 사용되지 않기 때문에 Iterator를 구현하는 클래스는 remove 메소드를 빈 구현으로 제공하였습니다.

디폴트 메소드를 이용하면 각 구현체마다 불필요하게 빈 구현체를 제공하는 대신 Iterator 인터페이스에서 처리할 수 있습니다.

interface Iterator<T> {
	boolean hasNext();

	T next();

	default void remove() {
		throw UnsupportedOperationException();
	}
}

13.3.2 동작 다중 상속

상속은 하나만 가능하지만 인터페이스는 다중 구현이 가능하므로, 바디를 가진 메소드를 구현한 인터페이스를 구현한다면 다중 상속처럼 사용할 수 있습니다.

이 자체는 어려운 내용이 아니라 추가적인 코멘트는 달지 않지만, 다중 상속이 가능해지면 똑같은 시그니처를 가진 인터페이스 2개를 구현한다면 어떤걸 사용하게 되지? 같은 의문이 듭니다.

13.4 해석 규칙

대규칙은 항상 구체적인게 이깁니다.
아래의 규칙도 대규칙을 따르는 것입니다.

  1. 클래스에서 정의한게 항상 이김
  2. 1번 규칙 이외의 상황에서는 서브 인터페이스가 이김
  3. 위의 상황으로 해결되지 않을 경우 명시적으로 디폴트 메소드를 호출해줘야 함

💡 다이아몬드 문제

public interface A {

	default void hello() {
		System.out.println("A");
	}
}

interface B extends A {}

interface C extends A {}

class D implements B, C {

	public static void main(String[] args) {
		D d = new D();
		d.hello();
	}
}

B, C 클래스 중 어떤 인터페이스의 hello()를 실행할지 명시적으로 적어주어야 할 것 같지만, 실질적으로 hello()A 인터페이스에 선언된거 뿐이기 때문에 결과는 A가 찍혀나옵니다.

  • B, C 중 하나만 오버라이딩 한다면 2번 규칙에 의해 오버라이딩 한 hello()가 실행됨
  • B, C 클래스에서 각각 오버라이딩을 한다면 3번 규칙에 의해 명시적으로 어떤 인터페이스의 hello()를 사용할지 적어주어야 함

13.5 마치며

  • Java 8의 인터페이스는 구현 코드를 포함하는 디폴트 메소드, 정적 메소드를 정의할 수 있음
  • 디폴트 메소드default 키워드로 시작하며 일반 클래스 메소드처럼 바디를 가짐
  • 공개된 인터페이스에 추상 메소드를 추가하면 소스 호환성이 꺠짐
  • 디폴트 메소드 덕분에 라이브러리 설계자가 API를 바꿔도 기존 버전호환성 유지
  • 선택형 메소드동작 다중 상속에도 디폴트 메소드를 사용할 수 있음
  • 클래스가 같은 시그니처를 갖는 여러 디폴트 메소드를 상속하면 생기는 충돌 문제는 대규칙인 구체적인게 항상 우선순위를 가지며, 우선순위가 동일할 경우 명시적으로 어떤 인터페이스의 메소드를 사용할지 작성해야 함