얼마전에 자바를 공부하다가 상속 파트에서 이상한 현상을 봤다.
일단 코드는 다음과 같다
public class ApptEx {
private static class Appt{
@Override
public String toString() {
return "Appt";
}
}
private static class ApptCalendar{
private final static int MAX_APPT = 1000;
private Appt[] appts;
private int size;
public ApptCalendar() {
this.appts = new Appt[MAX_APPT];
size = 0;
}
public void addAppt(Appt appt) {
if (size == appts.length) {
return;
}
System.out.println("부모꺼");
appts[size++] = appt;
}
public void addAppts(Appt[] appts) {
for (int i = 0; i < appts.length; i++) {
addAppt(appts[i]);
}
}
}
private static class LoggingApptCalendar extends ApptCalendar {
@Override
public void addAppt(Appt appt) {
System.out.println("자식꺼");
}
@Override
public void addAppts(Appt[] appts) {
for (int i = 0; i < appts.length; i++) {
System.out.println(appts[i].toString());
}
super.addAppts(appts);
}
}
static void main() {
LoggingApptCalendar loggingApptCalendar = new LoggingApptCalendar();
loggingApptCalendar.addAppts(new Appt[] {new Appt(), new Appt(), new Appt()});
}
}
ApptCalendar는 슈퍼 클래스이고, 이를 상속받은 LogginApptCalendar 서브 클래스가 있다. main 메소드에서는 서브 클래스를 인스턴스화하여서 만들었고, 서브 클래스에서 오버라이딩된 addAppts를 호출하였다. 당연히 addAppts에서는 super.addAppts로 슈퍼 클래스의 addAppts를 호출한다.
여기서 문제인데, 슈퍼 클래스의 addAppts는 addAppt를 다시 호출한다. 나는 당연히 슈퍼 클래스의 addAppt를 호출할줄 알았는데, 동작을 보니까 서브 클래스에서 오버라이딩된 addAppt를 호출하고 있었다.
그래서 this를 한번 붙여서 해봤다.
public void addAppts(Appt[] appts) {
for (int i = 0; i < appts.length; i++) {
this.addAppt(appts[i]);
}
}
그래도 동작은 똑같이 자식의 메소드를 호출하고 있었다!!
이유가 무엇일까 찾아보는데, this는 인스턴스가 자신의 멤버를 호출할수 있도록 사용되는 변수고, 이 안에는 인스턴스에 대한 참조가 있다고 한다. 그리고 이 this 변수는 자바 컴파일러가 알아서 메소드 파라미터의 가장 좌측에 넣어준다고 한다.
이미 main 메소드에서 loggingApptCalendar.addAppts(new Appt[] {new Appt(), new Appt(), new Appt()}) 를 호출했을 때, 이미 가장 좌측에 LoggingApptCalendar 인스턴스에 대한 참조 변수가 들어갔다는 의미이다.
그래서 슈퍼 클래스의 addAppts에서 this.addAppt를 호출해봤자, 이미 this는 LoggingApptCalendar 인스턴스에 대한 참조를 갖고 있기 때문에 서브 클래스의 appAppt를 호출하게 된 것이었다.
그렇다면 위와 같은 상황에서 슈퍼 클래스의 메소드를 호출하는건 아예 안되는걸까?
나는 리플렉션을 써보기로 했다.
public void addAppts(Appt[] appts) {
for (int i = 0; i < appts.length; i++) {
// this.addAppt(appts[i]); // this를 써도 안되네?
try {
Method addAppt = ApptCalendar.class.getDeclaredMethod("addAppt", Appt.class);
addAppt.invoke(new ApptCalendar(),(Object) appts[i]);
} catch (Exception e) {
System.err.println("예외터짐");
}
}
}
뱔로 어려운 코드는 아니다. ApptCalendat에서 addAppt 메소드에 대한 정보를 리플렉션으로 가져오고, invoke를 통해 실행시키는 것이다. 여기서 invoke의 인자는 해당 메소드를 실행시켜줄 인스턴스와, addAppt 메소드의 파라미터로 넘겨줄 정보이다.
내 예측대로라면 이전에는 this가 서브 클래스의 인스턴스라서 생긴 문제였으니, 이번에는 리플렉션으로 슈퍼 클래스의 인스턴스 정보를 넘겨줬으니까 슈퍼 클래스의 addAppt가 호출되어야 한다.
결과는...?
Appt
Appt
Appt
부모꺼
부모꺼
부모꺼
과연 예측대로 나왔다 !!
그러면 리플렉션으로 안쓰고 new를 통해 슈퍼 클래스의 인스턴스를 만들고 해도 똑같겠네?
public void addAppts(Appt[] appts) {
for (int i = 0; i < appts.length; i++) {
// this.addAppt(appts[i]); // this를 써도 안되네?
new ApptCalendar().addAppt(appts[i]);
// try {
// Method addAppt = ApptCalendar.class.getDeclaredMethod("addAppt", Appt.class);
// Method logAppt = LoggingApptCalendar.class.getDeclaredMethod("addAppt", Appt.class);
// addAppt.invoke(new ApptCalendar(),(Object) appts[i]);
// } catch (Exception e) {
// System.err.println("예외터짐");
// }
}
}
이렇게 해도 결과는 똑같이 나왔다. 하지만 이게... 과연 잘 만들어진 설계일까?
만약 슈퍼 클래스의 메소드를 위와 같은 상황에서 쓸 때는 그냥 슈퍼 클래스의 메소드에 final 키워드를 달고 상속되지 않도록 하는 편이 좋겠다.
'Java' 카테고리의 다른 글
| 클래스 로더가 로딩 + 링크 + 초기화를 모두 한다는 오해 (0) | 2026.05.31 |
|---|---|
| JVM 튜?닝? (1) | 2025.11.27 |
| 자바 가비지 컬렉터 (GC) (0) | 2025.09.07 |
| JVM (0) | 2025.09.06 |
| SOLID 원칙 (0) | 2025.09.05 |