반응형

자바 8에서 추가된 Default Method에 대해서 간단하게 정리해보겠습니다.

이전에는 기존의 인터페이스에 기능을 추가하려면,
인터페이스에 추상 메소드를 추가한 뒤, 구현 클래스에 오버라이딩하여 작성해야만 했습니다.
인터페이스를 직접 제어할 수 있다면 문제 없이 기능을 확장할 수 있지만,
배포된 형태로 이미 다른 클라이언트들이 이용하고 있다면, 새로운 추상 메소드를 직접 구현하기 위해 클래스 코드를 수정해줘야하는 상황입니다.

이러한 문제를 해결하기 위한 방편으로 default method가 나오게 되었고,
인터페이스에 default 키워드로 구현이 포함된 메소드를 정의하면,
인터페이스를 구현한 클래스는 이 메소드를 오버라이딩하지 않아도 구현을 포함하게 됩니다.

public interface A {
    default void foo() {
        System.out.println("A.foo");
    }
}

public class B implements A {
    public static void main(String[] args) {
        new B().foo(); // A.foo
    }
}

클래스 B에서 A를 구현하지않았는데도, B 인스턴스는 foo()를 호출할 수 있습니다.

이 기능을 사용해서 이미 사용되던 인터페이스에 자유롭게 새로운 메소드를 추가할 수 있게 되었고,
기존 클래스를 수정하지 않아도 이전과 동일하게 사용하면서 새로운 기능 또한 가지게 되었습니다.
이전 버전과 호환성을 유지하면서 API를 바꿀 수 있게 된 것입니다.

default method로 인해 추가된 대표적인 API에는 List 인터페이스의 sort가 있습니다.

List<Integer> list = Arrays.asList(3, 1, 2);
Collections.sort(list);
System.out.println("list = " + list);

기존에는 위처럼 Collections 클래스에 정의된 스태틱 메소드 sort를 이용해서 정렬을 해야했습니다.

List<Integer> list = Arrays.asList(3, 1, 2);
list.sort(naturalOrder());
System.out.println("list = " + list);

하지만 List에 sort라는 default method가 생기고나선, list에서 바로 정렬을 호출할 수 있습니다.

그런데 하나 의문을 가질 점은, 인터페이스 다중 상속을 통해 구현 클래스는 여러 개의 타입을 가질 수 있었습니다.
이제는 단순히 타입만 상속받는 것이 아니라, 하나의 클래스가 여러 인터페이스를 다중 상속 받으면서 default method에 의해서 구현된 여러 기능 또한 상속받을 수 있게 된 것입니다.

하지만 다중 상속이 가능하게 되면서, 동일한 메소드 시그니처 사이에서 어떤 것을 선택해야할지 모호하게 되었습니다.
C++과 같이 다중 상속을 지원하는 언어에서 있던 다이아몬드 상속 문제에서 발생하는 상황이 생기게 된 것입니다.
여러 개의 인터페이스나 다른 클래스에 동일한 시그니처를 가진 메소드가 포함되어 있다면, 어떤 메소드를 구현할지 자바 컴파일러는 다음과 같은 규칙에 의해서 결정하게 됩니다.
.
1. 클래스가 항상 우선권을 가진다. 클래스나 슈퍼클래스에서 정의한 메소드가 디폴트 메소드보다 우선권을 가진다.
2. 이 외의 상황에서는 항상 서브인터페이스가 우선권을 가진다. 상속 관계를 갖는 두 개의 인터페이스에서 같은 시그니처를 갖는 메소드를 정의한다면, 서브 인터페이스가 이긴다. 즉, B가 A를 상속한다면, B가 우선권을 가진다.
3. 1번과 2번 규칙에 의해서 디폴트 메소드에 우선순위가 결정되지 않았다면, 컴파일 에러가 발생한다. 따라서, 여러 인터페이스를 상속받는 구현 클래스가 명시적으로 디폴트 메소드를 오버라이딩 해야한다.

몇 가지 예제를 살펴보겠습니다.
interface A : default method foo()
interface B extends A : default method foo()
class C implements A, B 가 있을 때,
C에서 foo()를 호출하면 A와 B 중에 어디에 있는 메소드가 호출될까요?

public interface A {
    default void foo() {
        System.out.println("A.foo");
    }
}

public interface B extends A {
    default void foo() {
        System.out.println("B.foo");
    }
}

