- 1. 변화하는 API
- 2. 디폴트 메서드란 무엇인가?
- 3. 디폴트 메서드 활용 패턴
- 4. 해석 규칙
- 5. 마치며
- 6. 배우고 느낀점
전통적인 자바에서 인터페이스와 관련 메서드는 한 몸처럼 구성이된다. 하지만 이전 까지는 인터페이스를 바꾸면 이전에 해당 인터페이스를 구현했던 모든 클래스의 구현도 고쳐야 한다. 자바 8에서는 기본 구현을 포함하는 인터페이스를 정의하는 방법 정적 메서드, 디폴트 메서드 기능을 제공한다.
자바 8에서는 메서드 구현을 포함하는 인터페이스를 정의할 수 있다. 결과적으로 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 된다. 예를 들어, List 인터페이스의 sort를 봐보자.
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
default는 해당 메서드가 디폴트 메서드임을 알린다.
API는 바꾸는 것이 어렵다. 이미 제공된 API를 사용자에게 제공하고, 시간이 지나 API를 바꾸려고 한다면, 사용자가 이전 라이브러리를 이용해 만든 클래스를 API 설계자가 어떻게 할 수가 없다.
public interface Resizable extends Drawable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height);
}
위와 같은 인터페이스를 가지고 있고, 추가된 API는 일단 생략한 채로 생각해보자. 다음은 API를 이용해 사용자가 Ellipse라는 클래스를 만들었다.
public class Ellipse implements Resizable {
@Override
public int getWidth() {
return 0;
}
@Override
public int getHeight() {
return 0;
}
@Override
public void setWidth(int width) {
}
@Override
public void setHeight(int height) {
}
@Override
public void setAbsoluteSize(int width, int height) {
}
@Override
public void draw() {
}
}
또한 이것을 이용해서, 게임과 유틸 클래스를 만들었다.
public class Game {
public static void main(String... args) {
List<Resizable> resizableShapes = Arrays.asList(
new Square(), new Triangle(), new Ellipse());
Utils.paint(resizableShapes);
}
}
public class Utils {
public static void paint(List<Resizable> l) {
l.forEach(r -> {
r.setAbsoluteSize(42, 42);
});
}
}
문제없이 사용자가 구현한대로 잘 굴러갈 것이다. 하지만, 이 상황에서 API가 업그레이드된다면 어떻게 될까?
많은 사용자로 부터 API 설계자는 API의 구현을 개선해 달라는 많은 요청을 받았다. 이에 수정본을 제공했다.
public interface Resizable extends Drawable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height);
// 추가된 API
void setRelativeSize(int widthFactor, int heightFactor);
}
public class Ellipse implements Resizable {
@Override
public int getWidth() {
return 0;
}
@Override
public int getHeight() {
return 0;
}
@Override
public void setWidth(int width) {
}
@Override
public void setHeight(int height) {
}
@Override
public void setAbsoluteSize(int width, int height) {
}
@Override
public void draw() {
}
}
이렇게 API가 업그레이드 되었을 때 사용자는 어떤 문제를 겪게 될까? 먼저 Resizable을 고쳐 몇가지 문제가 발생한다. 첫 번째로 Resizable을 구현하는 모든 클래스는 업데이트된 메서드를 구현해야 한다. 인터페이스에 새로운 메서드를 추가하면 바이너리 호환성(새로 추가된 메서드를 호출하지만 않으면, 새로운 메서드 구현이 없이도 기존 클래스 파일 구현이 잘 동작한다)은 유지된다. 하지만 Resizable에서 추가된 메서드를 사용하도록 코드를 바꾼다면, 문제가 생길 것이다. 두 번째로 전체 애플리케이션을 재빌드할 때, 컴파일 에러가 발생할 수 있다. 공개된 API를 수정하게 된다면 기존 버전과 호환성 문제가 발생하기 때문이다.
이에 자바 8에서는 디폴트 메서드를 이용해 API를 바꿔 기본 구현을 제공하므로, 문제가 생기지 않는다.
바이너리 호환성 : 새로 추가된 메서드를 호출하지만 않으면, 새로운 메서드 구현이 없이도 기존 클래스 파일 구현이 잘 동작한다. 소스 호환성 : 코드를 고쳐도 기존 프로그램을 성공적으로 재컴파일할 수 있다. 동작 호환성 : 코드를 바꾼 다음 입력값이 주어지면, 프로그램이 같은 동작을 수행함.
디폴트 메서드란 뭘까? 인터페이스는 자신을 구현하는 클래스에서 메서드를 구현하지 않을 수 있는 새로운 메서드 시그니처를 제공한다. 그렇다면, 디폴트 메서드를 누가 구현할 까? 인터페이스를 구현하는 클래스에서 구현하지 않은 메서드는 인터페이스 자체에서 기본으로 제공한다. 그래서, 이를 디폴트 메서드라 부르는 것이다. 디폴트 메서드는 default라는 키워드로 시작하며, 다른 클래스에 선언된 메서드처럼 메서드 바디를 포함한다.
public interface Sized {
int size();
default boolean isEmpty() {
return sizz() == 0;
}
}
그렇다면 인터페이스가 구현을 가질 수 있고, 클래스는 여러 인터페이스를 동시에 구현할 수 있으므로, 결국 자바도 다중 상속을 지원하는 것일까? (비슷하게 지원한다.)
디폴트 메서드를 이용하면 라이브러리를 바꿔도 호환성이 유지되는 것을 알게되었다. 그러면 디폴트 메서드를 이용하는 방법 두 가지 선택형 메서드, 동작 다중 상속을 알아보자.
자바 라이브러리를 보면, 인터페이스를 구현하는 클래스에서 메서드의 내용이 비어있는 상황을 본 적이 있을 것이다.(시작 부터 자바 8이라 못봤..) 예를 들어 Iterator 인터페이스를 보면, hasNext, next 뿐만 아니라 remove도 존재하는 걸 알 수 있는데, 기능을 사용하지 않으므로, 기능을 무시했다. 하지만 디폴트 메서드가 추가된 이후로 우리는 쉽게 정의할 수 있다.
interface Iterator<T> {
boolean hasNext();
T next();
default void remove() {
throw new UnsupportedOperationException();
}
}
디폴트 메서드를 이용하면, 기존에 불가능 했던 동작 다중 상속 기능을 구현할 수 있다. 말이 좀 어렵지만, 각각의 기능을 인터페이스로 디폴트 메서드로 구현한 뒤, 다중 상속을 받아 하나로 조립하는 것과 같다.
public clas ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {}
우리는 이를 통해 하나의 기능을 가진 인터페이스를 구현해 다중 상속 형식을 구현할 수 있고, 기능이 중복되지 않는 최소의 인터페이스를 구축할 수 있다. 또한 인터페이스끼리 조합해 새롭게 만들어 낼 수 잇다.
하지만 상속으로 코드 재사용 문제를 모두 해결할 수 있는 것은 아니다 예를 들어 한 개의 메서드를 재사용하려고 100개의 메서드와 필드가 정의되어 있는 클래스를 상속받는 것은 멍청한 행동이다. 이럴 때 델리게이션 멤버 변수를 이용해서 클래스에서 필요한 메서드를 호출하는 메서드를 작성하는게 좋다. 종종 final로 선언된 클래스(String 클래스)가 대표적인 예시다.
자바의 클래스는 부모 클래스만 상속받을 수 있지만, 여러 인터페이스를 동시에 구현할 수 있다. 자바 8에는 디폴트 메서드가 추가되었으므로, 같은 시그니처를 갖는 디폴트 메서드를 상속받는 상황이 생길수도 있다. 지금부터 예시를 보며 알아보자.
먼저 다른 클래스나 인터페이스로부터 같은 시그니처를 갖는 메서드를 상속받을 때에는 세 가지 규칙을 따라야한다.
-
클래스가 항상 이긴다. 클래스나 슈퍼클래스에서 정의한 메서드가 디폴트 메서드보다 우선권을 갖는다.
-
1번 규칙 이외의 상황에서는 서브인터페이스가 이긴다. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브인터페이스가 이긴다. 즉, B가 A를 상속받는다면 B가 A를 이긴다.
-
여전히 디폴트 메서드의 우선순위가 결정되지 않는다면, 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.
static interface A{
void hello();
}
static interface B extends A{
void hello();
}
class C {
new B().hello();
}
// 위와 같은 경우 2번 규칙에 해당되어 서브인터페이스 B가 이긴다.
그렇다면 서로 상속을 주지 않는 인터페이스의 경우는 어떨까?
public interface A {
default void hello() {print("A");}
}
public interface B {
default void hello() {print("B");}
}
public class C implements B, A { }
다음과 같은 경우 인터페이스 내부 상속이 없어 2번 규칙을 적용할 수 없다. A, B를 구분하는 기준이 없으므로, Error가 발생하게 된다.
이것을 해결하기 위해선 자바에서는 X.super.m(...) 형태의 새로운 문법을 제공한다. 여기서 X는 호출하려는 메서드 m의 슈퍼 인터페이스이다.
public class C implements B, A {
void hello() {
B.super.hello();
}
}
그렇다면, 다음을 봐보자
public class Diamond {
public static void main(String... args) {
new D().hello();
}
static interface A {
public default void hello() {
System.out.println("Hello from A");
}
}
static interface B extends A {}
static interface C extends A {}
static class D implements B, C {}
}
위 코드를 UML로 그려보면 다이아몬드 처럼 생긴 모양이 나오게 되는데, 이를 다이아몬드 문제라고 부른다. D는 B와 C중 누구의 디폴트 메서드 정의를 상속받을까? 실제로 선택할 수 있는 메서드는 A만 디폴트 메서드를 정의하므로, Hello from A가 되지만, B, C에도 같은 시그니처의 디폴트 메서드가 있다면, 충돌이 발생해 X.super.m(...)으로 명시적으로 호출해야한다.
-
자바 8의 인터페이스는 구현 코드를 포함하는 디폴트 메서드, 정적 메서드를 정의할 수 있다.
-
디폴트 메서드의 정의는 default 키워드로 시작하며, 일반 클래스 메서드처럼 바디를 갖는다.
-
공개된 인터페이스에 추상 메서드를 추가하면 소스 호환성이 깨진다.
-
디폴트 메서드 덕분에 라이브러리 설계자가 API를 바꿔도 기존 버전과 호환성을 유지할 수 있다.
-
선택형 메서드와 동작 다중 상속에도 디폴트 메서드를 사용할 수 있다.
-
클래스가 같은 시그니처를 갖는 여러 디폴트 메서드를 상속하면서 생기는 충돌 문제를 해결하는 규칙이 있다.
-
클래스나 슈퍼 클래스에 정의된 메서드가 다른 디폴트 메서드 정의보다 우선한다. 이 외의 상황에서는 서브인터페이스에서 제공하는 디폴트 메서드가 선택된다.
-
두 메서드의 시그니처가 같고, 상속관계로도 충돌 문제를 해결할 수 없을 때는 디폴트 메서드를 사용하는 클래스에서 메서드를 오버라이드해서 어떤 디폴트 메서드를 호출할지 명시적으로 결정해야한다.
이해도 잘되고, 쉬어가는 챕터라고 생각이 든다. 이전까지는 다중 상속을 지원하는 걸로 알고있었는데, 읽어보니 다중 상속 비슷하게 지원을 하는걸 깨닫게 되었다.