상속과 모호성

코코딩딩·2022년 4월 11일
0

JAVA Basic

목록 보기
4/7
post-thumbnail

개요

자바의 interface를 공부 중 흥미로운 부분을 발견하였습니다.
java 7까지는 interface의 method는 method body를 가질 수 없었습니다.
즉, 구현부가 없어 로직을 가질 수 없었죠.
그러나 java 8 부터는 default method가 추가되어 interface가 구현부를 가진 method를 멤버로 가질 수 있습니다.
이에 대해서 재미난 실험이 생각나서 직접 코드로 작성해보았습니다.

목표

🎯 interface의 default method가 추가된 이유를 알아봅니다
🎯 다중상속으로 인한 모호성에 대해서 알아봅니다
🎯 interface를 상속할 경우 발생하는 모호성을 살펴봅니다.
🎯 다중상속으로 인한 모호성을 java에서는 어떻게 해결하는지 알아봅니다.

Default Method 뭐죠?

Default method는 java 8에서부터 추가된 기능입니다.
java 7까지 interface는 추상메소드 및 상수만 멤버로 가질 수 있었습니다.
하지만 java 8에서는 static method와 default method도 멤버로 가질 수 있게 되었습니다.
default method가 추가된 주된 이유는 바로 하위호환성 때문입니다.
Oracle Docmentation을 살펴보겠습니다.

...
Suppose that you want to add new functionality to the TimeClient interface, such as the ability to specify a time zone through a ZonedDateTime object (which is like a LocalDateTime object except that it stores time zone information)
...
Following this modification to the TimeClient interface, you would also have to modify the class SimpleTimeClient and implement the method getZonedDateTime. However, rather than leaving getZonedDateTime as abstract (as in the previous example), you can instead define a default implementation. (Remember that an abstract method is a method declared without an implementation.)
...

위 내용에 대해서 예를 들자면 이러합니다.

SuperType이라는 interface와 이 인터페이스의 구현체 SubTypeA~C이 있다고 가정해봅시다.
유지보수를 하다가 새로운 구현체인 SubTypeD을 만들고 여기에 새로운 메소드인 method3를 추가해야하는 상황이 왔습니다.

이 때 새로운 기능을 포함한 SubTypeD을 구현하기 위해서는 SubTypeA~C를 모두 수정해야합니다.
SuperType에 새롭게 method3 추가해야하기 때문입니다.
안타깝게도 java 7까지는 모두 수정해야했습니다만, java 8 이후부터는 default method를 통해서 이 문제를 해결할 수 있습니다.

위와 같이 interface에 defalt method를 추가하면 모든 구현체들은 이 default method를 상속할 수 있고 별도의 수정이 필요 없게 됩니다.

어느 메소드를 호출하는 겁니까?!

java는 클래스간의 다중상속을 지원하지 않습니다.
두 클래스를 상속받을 때 두 클래스에 같은 signature를 가지는 method가 있다면 어느 상속받은 객체가 어떤 메소드를 호출할지 모호해집니다. 이를 다이아몬드 문제라 합니다.
java는 이러한 문제 때문에 interface를 통해서 다중상속을 지원합니다.
interface의 추상메소드는 구현체에서 재정의되어야 합니다.
그렇기에 구현체에서 어느 메소드에서 상속받던 해당 구현체에서 구현된 내용의 method를 호출합니다.
그런데 default method는 method body를 가지고 있으며, 구현체에서 재정의하지 않아도 됩니다.
그러면... class에서 상속받는 method 이름과 inteface에서 상속받는 default method 이름이 같으면 무슨일이 일어날까요?

코드로 살펴보는 모호성

다중상속으로 인한 모호성에 대해서 코드로 살펴보겠습니다.

case1 : class - interface

public class SuperClassA {
    public void defaultMethod(){
        System.out.println("부모클래스에서 호출");
    }
}

public interface InterfaceA {
    default void defaultMethod(){
        System.out.println("인터페이스에서 호출");
    }
}

public class SubClassA extends SuperClassA implements InterfaceA{
}

