개발/기술

JAVA - 다중상속을 허용하지 않는 이유는 뭘까?

kwony 2023. 2. 24. 12:59

최근 면접에서 "Java가 다중상속을 허용하지 않는 이유는 뭘까요?" 라는 질문을 받았다. 상속은 오직 클래스 하나만 가능하다는 문법만 알았지 그 이유에 대해서 깊게 고민해보지는 않았다. "인터페이스는 다중으로 구현할 수 있는데, 상속만 이렇게 막은 이유가 있을까요?" 라는 추가질문이 이어지자 제대로 답변도 못하고 '정말 그렇네... 왜그렇게 만들었을까...?' 라는 생각이 면접을 마치고 집에 와서도 머릿속을 멤돌았다.


java class 는 암시적으로 object 클래스를 상속하고 있다. 대부분의 클래스에서 기본으로 포함된 함수 toString(), hashCode(), equals() 가 object 로부터 상속받은 함수들이다. 클래스 상속 관계에선 위 함수들은 어떻게 처리될까?

 

public class A {
    @Override
    public String toString() {
        return "A";
    }
}

public class B extends A {}

 

클래스 A는 toString() 을 오버라이드 했고 클래스 B는 클래스 A를 상속 받았다. 상속이 발생하면 자연스럽게 자식 클래스는 부모 함수들을 호출하게 된다. 그래서 클래스 B에는 별도의 오버라이드 함수가 없으므로 객체 b는 클래스 A에 선언된 toString() 함수를 실행한다.

 

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world!");
        B b = new B();
        System.out.println("b.toString() " + b.toString());
    }
}

--------------------------------------------------------------
b.toString() A

 

그런데 아래 코드처럼 다중 상속이 가능하다면 문제가 생긴다. 클래스 B는 A와 A2중에서 어떤 클래스의 함수의 toString()을 가져와야할까? 결정할 수 없다. 여기서 메소드 충돌이 발생한다. 자바 컴파일러는 이런 상황을 막고자 다중 상속을 제한한다.

 

public class A2 {
    @Override
    public String toString() {
        return "A2";
    }
}

public class B extends A, A2 {

}

 

그런데 메소드가 원인이라면, 인터페이스는? 인터페이스도 함수 이름이 겹칠 수 있지만 다중으로 구현할 수 있도록 만들지 않았나? 맞다. 이름이 겹치는 함수를 갖고 있는 인터페이스를 클래스가 구현할 수 있다. 예로 이런 코드가 가능하다.

 

public interface A3 {
    String printHappy();
}
public interface A4 {
    String printHappy();
}
public class B implements A3, A4 {
    @Override
    public String printHappy() {
        return "happy";
    }
}

 

이때는 컴파일 오류도 발생하지 않고 정상적으로 실행된다. 클래스의 입장에선 A3, A4가 구현을 요구한 함수 printHappy() 함수를 구현했을 뿐이다. 상속처럼 어떤 곳을 참조해야하는지 결정해야하는 문제는 발생하지 않는다. 이런 방식은 실제 운영에선 문제가 되겠지만 문법상으로는 문제가 없다.

 

그런데 최근에 추가된 default 함수를 사용하면 동작이 다르다. 먼저 아래 코드는 컴파일이 될까?

 

public interface A3 {
    default String printHappy() {
        return "A3 happy";
    }
}
public interface A4 {
    default String printHappy() {
        return "A4 happy";
    }
}
public class B implements A3, A4 { }

 

컴파일 오류가 발생한다. 클래스 B는 A3와 A4가 구현한 printHappy() 함수중 어떤 것을 선택해야할지 결정 할 수 없다. 다중 상속때와 같은 메서드 충돌이 여기서도 발생한다. 그럼 해결 방법은? 클래스 B 내에서 printHappy() 함수를 오버라이드해 자체적으로 메서드 충돌을 해결해야한다. IDE에서 제시하는 문제해결 방식도 동일하다

 

default가 도입되면서 super의 모습이 달라진게 보인다. 예전의 super는 부모 클래스를 호출하는 것이었는데 앞에 인터페이스의 이름을 붙이면 default 함수를 바라볼 수 있게 됐다.

 

public class B implements A3, A4 {
    @Override
    public String printHappy() {
        return A3.super.printHappy() + A4.super.printHappy();
    }
}

 

그러면 부모 클래스는 코드로 어떻게 표현할까? 테스트로 추상 클래스 A2를 상속받도록 했다. 그리고 클래스 B에서 super 객체에서 printHappy 를 호출 결과를 확인했다.

 

public abstract class A2 {
    String printHappy() {
        return "A2 happy";
    }
}

public class B extends A2 implements A3, A4 {
    @Override
    public String printHappy() {
        return super.printHappy() + A3.super.printHappy() + A4.super.printHappy();
    }
}

------------------------------------------------
A2 happy A3 happy A4 happy

 

super.printHappy() 에선 부모클래스, A2의 함수가 출력됐다. 접두사로 아무것도 붙이 않으면 super 는 부모 클래스를 바라본다. 만약 상속 받은게 없다면 컴파일 에러가 발생한다.

 

 

Why is there no multiple inheritance in Java, but implementing multiple interfaces is allowed?

Java doesn't allow multiple inheritance, but it allows implementing multiple interfaces. Why?

stackoverflow.com