[모던 자바 인 액션] chap13. 디폴트 메서드

2023. 3. 30. 16:48Java

내용
디폴트 메서드란 무엇인가?
진화하는 API가 호환성을 유지하는 방법
디폴트 메서드의 활용 패턴
해결 규칙

전통적인 자바에서 인터페이스와 관련 메서드는 한 몸처럼 구성된다. 인터페이스를 구현하는 클래스는 인터페이스에서 정의하는 모든 메서드 구현을 제공하거나 아니면 슈퍼 클래스의 구현을 상속받아야 한다. 기존의 List 인터페이스를 구현했던 모든 프로그래머가 sort 메서드를 구현하도록 List 인터페이스를 상속ㄴ한 모든 클래스를 고쳐야 한다고 발표한다면 당황스러울 것이다.

하지만 걱정 할 필요 없다! 자바 8 에서는 이 문제를 해결하는 새로운 기능을 제공한다.

 

1. 변화하는 API

이미 릴리즈된 인터페이스를 고치면 어떤 문제가 발생하는지 살펴보자.

 

1. API 버전 1
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 클래스를 만들었다.

public class Ellispe implements Resizable {
	...
}

// 이 사용자는 Resizable 모양을 처리하는 게임을 만들었다.

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);		// 각 모양에 setAbsoluteSize 호출
    	});
    }
}

 

2. API 버전 2

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

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);		// 버전2 에 추가될 새로운 메서드
}

Resizable에 메서드를 추가하면서 API가 바뀌었다. 인터페이스를 바꾼 다음에 애플리케이션을 재컴파일하면 에러가 발생한다.

 

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

자바 8에서는 호환성을 유지하면서 API를 바꿀 수 있도록 새로운 기능인 디폴트 메서드를 제공한다. 이제 인터페이스는 자신을 구현하는 클래스에서 메서드를 구현하지 않을 수 있는 새로운 메서드 시그니처를 제공한다. 그럼 누가 디폴트 메서드를 구현할까? 인터페이스를 구현하는 클래스에서 구현하지 않은 메서드는 인터페이스 자체에서 기본으로 제공한다.

public interface Sized {
	int size();
    default boolean isEmpty() {
    	return size() == 0;
    }
}

이제 Sized 인터페이스를 구현하는 모든 클래스는 isEmpty의 구현도 상속받는다. 즉 인터페이스에 디폴트 메서드를 추가하면 소스 호환성이 유지된다. 이제 Resizable 예제로 다시 돌아간다면, 디폴트 메서드를 구현해 제공한다면 호환성을 유지하면서 라이브러리를 고칠 수 있다.

void setRelativeSize(int wFactor, int hFactor) {
	setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
}

인터페이스가 구현을 가질 수 있고 클래스는 여러 인터페이스를 동시에 구현할 수 있으므로 결국 자바도 다중 상속을 지원하는 걸까? 인터페이스를 구현하는 클래스가 디폴트 메서드와 같은 메서드 시그니처를 정의하거나 아니면 디폴트 메서드를 오버라이드 한다면 어떻게 될까? 이는 13.4절에서 살펴본다.

 

3. 디폴트 메서드 활용 패턴

디폴트 메서드를 다른 방식으로도 활용할 수 있을까? 이 절에서는 디폴트 메서드를 이용하는 두 가지 방식, 선택형 메서드와 동작 다중 상속을 설명한다.

 

1. 선택형 메서드

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

interface Iterator<T> {
	boolean hasNext();
    T next();
    default void remove() {
    	throw new UnsupportedOperationException();
    }
}

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

 

2. 동작 다중 상속

디폴트 메서드를 이용하면 기존에는 불가능했던 동작 다중 상속 기능도 구현할 수 있다.

public class ArrayList<E> extends AbstractList<E>		// 한개의 클래스를 상속
	implements List<E>, RandomAccess, Cloneable, Serializable {
    		// 네개의 인터페이스를 구현
}

