Java8 In Action - 7. 디폴트 메서드


Java8 In Action 을 읽고 정리한 내용이다. 디폴트 메서드란 무엇이며 API가 바뀌면서 발생한 문제를 디폴트 메서드로 어떻게 해결할 수 있는 설명한다. 그리고 디폴트 메서드를 만들어 다중 상속을 달성하는 방법을 보여준다.

변화하는 API

우리가 인기 있는 자바 그리기 라이브러리 설계자가 되었다고 가정한다.

  1. 모양의 크기를 조정하는 데 필요한 setHeigh, setWidth, getHeight, getWidth, setAbsoluteSize 등의 메서드를 정의하는 Resizable 인터페이스 생성
  2. Rectagle이나, Square처럼 Resizable을 구현하는 클래스 제공
  3. 일부 사용자가 직접 Resizable 인터페이스를 구현하는 Ellipse라는 클래스 구현
  4. API를 릴리즈한지 몇개월이 지나면서 Resizable에 몇 가지 기능이 부족하여 크기 조절 인수로 모양의 크기를 조절할 수 있는 setRelativeSize라는 메서드를 추가
  5. ResizablesetRelativeSize를 추가하고 SquareRectangle구현을 수정

문제점 : 바로 자바 라이브러리 설계자가 라이브러리를 바꾸고 싶을 때 같은 문제가 발생

API 버전 1


Resizable 인터페이스 초기 버젼은 다음과 같은 메서드를 포함한다.

Resizable.java
1
2
3
4
5
6
7
public interface Resizable extends Drawable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height);
}

사용자 구현

우리 라이브러리를 즐겨 사용하는 사용자 중 한 명은 직접 Resizable을 구현하는 Ellipse클래스를 만들었다.

Ellipse.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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() {

}
}

이 사용자는 다양한 Resizable 모양(자신이 만든 Ellipse를 포함해서)을 처리하는 게임을 만들었다.

Game.java
1
2
3
4
5
6
7
public class Game {

public static void main(String...args){
List<Resizable> resizableShapes = Arrays.asList(new Square(), new Triangle(), new Ellipse()); // 크기를 조절할 수 있는 모양 리스트
Utils.paint(resizableShapes);
}
}
Utils.java
1
2
3
4
5
6
7
8
9
10
11
public class Utils {

public static void paint(List<Resizable> l){
public static void paint(List<Resizable> l){
l.forEach(r -> {
r.setAbsoluteSize(42, 42); // 각 모양에 setAbsoluteSize 호출
r.draw();
});
}

}

API 버전 2


몇 개월이 지나자 Resizalbe을 구현하는 SquareRectangle구현을 개선해달라는 많은 요청을 받았다.

Resizable.java
1
2
3
4
5
6
7
8
public interface Resizable extends Drawable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height);
void setRelativeSize(int wFactor, int hFactor); // API 버전 2에 추가된 새로운 메서드
}

사용자가 겪는 문제

Resizable을 고치면 몇가지 문제가 발생한다.

  1. Resizable을 구현하는 모든 클래스는 setRelativeSize 메서드를 구현해야 한다. 하지만 라이브러리 사용자가 직접 구현한 EllipsesetRelativeSize 메서드를 구현해야 한다.
  2. 공개된 API를 고치면 기존 버전과의 호환성 문제가 발생한다.
  3. 사용자가 라이브러리를 관리하면 결국 프로젝트에서 로딩해야 할 클래스 파일이 많아지면서 메모리 사용과 로딩 시간 문제가 발생한다.

위와 같은 문제를 디폴트 메서드로 해결할 수 있다.

디폴트 메서드란 무엇인가?

자바8에서는 호환성을 유지하면서 API를 바꿀 수 있도록 새로운 기능인 디폴트 메서드(default method)를 제공한다. 우선 디폴트 메서드는 defalut라는 키워드로 시작하며 다른 클래스에 선언된 메서드처럼 메서드 바디를 포함한다.

Sized.java
1
2
3
4
5
6
public interface Sized {
int size();
default boolean isEmpty() {
return size() == 0;
}
}

이제 Sized 인터페이스를 구현하는 모든 클래스는 isEmpty의 구현도 상속 받는다. 즉, 인터페이스에 디폴트 메서드를 추가하면 소스 호환성이 유지된다.

추상 클래스와 자바 8의 인터페이스

추상 클래스와 인터페이스는 다른점은, 둘 다 추상 메서드와 바디를 포함하는 메서드를 정의 할 수 있다.

  1. 클래스는 하나의 추상 클래스만 상속받을 수 있지만 인터페이스를 여러 개 구현할 수 있다.
  2. 추상 클래스는 인스턴스 변수(필드)로 공통 상태를 가질 수 있다. 하지만 인터페이스는 인스턴스 변수를 가질 수 없다.

디폴트 메서드 활용 패턴

디폴트 메서드를 이용하는 두가 방식, 선택형 메서드(optional method)동작 다중 상속(multiple inheritance of behavior)을 설명한다.

선택형 메서드

예를 Iterator인터페이스를 보자. IteratorhasNextnext뿐 아니라 remove메서드도 정의한다. 사용자들이 remove 기능은 잘 사용하지 않으므로 자바 8 이전에는 remove기능을 무시했다. 결과적으로 Iterator를 구현하는 많은 클래스에서는 remove에 빈 구현을 제공했다. 자바8의 Iterator 인터페이스는 다음처럼 remove 메서드를 정의한다.

Iterator.java
1
2
3
4
5
6
7
interface Iterator<T> {
boolean hasNext();
T nexT()
default void remove() {
throw new UnsupportedOperationException();
}
}

기본 구현이 제공되므로 Iterator 인터페이스를 구현하는 클래스는 빈 remove 메서드를 구현 할 필요가 없어졌고, 불필요한 코드를 줄일 수 있다.

동작 다중 상속

자바에서 클래스는 한 개의 다른 클래스만 상속할 수 있지만 인터페이스는 여러 개 구현할 수 있다. 다음은 자바 API에 정의된 ArrayList클래스다.

ArrayList.java
1
2
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable, Iterable<E>, Collection<E> {
}

해석 규칙

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

소스코드

참조