본문 바로가기

개발

[Java] 인터페이스만 다중 상속이 가능한 이유?

 


 

추가사항

( Q.질문 그렇다면 여러 인터페이스들로부터 상속받은 하나의 클래스가 같은 이름의 메서드들을 오버라이딩할 때 클래스는 어떤 인터페이스로부터 상속받은 메서드인지 어떻게 알 수 있을까? JVM이 알아서 판단하니 개발자가 알 필요가 없는 것일까?)

JVM도, 개발자도 어떤 인터페이스의 메서드를 구현한 것인지 알 필요가 없습니다. 그래서 구분할 수도 없습니다. 인터페이스는 행동만을 표현하고, 구현체를 제공하지 않기 때문입니다. 해당 행동을 구현한 클래스의 메서드 구현체에 집중이 되어야 하는 것이지, 그 행동이 어디로부터 왔는지는 중요하지 않습니다. 그 덕분에 두 인터페이스를 구현한 클래스에서는 해당 메서드에 대해 다이아몬드 문제가 발생하지 않게 되는 것이죠.

 

그렇다면 이런 질문이 생길 수 있습니다. 만약, 해당 클래스에서 각각의 인터페이스를 타입으로 두고 해당 메서드를 호출하고 싶은 상황에서는 어떻게 해야 하나구요.

 

public class InterfaceTest {
    interface Gift  { void present(); }
    interface Guest { void present(); }

    interface Presentable extends Gift, Guest { }

    public static void main(String[] args) {
        Presentable johnny = new Presentable() {
            @Override public void present() {
                System.out.println("Heeeereee's Johnny!!!");
            }
        };
        johnny.present();                     // "Heeeereee's Johnny!!!"

        ((Gift) johnny).present();            // "Heeeereee's Johnny!!!"
        ((Guest) johnny).present();           // "Heeeereee's Johnny!!!"

        Gift johnnyAsGift = (Gift) johnny;
        johnnyAsGift.present();               // "Heeeereee's Johnny!!!"

        Guest johnnyAsGuest = (Guest) johnny;
        johnnyAsGuest.present();              // "Heeeereee's Johnny!!!"
    }
}

 

위의 코드를 보면, Gift와 Guest를 타입으로 두어 각각 호출하는 것 역시 가능합니다. 그 이유는 두 인터페이스의 present() 메서드가 동일한 method signature를 가졌기 때문이죠. 그래서 둘은 @Override-equivalent 하기 때문에 문제가 없습니다.
(https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.4.8.4)

 

 

메서드 시그니처(Method signature)

메서드 시그니처(Method signature)란? Java에서 메서드 시그니처는 메서드의 정의에서 메서드 이름과 매개변수 리스트의 조합을 말한다. 메서드 이름과 매개변수 리스트가 중요한 이유는 오버로딩

wanna-b.tistory.com

 

그럼, 서로 다른 두 인터페이스에서 return type만 다르고 똑같은 이름의 메서드를 하나의 클래스에서 구현하고자 한다면 무슨 일이 벌어질까요?

 

public class InterfaceTest {
    interface Gift  { void present(); }
    interface Guest { boolean present(); }

    interface Presentable extends Gift, Guest { } // DOES NOT COMPILE!!!
    // "types InterfaceTest.Guest and InterfaceTest.Gift are incompatible;
    //  both define present(), but with unrelated return types"
}

 

이 경우에는 서로 다른 method signature를 가졌기 때문에 @Override-equivalen 하지 않아서 문제가 생깁니다. 컴파일 타임시 에러가 발생하게 되죠. 다만, 이건 복잡하게 생각하지 않아도 됩니다. 인터페이스를 통하지 않아도, 원래 return type이 다르고, 이름이 같은 메서드를 구현해보면 컴파일 오류가 뜨니까요.

(https://stackoverflow.com/questions/19959371/calling-same-method-name-from-two-different-interface-java)

 

인터페이스에서 다이아몬드 문제가 발생하는 경우 - 1

그러나 Java 8 이후로는 인터페이스에서도 다이아몬드 문제가 발생하기 시작했습니다. 그 이유는 default method 때문이에요.

 

Default method란?

default 메서드는 기존에 존재하던 인터페이스에 새로운 기능을 추가함과 동시에 하위 호환성 유지를 위해 만들어졌습니다. default 메서드 설명을 위해 한 가지 상황을 예시로 들어볼게요.

우리에게는 TimeClient 인터페이스가 있습니다.

 

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
}

 

그리고 이를 구현하는 SimpleTimeClient 클래스가 있었습니다.

 

