Why super.super.method() is Not Allowed in Java
1. 개요
Java에서 상속은 클래스 간의 기능을 재사용하고 확장하는 강력한 메커니즘입니다. super 키워드를 활용함으로써, 우리는 서브클래스에서 부모 클래스의 메서드와 속성에 접근할 수 있습니다.
하지만 Java에서 의외의 제한이 있습니다: super.super.method()는 허용되지 않습니다.
이 튜토리얼에서는 이 이유와 Java의 설계 철학이 이러한 동작을 어떻게 형성하는지에 대해 알아보겠습니다.
2. 문제에 대한 소개
일반적으로, 예제를 통해 문제를 이해해 봅시다. 세 개의 클래스가 있다고 가정해 봅시다:
class Person {
String sayHello() {
return "Person: How are you?";
}
}
class Child extends Person {
@Override
String sayHello() {
return "Child: How are you?";
}
String mySuperSayHello() {
return super.sayHello();
}
}
class Grandchild extends Child {
@Override
String sayHello() {
return "Grandchild: How are you?";
}
String childSayHello() {
return super.sayHello();
}
String personSayHello() {
return super.super.sayHello();
}
}
위 코드에서 세 개의 클래스는 다음과 같은 계층 구조를 가집니다:
[Person] <--- subtype --- [Child] <--- subtype --- [Grandchild]
모든 서브타입은 부모 클래스의 sayHello() 메서드를 오버라이드합니다.
특히 Grandchild는 두 개의 추가 메서드: childSayHello()와 personSayHello()를 가지고 있습니다. 이들의 구현은 간단하여 childSayHello()에서 super.sayHello()를 호출하고, personSayHello()에서는 super.super.sayHello()를 호출합니다.
하지만 Java는 super.super.sayHello()를 컴파일하지 않고 불평합니다:
java: <identifier> expected
그러면 다음으로, Java에서 super.super.method()를 호출하는 것이 왜 허용되지 않는지, 그리고 조부모 클래스의 메서드를 호출하는 방법을 알아봅시다.
3. Java에서 super.super.method() 호출이 허용되지 않는 이유
super.super.method() 사용을 금지하는 것은 의도적으로 설계된 것입니다. 다음으로, 그 이유를 알아봅시다.
3.1. 캡슐화 위반
Java는 캡슐화와 추상을 핵심 원칙으로 설계되었습니다. 이 원칙은 클래스가 직접 부모와만 관련을 가지고, 조부모나 그 이상의 구현 세부사항에 관심을 가져서는 안 된다고 규정합니다.
조부모의 메서드를 직접 호출하는 것은 상속 체인을 제어하는 부모의 역할을 무시하고, 부모가 의도적으로 숨기거나 수정했을 수 있는 내부 로직을 노출하게 됩니다.
다음으로, Java에서 super.super.method() 사용을 금지하는 이유를 설명하는 새로운 예제를 살펴보겠습니다.
3.2. Players 예제
우선, Player 클래스를 선언해 봅시다:
class Player {
private String name;
private String type;
private int rank;
public Player(String name, String type, int rank) {
this.name = name;
this.type = type;
this.rank = rank;
}
// ...getter and setter methods are omitted
}
name과 rank 속성은 간단하고, type 속성은 플레이어가 속한 분야를 나타냅니다. 예를 들면 “축구” 또는 “테니스” 등이 있습니다.
다음으로, Player 컬렉션 타입의 세트를 생성해 보겠습니다:
class Players {
private List<Player> players = new ArrayList<>();
protected boolean add(Player player) {
return players.add(player);
}
}
class FootballPlayers extends Players {
@Override
protected boolean add(Player player) {
if (player.getType().equalsIgnoreCase("football")) {
return super.add(player);
}
throw new IllegalArgumentException("Not a football player");
}
}
class TopFootballPlayers extends FootballPlayers {
@Override
protected boolean add(Player player) {
if (player.getRank() < 10) {
return super.add(player);
}
throw new IllegalArgumentException("Not a top player");
}
}
위 코드에서 세 개의 Player 컬렉션 클래스의 상속 계층은 다음과 같습니다:
[Players] <--- subtype --- [FootballPlayers] <--- subtype --- [TopFootballPlayers]
FootballPlayers와 TopFootballPlayers 모두 상위 클래스에서 add() 메서드를 오버라이드합니다. 이들은 각각의 add() 메서드에 대해 추가적인 검사를 추가하여 특정 타입의 Player 객체만 추가될 수 있도록 합니다. 예를 들어, TopFootballPlayers.add()는 플레이어의 등급이 10보다 높아야 합니다. 그러고 나서 TopFootballPlayers.add()는 super.add()를 호출하여 오직 최고 축구 선수만 추가될 수 있도록 보장합니다.
테스트를 통해 이 상속 구조가 어떻게 작동하는지 빠르게 확인할 수 있습니다:
Player liam = new Player("Liam", "football", 9);
Player eric = new Player("Eric", "football", 99);
Player kai = new Player("Kai", "tennis", 7);
TopFootballPlayers topFootballPlayers = new TopFootballPlayers();
assertTrue(topFootballPlayers.add(liam));
Exception exEric = assertThrows(IllegalArgumentException.class, () -> topFootballPlayers.add(eric));
assertEquals("Not a top player", exEric.getMessage());
Exception exKai = assertThrows(IllegalArgumentException.class, () -> topFootballPlayers.add(kai));
assertEquals("Not a football player", exKai.getMessage());
보시다시피, eric은 축구 선수이지만 그의 rank가 10보다 낮기 때문에, TopFootballPlayers.add()에서 추가가 거부됩니다.
마찬가지로 kai도 추가가 거부됩니다. 그는 테니스 선수이기 때문입니다, 비록 그의 rank가 7일지라도. 이 점은 TopFootballPlayers의 상위 클래스인 FootballPlayer의 add() 메서드를 사용하여 확인됩니다.
3.3. 만약 super.super.add()가 허용된다면
super.super.method() 호출이 허용된다고 가정해 봅시다. 그러면 TopFootballPlayers.add()를 다음과 같이 다시 작성할 수 있습니다:
class TopFootballPlayers extends FootballPlayers {
@Override
protected boolean add(Player player) {
if (player.getRank() < 10) {
return super.super.add(player); // 조부모의 add() 메서드를 직접 호출하는 것
}
throw new IllegalArgumentException("Not a top player");
}
}
그렇다면 rank < 10인 모든 Player 인스턴스를 추가할 수 있게 됩니다. 즉, TopFootballPlayers는 kai를 추가할 수 있게 됩니다, 비록 kai가 테니스 선수라 하더라도. 이렇게 되면 상위 클래스인 FootballPlayers의 불변성을 깨뜨리게 됩니다.
3.4. 다른 이유
Java가 super.super.method()를 허용하지 않는 다른 이유도 있습니다.
Java는 언어 설계에서 단순함과 명료함을 우선시합니다. super.super.method()를 허용하면 불필요한 복잡성이 도입되어 깊은 상속 계층을 동적으로 해결해야 합니다. 이러한 복잡성은 코드를 읽고 이해하기 어렵게 만들어, 특히 더 큰 코드베이스에서 더욱 그러합니다. 즉, Java는 접근을 즉각적인 부모만으로 제한함으로써 상속 체인이 명확하고 관리 가능하게 유지됩니다.
설계의 단순성 외에도 또 다른 이유는 “super”가 “super”를 가지지 않을 수 있기 때문입니다. 따라서 super.super가 존재하지 않는 경우가 있습니다.
우리는 모든 클래스가 Java에서 Object의 하위 클래스임을 알고 있습니다. 따라서, 우리가 생성한 모든 클래스는 유효한 “super”를 가지고 있습니다. 하지만 OurClass가 Object에서 직접 상속받는 경우, ourClass.super는 Object가 되며, ourClass.super.super는 존재하지 않습니다. 이로 인해 super.super가 유효하지 않게 됩니다.
그러나 경우에 따라 우리는 조부모 클래스의 메서드를 호출하고 싶을 수 있습니다. 다음으로 이를 달성하는 방법을 살펴보겠습니다.
4. 우회 방법: 간접 호출
Java 리플렉션은 거의 모든 것을 수행할 수 있을 만큼 강력합니다. 리플렉션을 사용하면 super.super.method()를 호출할 수 있습니다. 그러나 이러한 리플렉션 구현에 대해서는 이 튜토리얼에서는 다루지 않을 것입니다, 왜냐하면 그것도 여전히 캡슐화를 위반하기 때문입니다.
super.super.method()가 허용되지 않지만, Java는 원하는 기능을 달성할 수 있는 다른 메커니즘을 제공합니다. 예를 들어, 우리는 부모 클래스를 리팩토링하여 서브클래스가 조부모의 메서드에 접근할 필요가 있을 경우, 부모 클래스에서 이를 명시적으로 노출할 수 있습니다.
다음으로, 우리의 Person-Child-Grandchild를 사용하여 이것이 어떻게 이루어지는지 보여주겠습니다:
class Child extends Person {
// ...같은 코드 생략
String mySuperSayHello() {
return super.sayHello();
}
}
class Grandchild extends Child {
// ...같은 코드 생략
String personSayHello() {
return super.mySuperSayHello();
}
}
우리는 Grandchild에서 Person.sayHello()를 호출하고자 합니다. 따라서 코드에서 보는 바와 같이, 우리는 Child 클래스의 Child.mySuperSayHello()에서 Person.sayHello()를 노출할 수 있습니다:
Grandchild aGrandchild = new Grandchild();
assertEquals("Grandchild: How are you?", aGrandchild.sayHello());
assertEquals("Child: How are you?", aGrandchild.childSayHello());
assertEquals("Person: How are you?", aGrandchild.personSayHello());
이 접근 방식은 캡슐화를 위반하지 않으면서도 작업을 수행합니다.
5. 결론
이 글에서는 Java가 super.super.method()를 허용하지 않는 이유에 대해 논의했습니다. super.super.method()의 부재는 단순한 실수가 아니라 캡슐화, 추상화, 단순성의 원칙에 부합하는 의도적인 설계 선택입니다.
Java는 서브클래스가 즉각적인 부모와만 상호 작용하도록 제한하여 더 나은 코드 조직과 유지 관리를 유도합니다.
언제나처럼, 예제의 전체 소스 코드는 GitHub에서 확인할 수 있습니다.