위 코드를 보면 interface InterfaceA와 class SuperClassA는 defaultMethod라는 method를 가지고 있고, 이를 SubClassA가 상속받고 있습니다.
만약 main method에서 SubClassA의 객체를 생성하고 defaultMethod를 호출하면 콘솔에 어떤 결과가 나타날까요?

public class inheritApp {
    public static void main(String[] args) {
        SuperClassA superA = new SubClassA();
        superA.defaultMethod();
        InterfaceA interfaceA = new SubClassA();
        interfaceA.defaultMethod(); 
        SubClassA subClassA = new SubClassA();
        subClassA.defaultMethod();
    }
}

/**
부모클래스에서 호출
부모클래스에서 호출
부모클래스에서 호출
**/

다형성을 적용하더라도 결국 SuperClassA의 defaultMethod만 호출하는군요.
재미있는 점은 intelliJ는 어떤 메소드를 바라보는지 헷갈려하는 것 같습니다.
아래 이미지를 보면 실제 동작결과와는 다르게 interface의 method라고 toolBox가 뜨는군요...

case2 : abstract class - interface

이번엔 abstract class와 interface가 같은 메소드를 가지고 있으면 어떻 출력결과가 나오게 될까요?

public abstract class AbsClassB {
    public void defaultMethod(){
        System.out.println("추상클래스에서 호출");
    }
}

public interface InterfaceA {
    default void defaultMethod(){
        System.out.println("인터페이스에서 호출");
    }
}

public class SubClassB extends AbsClassB implements InterfaceA{
}

public class inheritApp {
    public static void main(String[] args) {
        AbsClassB absB = new SubClassB();
        absB.defaultMethod();
        InterfaceA interfaceA = new SubClassB();
        interfaceA.defaultMethod();
        SubClassB subClassA = new SubClassB();
        subClassA.defaultMethod();
    }
}
/**
추상클래스에서 호출
추상클래스에서 호출
추상클래스에서 호출
**/

콘솔에서 abstract class의 defaultMethod가 호출된 것을 확인할 수 있습니다.

public class SuperClassA {
    public void defaultMethod(){
        System.out.println("부모클래스에서 호출");
    }
}

public interface InterfaceA {
    default void defaultMethod(){
        System.out.println("인터페이스에서 호출");
    }
}

public interface InterfaceB {
    default void defaultMethod(){
        System.out.println("인터페이스B에서 호출");
    }
}


public class SubClassC extends SuperClassA implements InterfaceA, InterfaceB{
}

또한 class(혹은 abstract class)와 여러 interface를 상속하면서 이 interface들이 같은 메소드를 가지고 있을 때도 동일하게 나타납니다.

case3 : 여러 인터페이스 상속

하지만 여러 인터페이스만 상속받을 때는 다른 경우가 발생합니다.

public interface InterfaceA {
    default void defaultMethod(){
        System.out.println("인터페이스에서 호출");
    }
}

public interface InterfaceB {
    default void defaultMethod(){
        System.out.println("인터페이스B에서 호출");
    }
}


public class SubClassC implements InterfaceA, InterfaceB{
}
/**
컴파일 에러 
: SubClassC inherits unrelated defaults for defaultMethod() 
from types InterfaceA and InterfaceB
**/

이때는 defaultMethod를 재정의 해줘야지만 에러가 사라집니다.
여러 interface를 상속할 때 나타나는 모호성을 없애기 위해서 overriding을 강제하고 있습니다.

상속에 대한 우선순위가 있었슴다..

java 언어의 디자이너는 왜 이렇게 만든건지는 정확히 알 수 없으나 위 코드로 몇 가지를 규칙을 확인해볼 수 있습니다.

method 상속 시 class(or abstract class)가 interface보다 우선순위가 높다.
여러 interface로부터 동일한 default method 상속 시 구현체에서 overriding해야한다.

java에서는 이러한 규칙을 통해서 상속에서 나타나는 모호성을 제거하고 있었습니다.

결론

호기심으로 시작한 실험(?!)이었는데 나름 재미있었습니다.
물론 상속 시 의도적으로 위와같은 상황을 만들진 않겠지만, 어떤식으로 동작하는지 직접 눈으로 봐본 것은 처음이었습니다.

📖 참조
Oracle Java documentation - Default Method
WIKIPEDIA - Muliple Inheritance

0개의 댓글