public class C implements A, B {
    public static void main(String[] args) {
        new C().foo(); // B.foo
    }
}

위에서 말한 2번 규칙에 의해서 서브 인터페이스 B가 이기게 되고, B.foo가 출력됩니다.


그렇다면, 다음과 같은 경우는 어떨까요?
interface A : default method foo()
interface B extends A : default method foo()
class D extends A
class C extends D implements B, A

public interface A {
    default void foo() {
        System.out.println("A.foo");
    }
}

public interface B extends A {
    default void foo() {
        System.out.println("B.foo");
    }
}

public class D implements A {

}

public class C extends D implements A, B {
    public static void main(String[] args) {
        new C().foo(); // B.foo
    }
}

D는 A의 foo() 구현을 그대로 상속받습니다.
따라서 컴파일러는 인터페이스 A와 B에 구현된 메소드 둘 중 하나를 선택해야합니다.
2번 규칙에 의해서 B가 A를 상속받는 관계이므로 서브인터페이스인 B의 foo()가 수행됩니다.

이번에는 바로 위 예제와 동일한 상황에서 D 클래스가 A의 foo() 메소드를 오버라이딩해보겠습니다.
class D implements A : foo()

public class D implements A {
    @Override
    public void foo() {
    	System.out.println("D.foo");
    }
}

1번 규칙에 의해서 슈퍼클래스 D에 정의된 메소드가 우선권을 가지게 됩니다.
따라서, D.foo가 출력됩니다.


두 개의 독립적인 인터페이스에서 동일한 시그니처의 메소드가 충돌되는 상황이면 어떻게 될까요?
interface A : default method foo()
interface B : default method foo()
class C implements A, B

public interface A {
    default void foo() {
        System.out.println("A.foo");
    }
}

public interface B {
    default void foo() {
        System.out.println("B.foo");
    }
}

public class C implements A, B {
    public static void main(String[] args) {
        new C().foo();
    }
}

다음과 같은 에러가 발생하게 됩니다.

이번에는 3번 규칙에 의해서 명시적으로 오버라이딩을 해줘야하는 것입니다.

public class C implements A, B {
    @Override
    public void foo() {
        System.out.println("C.foo");
    }
    
    public static void main(String[] args) {
        new C().foo(); // C.foo
    }
}

C에서 foo()를 오버라이딩하고, 정상적으로 컴파일되어 C.foo가 출력됩니다.

또는, 다음과 같이 super 키워드를 사용해서 A와 B 중에 호출할 메소드를 명시할 수도 있습니다.

public class C implements A, B {
    @Override
    public void foo() {
        A.super.foo();
    }
    
    public static void main(String[] args) {
        new C().foo(); // A.foo
    }
}

A.foo가 출력되는 것을 확인할 수 있습니다.


이번에는 다이아몬드 구조를 살펴보겠습니다.
interface A : default method foo()
interface B extends A
interface C extedns A
class D implements B, C

public interface A {
    default void foo() {
        System.out.println("A.foo");
    }
}

public interface B extends A{

}

public interface C extends A{

}

public class D implements B, C {
    public static void main(String[] args) {
        new D().foo(); // A.foo
    }
}

위와 같은 경우에서는, D가 B와 C를 상속하고있더라도,
A에만 default method가 정의되어있으니 A.foo가 출력됩니다.

그렇다면, 방금과 동일한 상황에서 B가 foo를 오버라이딩하면 어떻게 될까요?

public interface B extends A {
    @Override
    default void foo() {
        System.out.println("B.foo");
    }
}

2번 규칙에 의해서 서브 인터페이스가 우선권을 가지게 되므로, B.foo가 출력됩니다.

마지막으로, 위처럼 B에서 foo를 오버라이딩한 상황에 C에서도 foo를 오버라이딩한다면?

public interface B extends A {
    @Override
    default void foo() {
        System.out.println("B.foo");
    }
}

public interface C extends A {
    @Override
    default void foo() {
        System.out.println("C.foo");
    }
}

이번에는 B와 C의 foo() 중에서 우선권을 결정하지못하므로 컴파일 에러가 발생하게 됩니다.
따라서, D에서 오버라이딩하여 어떤 메소드를 호출할지 명시하거나 재정의해줘야합니다.

이렇게 세 가지 규칙과 몇가지 예시를 통해서 default method에 우선순위까지 살펴보았습니다.

참고자료 : 모던자바인액션

반응형

+ Recent posts