여기서 ArrayList는 한 개의 클래스를 상속받고, 여섯 개의 인터페이스를 구현한다. 결과적으로 ArrayList는 AbstractList, List, RandomAccess, Cloneable, Serializable, Iterable, Collection의 서브형식이 된다. 따라서 디폴트 메서드를 사용하지 않아도 다중 상속을 활용할 수 있다.

 

기능이 중복되지 않는 최소의 인터페이스

public interface Rotatable {
	void setRotationAngle(int angleInDegress);
    int getRotationAngle();
    default void rotateBy(int angleInDegress) {
    	setRotationAngle((getRotationAngle() + angleInDegerss) % 360);
    }
}

public interface Moveable {
	int getX();
    int getY();
    void setX(int x);
    void setY(int y);
    
    default void moveHorizontally(int distance) {
    	setX(getX() + distance);
    }
    
    default void moveVertically(int distance) {
    	setY(getY() + distance);
	}
}

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) {
  	setAbsoluteSize(getWidth() / wFActor, getHeight() / hFactor);
  }
}
public class Monster implements Rotatable, Moveable, Resizable {
	...	// 모든 추상 메서드의 구현은 제공해야 하지만 디폴트 메서드의 구현은 제공할 필요가 없다.
}

Monster m = new Monster();
m.rotateBy(100);	// Rotatable의 rotateBy 호출
m.moveVertically(10);	// Moveable의 moveVertically 호출

public class Sun impelments Moveable, Rotatable {
	...
}

위 인터페이스를 쓰는 시나리오

인터페이스에 디폴트 구현을 포함시키면 또 다른 장점이 생긴다. 예를 들어 moveVertically의 구현을 더 효율적으로 고쳐야 한다고 가정하자. 디폴트 메서드 덖분에 Moveable 인터페이스를 직접 고칠 수 있고 따라서 Moveable을 구현하는 모든 클래스도 자동으로 변경한 코드를 상속받는다(구현 클래스에서 메서드를 정의하지 않은 상황에 한해서).

 

4. 해석 규칙

자바 8에는 디폴트 메서드가 추가되었으므로 같은 시그니처를 갖는 디폴트 메서드를 상속받는 상황이 생길 수 있다. 이런 상황에서는 어떤 인터페이스의 디폴트 메서드를 사용하게 될까? 실전에서 자주 일어나는 일은 아니지만 이를 해결할 수 있는 규칙이 필요하다.

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 clas C implements B, A {
	public static void main(String... args) {
    	new C().hello();	// 무엇이 출력 될까?
    }
}

 

 

1. 알아야 할 세 가지 해결 규칙

다른 클래스나 인터페이스로부터 같은 시그니처를 갖는 메서드를 상속받을 때는 세 가지 규칙을 따라야 한다.

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

 

2. 디폴트 메서드를 제공하는 서브인터페이스가 이긴다.

B와 A를 구현하는 클래스 C의 예제를 보자. B와 A는 hello라는 디폴트 메서드를 정의한다. 또한 B는 A를 상속받는다. 컴파일러는 누구의 hello 메서드 정의를 사용할까? 2번 규칙에서는 서브인터페이스가 이긴다고 설명한다. 즉 B가 A를 상속받았으므로 컴파일러는 B의 hello를 선택한다. 따라서 프로그램은 'Hello from B'를 출력한다.

public class D implements A {}
public class C extends D implements B, A {
	public static void main(String... args) {
    	new C().hello();	// 무엇이 출력될까?
    }
}

1번 규칙은 클래스의 메서드 구현이 이긴다고 설명한다. D는 hello를 오버라이드하지 않았고 단순히 인터페이스 A를 구현했다. 따라서 D는 인터페이스 A의 디폴트 메서드 구현을 상속받는다. 2번 규칙에서는 클래스나 슈퍼클래스에서 메서드 정의가 없을 때는 디폴트 메서드를 정의하는 서브인터페이스가 선택된다. 따라서 컴파일러는 인터페이스 A의 hello나 B의 hello 둘 중 하나를 선택해야 한다. 여기서 B가 A를 상속받는 관계이므로 이번에도 'Hello from B'가 출력된다.