7 min to read
자바 8 인 액션
9. 디폴트 메서드
전통적인 자바에서 인터페이스와 관련 메서드는 한 몸처럼 구성된다. 인터페이스를 구현하는 클래스는 인터페이스에서 정의하는 모든 메서드 구현을 제공하거나 아니면 슈퍼클래스의 구현을 상속받아야 한다.
평소에는 큰 문제가 없지만 라이브러리 설계자 입장에서 인터페이스에 새로운 메서드를 추가하는 등 인터페이스를 수정하고 싶을 때는 이전에 해당 인터페이스를 구현했던 모든 클래스의 구현도 고쳐야 하는 문제
가 발생한다.
하지만, 자바 8에서는 두 가지 방법으로 이 문제를 해결한다.
- 인터페이스 내부에
정적 메서드(static method)를 사용
하는 방법 - 인터페이스의 기본 구현을 제공할 수 있도록
디폴트 메서드(default method)
기능을 사용하는 방법
즉, 자바 8에서는 메서드 구현을 포함하는 인터페이스를 정의할 수 있다.
결과적으로 기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 된다. 이로써 기존의 코드 구현을 바꾸도록 강요하지 않으면서도 인터페이스를 바꿀 수 있다.
- ex.
default void sort(Comparator<? super E> c) {
Collectors.sort(this, c);
}
List<Integer> numbers = Arrays.asList(3, 5, 1, 2, 6);
numbers.sort(Comparator.naturalOrder()); // sort는 List 인터페이스의 디폴트 메서드다.
반환형식 void 앞 default라는 키워드를 통해 디폴트 메서드를 선언하고 덕분에 리스트에 직접 sort를 호출할 수 있게 되었다.
디폴트 메서드는 주로 라이브러리 설계자들이 사용한다. 정리하면 디폴트 메서드를 이용하면 자바 API의 호환성을 유지하면서 라이브러리를 변경할 수 있다.
디폴트 메서드가 없던 시절에는 인터페이스에 메서드를 추가하게 되면 해당 인터페이스를 구현하고 있던 기존 클래스에 새로 추가된 메서드를 구현하도록 일일히 고쳐야 했지만, 디폴트 메서드를 이용하면 인터페이스의 기본 구현을 그대로 상속하므로 자유롭게 새로운 메서드를 추가할 수 있게 된다.
정적 메서드와 인터페이스
자바 8에서는 인터페이스에 직접 정적 메서드를 선언할 수 있으므로, 유틸리티 클래스(다양한 정적 메서드를 정의하는 클래스)를 없애고 직접 인터페이스 내부에 정적 메서드를 구현할 수 있다.
변화하는 API
API 버전 1
사용자 구현
API 버전 2
사용자가 겪는 문제
인터페이스에 새로운 메서드를 추가하면 바이너리 호환성
은 유지된다. 하지만, 언젠가는 누군가 새로 추가된 메서드를 하위 클래스에서 호출하게 된다면 아래와 같은 런타임 에러가 발생할 것이다.
Exception in thread "main" java.lang.AbstractMethodError:
com.company.Ellipse.setRelativeSize(II)V
두 번째로 사용자가 전체 애플리케이션을 재빌드하게 되면 아래와 같은 컴파일 에러가 발생한다.
com/company/Ellipse.java:6: error: Ellipse is not abstract and does not override abstract method setRelativeSize(int,int) in Resizable
자바 프로그램 변경과 관련된 호환성의 종류
바이너리 호환성
새로 추가된 메서드를 호출하지만 않으면
새로운 메서드 구현이 없이도 기존 클래스 파일 구현이 정상 동작한다. 인터페이스에 메서드를 추가했을 때는 바이너리 호환성을 유지하지만, 인터페이스를 구현하는 클래스를 재컴파일하면 에러가 발생한다.
뭔가를 바꾼 이후에도 에러 없이 기존 바이너리가 실행될 수 있는 상황을바이너리 호환성
이라고 한다.(바이너리 실행에는 인증, 준비 해석 등의 과정이 포함된다.)소스 호환성
코드를 고쳐도 기존 프로그램을 성공적으로 재컴파일할 수 있음을 말한다.
예를 들면, 인터페이스에 메서드를 추가하면 소스 호환성이 아니다. 추가한 메서드를 구현하도록 클래스를 고쳐야 하기 때문이다.동작 호환성
코드를 바꾼 다음에도 같은 입력값이 주어지면 프로그램이 같은 동작을 실행한다는 의미다.
예를 들면, 인터페이스에 메서드를 추가하더라도 프로그램에서 추가된 메서드를 호출할 일은 없으므로(혹은 우연히 구현 클래스가 이를 오버라이드했을 수도 있다.) 동작 호환성은 유지된다.
디폴트 메서드란 무엇인가?
자바 8은 호환성을 유지하면서 API를 바꿀 수 있는 디폴트 메서드
를 제공한다. 디폴트 메서드를 사용하면 소스 호환성
이 유지된다.
추상 클래스와 자바 8의 인터페이스
추상 클래스와 인터페이스의 차이는 무엇일까? 둘 다 추상 메서드와 바디를 포함하는 메서드를 정의할 수 있다.
- 클래스는 하나의 추상 클래스만 상속받을 수 있지만, 인터페이스는 여러 개 구현할 수 있다.
- 추상 클래스는 인스턴스 변수(필드)로 공통 상태를 가질 수 있지만, 인터페이스는 인스턴스 변수를 가질 수 없다.
디폴트 메서드 활용 패턴
우리가 만드는 인터페이스에도 디폴트 메서드를 추가할 수 있다. 디폴트 메서드를 이용하는 방식으로 선택형 메서드(optional method)
와 동작 다중 상속(multiple inheritance of behavior)
두 가지가 있다.
선택형 메서드
자바 8이전에는 인터페이스를 구현할 때 마다 필요없는 추상메서드도 반드시 구현을 해줬어야 했으므로 빈 구현을 하게됐었지만, 자바 8부터는 디폴트 메서드를 통해 인터페이스부터 구현해주게 되면 구현 클래스에 쓸데없는 빈 구현 코드를 작성하지 않아도 된다.
동작 다중 상속
자바에서 클래스는 한 개의 다른 클래스만 상속할 수 있지만 인터페이스는 여러 개 구현할 수 있다.
- ArrayList 예시
public class ArrayList<E> extends AbstractList<E> // 한 개의 클래스를 상속받는다.
implements List<E>, RandomAccess, Cloneable, java.io.Serializable // 여러 개의 인터페이스를 구현한다.
{
}
다중 상속 형식
ArrayList는 한 개의 클래스를 상속받고, 여러 개의 인터페이스를 구현한다. 결과적으로 ArrayList는 AbstractList
자바 8에서는 인터페이스가 구현을 포함할 수 있으므로(디폴트 메서드) 클래스는 여러 인터페이스에서 동작(구현 코드)을 상속받을 수 있다.
다중 동작 상속을 활용하면 최소한의 인터페이스를 유지하면서 코드에서 동작을 쉽게 재사용하고 조합할 수 있다.
인터페이스 조합
인터페이스 조합을 통해 효율적인 메서드 사용이 가능해진다.
옳지 못한 상속
한 개의 메서드를 재사용하기 위해 100개의 메서드와 필드가 정의되어 있는 클래스를 상속받는 것은 좋지 못하다.
이 때는 델리게이션(delegation), 즉 멤버 변수를 이용해서 클래스에서 필요한 메서드를 직접 호출하는 메서드를 작성하는 것이 좋다.
종종 final이 선언된 클래스를 볼 수 있는데 이는 다른 클래스가 이 클래스를 상속받지 못하게 함으로써 원래 동작이 바뀌지 않길 원하기 떄문이다. 이로써 다른 누군가 final이 선언된 클래스의 핵심 기능을 바꾸지 못하도록 제한할 수 있다.
디폴트 메서드도 이러한 규칙을 적용하여 필요한 기능만 포함하도록 인터페이스를 최소한으로 유지한다면 필요한 기능만 선택할 수 있으므로 쉽게 기능을 조립할 수 있다.
해석 규칙
알아야 할 세 가지 해결 규칙
다른 클래스나 인터페이스로부터 같은 시그니처를 갖는 메서드를 상속받을 때는 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 {}
위 3번과 같이 인터페이스 간의 우선순위가 없는 채로 인터페이스를 구현한다면 자바 컴파일러는 Error: class C inherits unrelated defaults for hello() from types B and A.
같은 에러가 발생한다.
충돌 해결
위와 같은 경우 해결방법은 개발자가 직접 클래스 C에서 사용하려는 메서드를 명시적으로 선택
해야 한다. 즉, 클래스 C에서 hello 메서드를 오버라이드한 다음 호출하려는 메서드를 명시적으로 선택해야 한다.(ex. X.super.m(…))
pubic class C implements B, A {
void hello() {
B.super.hello(); // 명시적으로 인터페이스 B의 메서드를 선택한다.
}
}
요약
- 자바 8의 인터페이스는 구현 코드를 포함하는 디폴트 메서드, 정적 메서드를 정의할 수 있다.
- 디폴트 메서드의 정의는 default 키워드로 시작하며 일반 클래스 메서드처럼 바디를 갖는다.
- 공개된 인터페이스에 추상 메서드를 추가하면 소스 호환성이 깨진다.
- 디폴트 메서드 덕분에 라이브러리 설계자가 API를 바꿔도 기존 버전과 호환성을 유지할 수 있다.
- 선택형 메서드와 동작 다중 상속에도 디폴트 메서드를 사용할 수 있다.
- 클래스가 같은 시그니처를 갖는 여러 디폴트 메서드를 상속하면서 생기는 충돌 문제를 해결하기 위한 규칙이 있다.
- 클래스나 슈퍼클래스에 정의된 메서드가 다른 디폴트 메서드 정의보다 우선된다. 이 외의 상황에서는 서브인터페이스에서 제공하는 디폴트 메서드가 선택된다.
- 두 메서드의 시그니처가 같고, 상속관계도 충돌 문제를 해결할 수 없을 때는 디폴트 메서드를 사용하는 클래스에서 메서드를 오버라이드해서 어떤 디폴트 메서드를 호출할지 명시적으로 결정해야 한다.
Comments