public class SimpleTimeClient implements TimeClient {
    public void setTime(int hour, int minute, int second) {
        LocalDate currentDate = LocalDate.from(dateAndTime);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(currentDate, timeToSet);
    }

    public void setDate(int day, int month, int year) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime currentTime = LocalTime.from(dateAndTime);
        dateAndTime = LocalDateTime.of(dateToSet, currentTime);
    }

    public void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime timeToSet = LocalTime.of(hour, minute, second); 
        dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
    }

    public LocalDateTime getLocalDateTime() {
        return dateAndTime;
    }
}

 

어느 날, 우리는 TimeClient 인터페이스에 한 가지 기능을 추가할 필요성을 느꼈습니다. 그래서 getZonedDateTime() 라는 메서드를 추가하기로 했습니다.

 

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();                           
    ZonedDateTime getZonedDateTime(String zoneString); // 새롭게 추가하고 싶은 기능
}

 

그러나 위와 같은 방식을 사용한다면, 우리는 SimpleTimeClient 클래스를 수정하고, 그 안에서 getZonedDateTime() 메서드를 구현해야 합니다.

 

그러나 아래와 같이 default 메서드로 직접 구현을 한다면, 우리는 SimpleTimeClient 뿐만이 아니라, TimeClient를 구현하는 모든 클래스를 수정할 필요가 없습니다. 그저 단순히 가져와서 호출하기만 하면 되기 때문이죠.

 

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();

    static ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString + "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    default ZonedDateTime getZonedDateTime(String zoneString) { // 새롭게 추가한 default 메서드
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }
}

 

(https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html)

 

인터페이스에서 다이아몬드 문제가 발생하는 경우 - 2

그런데 만약, 같은 이름의 default 메서드를 가진 여러 개의 인터페이스를 하나의 클래스에서 구현하고자 한다면 무슨 일이 벌어질까요?

 

public interface I1 {
    default String getGreeting() {
        return "Good Morning!";
    }
}

public interface I2 {
    default String getGreeting() {
        return "Good Afternoon!";
    }
}


public class C1 implements I1, I2 {
    public static void main(String[] args) {
        System.out.println(new C1().getGreeting());
    }
}

 

이를 실행하면, 컴파일 에러가 발생합니다. 그 이유는 둘 중 어느 인터페이스의 getGreeting()를 호출해야 할지 모호해서 알 수 없기 때문입니다.

 

클래스의 다중상속 불가

이는 Java가 클래스의 다중상속을 막은 이유와 일맥상통합니다. 다이아몬드 문제가 발생하기 때문입니다. 더군다나 이를 무시하고 클래스의 다중상속을 문법적으로 허용할 경우, Java의 핵심인 객체지향 SOLID 원칙이 무너지게 됩니다.

  • Ex 1) SOLID에서의 S는 Single Responsibility Principle(SRP)로 단일 책임 원칙을 뜻합니다. 클래스는 오직 하나의 기능을 갖고, 그 책임에만 집중해야 합니다.
  • Ex 2) 또한, L : Liskov Subsititution Principle(LSP), 리스코프 치환 원칙에 의해 하위 클래스는 상위 클래스의 메서드를 모두 사용할 수 있어야 한다.

(https://gyubgyub.tistory.com/102)

 

인터페이스의 다이아몬드 문제 해결

다시 인터페이스로 돌아와서, 그럼 어떻게 하면 모호함을 풀어줄 수 있을까요? 바로 구현 클래스에서 해당 메서드를 @Override 함으로써 문제를 해결할 수 있습니다.

 

public class C1 implements I1, I2 {
    public static void main(String[] args) {
        System.out.println(new C1().getGreeting());
    }

    @Override
    public String getGreeting() {
        return "Good Evening!";
    }
}

 

위와 같은 경우에는 최우선적으로 클래스 내에 오버라이딩된 메서드를 호출하기 때문입니다.

그 외에도 아래와 같이 중복되는 메서드에 인터페이스를 명시적으로 위임함으로써 문제를 해결할 수 있습니다.

 

public class C1 implements I1, I2 {
    public static void main(String[] args) {
        System.out.println(new C1().getGreeting());
    }

    @Override
    public String getGreeting() {
        return I1.super.getGreeting();
    }
}

 

(https://webfirewood.tistory.com/130)
(https://stackoverflow.com/questions/22685930/implementing-two-interfaces-with-two-default-methods-of-the-same-signature-in-ja)

 


 

본 내용은 JavaChip 스터디를 통해 다룬 내용입니다.
https://github.com/Java-Chip4/StudyingRecord/issues/6