본문 바로가기
Language/Java

[Java 8] Default Method

by ocwokocw 2021. 2. 11.

- 출처: 자바 8 in action

- Default Method

자바 8 에서는 인터페이스 안에 기본 구현을 포함하는 기능을 제공한다. static method를 정의하거나 default method를 정의할 수 있다. 동작하는 구현로직은 인터페이스를 구현하는 클래스에서 정의하면 되는데, 왜 이기능이 필요하고 유용한것일까 라는 의문이 들 수도 있다.

 

만약 List 인터페이스에 어떤 메소드를 추가한다고 가정해보자. 만약 기본 구현을 정의할 수 없다면 여러분은 List를 구현하는 모든 클래스에 해당 메소드 추가 및 기본 구현을 추가해줘야 한다. 만약에 해당 인터페이스를 라이브러리 여기저기서 사용하고 있다면 엄청난 문제가 발생한다.

 

기존코드와 코드 구현을 바꾸도록 강제하지 않으면서 기능을 추가할 수 있는 것이다. 여러분이 라이브러리를 설계하는 수준의 개발자가 아니라면 쓸일이 없을지도 모르지만 유용한 기능이니 한번 살펴보도록 하자.


- 기본 문법

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

 

위의 코드는 Java List 인터페이스의 sort 메소드이다. void 앞에 default 키워드가 보일 것이다. 이 메소드가 디폴트 메소드임을 가리키는 키워드이다.


- 사용예제

만약 내가 Captcha(자동화된 사람과 컴퓨터를 판별, 웹서핑을 하다보면 로봇으로 수행하는 작업을 막기 위해 그림을 선택하라는것을 본적이 있을것이다.) 라이브러리를 제공하는 개발자라고 가정해보자. 아래 인터페이스를 이용하면 라이브러리 사용자가 자신만의 Captcha를 만들 수 있도록 제공하는 기능이 있다.

 

초기에는 아래와 같이 간단하게 문자열만 발생시키는 API만 설계하였다.

 

public interface CustomCaptcha {

	String generateString();
}

 

시간이 흘러 사진선택 기능과 네트워크 오류로 인하여 새로고침 기능인 drawImage()와 refresh() 함수도 추가하였다.

 

public interface CustomCaptcha {

	String generateString();
	
	void drawImage();
	
	void refresh();
}

 

기존에 이 인터페이스를 사용하여 자신만의 Catpcha 서비스를 제공하던 라이브러리 사용자들은 어떻게 될까? 물론 구현 클래스를 재 컴파일을 하지 않으면 바이너리 호환성은 유지된다. 하지만 CustomCaptcha를 재컴파일 해야할 경우가 있을 때는 호환이 유지되지 않는다.

 

이때 아래와 같이 default 메소드로 기본 동작을 제공하고, 라이브러리 사용자가 필요에 따라서만 다시 구현하면 이 문제를 해결할 수 있다. API 설계자가 호환성 문제를 잘 해결하면서 새로운 기능도 추가한것이다.

 

public interface CustomCaptcha {

	String generateString();
	
	default void drawImage() {
		System.out.println("This is default method: drawImage");
	};
	
	default void refresh() {
		System.out.println("This is default method: refresh");
	}
}

 

- 추상 클래스와 인터페이스

이렇게 되면 추상 클래스가 더 이상 의미가 있는가라고 생각할 수도 있다. 클래스는 하나의 추상 클래스만 상속이 가능하지만 인터페이스를 여러 개 구현한다. 또 추상 클래스는 인스턴스 변수로 공통 상태를 가질 수 있지만 인터페이스는 인터페이스 변수를 가질 수 없다.


- 동작 다중 상속

디폴트 메소드로 기존에는 불가했던 동작 다중 상속을 할 수 있다. Java는 1 클래스만 확장할 수 있는 단일 상속이지만 사실 인터페이스는 여러 개를 구현할 수 있기 때문에 다중 상속을 활용할 수 있었다.

 

기존에도 되던 기능인데 무엇이 특별한가라고 생각할 수도 있지만 소제목 앞에 동작이라는 말이 붙은 의미를 생각해보자. 기존에는 메소드 시그니처 관련해서만 다중 상속이었지만 이제는 동작을 추가할 수 있으므로 동작 다중 상속이 가능해지게 되었다.


- 해석 규칙

만약 같은 메소드 시그니처를 제공하는 두 인터페이스가 디폴트 메소드를 제공하고, 어떤 클래스에서 두 인터페이스를 모두 구현하였다면 어떤 인터페이스의 디폴드 메소드가 동작하게 되는 것일까? 자바는 아래 세 가지 규칙에 의해서 해석된다.

  • 클래스가 인터페이스 보다 우선한다.
  • 서브 인터페이스가 우선한다. B가 A를 상속받았다면 B가 우선한다.
  • 여전히 디폴트 메소드 우선순위가 정해지지 않았다면 상속받은 클래스가 명시적으로 디폴트 메소드를 오버라이드해서 호출해야 한다.

- 서브 인터페이스 우선 규칙

아래와 같이 C클래스에서 A를 확장한 B와 A를 동시에 구현하였을 때, 어떤 default 메소드가 호출될까? 2번째 규칙인 서브 인터페이스(A를 상속한 B가 서브인터페이스)가 우선하므로 Hello B가 프린트된다.

 

public interface A {

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

public interface B extends A{

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

public class C implements A, B{

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

 

- 구현 클래스 우선 규칙

아래와 같이 A를 구현하는 D클래스를 추가하고 C클래스에서 D를 상속받으면 어떻게 될까? 클래스가 우선 한다고 했으니 Hello A가 출력될까?

 

public class D implements A{

}

public class C extends D implements A, B{

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

 

실제로 돌려 보면 여전히 Hello B가 출력된다. D가 클래스이긴 하지만 해당 디폴트 메소드를 실제로 "구현" 하지 않았기 때문이다. 만약 D가 아래와 같이 해당 메소드를 구현했다면 결과는 Hello D로 달라진다.

 

public class D implements A{

	public void hello() {
		System.out.println("Hello D");
	}
}

 

- 규칙에 의한 해결 불가

B를 아래와 같이 변경하면 규칙에 의해 해결이 안되어 오류가 발생한다.

 

public interface B {

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

 

오류내용: Duplicate default methods named hello with the parameters () and () are inherited from the types B and A

이럴때는 아래처럼 명시적으로 재정의하여 어떤 동작을 할 것인지 정의를 해야 한다.

 

public class C implements A, B{

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

	@Override
	public void hello() {
		A.super.hello();
	}
}

댓글