[Java] 상속에서의 메소드 호출 C++과 비교해 이해하기

자바 상속관계의 메소드 호출 이해하기

자바를 공부하며 단번에 내부 동작을 이해할 수 없었던 내용과 평소 내부 동작을 이해하고 있던 C++을 기반으로 비교하며 상속관계에서의 메소드 호출 방식을 정리하였습니다!
부족한 부분에 대한 지적은 감사히 받겠습니다!

이펙티브 자바를 공부하다가 왜 이렇게 동작하는지 이해가 안가는 코드가 있었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.java.effective.chapter4.item18;

public class ParentClass {
public void methodA() {
System.out.println("This is Parent A Method");
}

public void methodB() {
System.out.println("This is Parent B Method and I am Calling Method A");
methodA();
}

public void whoAmI() {
System.out.println("I am instance Of A");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.java.effective.chapter4.item18;

public class ChildClass extends ParentClass{
@Override
public void methodA() {
System.out.println("This is Child A Method and I am Calling Parents Method A");
super.methodA();
}

@Override
public void methodB() {
System.out.println("This is Child B Method and I am Calling Parents Method B");
super.methodB();
}

@Override
public void whoAmI() {
System.out.println("I am instance Of B");
}
}
1
2
ChildClass c = new ChildClass();
c.methodB();

내가 예상한 동작은 다음과 같았다.

1
2
3
This is Child B Method and I am Calling Parents Method B
This is Parent B Method and I am Calling Method A
This is Parent A Method

하지만 결과는 다음과 같았다.

1
2
3
4
This is Child B Method and I am Calling Parents Method B
This is Parent B Method and I am Calling Method A
This is Child A Method and I am Calling Parents Method A
This is Parent A Method

조금이라도 더 익숙한 C++로 다음과 같은 코드를 작성해서 테스트를 해 보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Parent {
public:
void methodA() {
std::cout<<"This is Parent A Method"<<std::endl;
}

void methodB() {
std::cout<<"This is Parent B Method and I am Calling Method A"<<std::endl;
methodA();
}

void whoAmI() {
std::cout<<"I am instance Of A"<<std::endl;
}
};

class Child : public Parent {
public:
void methodA() {
std::cout<<"This is Child A Method and I am Calling Parents Method A"<<std::endl;
Parent::methodA();
}

void methodB() {
std::cout<<"This is Child B Method and I am Calling Parents Method B"<<std::endl;
Parent::methodB();
}

void whoAmI() {
std::cout<<"I am instance Of B"<<std::endl;
}
};

결과는 내 예상과 같았다.

1
2
3
This is Child B Method and I am Calling Parents Method B
This is Parent B Method and I am Calling Method A
This is Parent A Method

왜 이런일이 일어났을까? 아무래도 자바의 오버라이딩은 C++의 virtual 키워드를 포함하고 있다는 결론을 내릴 수 밖에 없었다. 즉 부모의 methodB를 호출했더라도 methodB에서 호출하는것은 methodA이고 현재 인스턴스는 Child의 인스턴스이기 때문에 Child의 methodA가 호출이 된다.

조금 더 알아보니 다음과 같은 내용을 찾을 수 있었다.

In Java, all non-static methods are by default “virtual functions.” Only methods marked with the keyword final, which cannot be overridden, along with private methods, which are not inherited, are non-virtual.

그럼 C++은 왜그럴까? 답은 간단하다. 함수가 virtual이 아니기 때문이다. virtual로 지정된 클래스는 virtual table이 존재하고 런타임에 어느 함수를 실행할지 결정을 할 수 있다. 그렇지만 위와 같은 경우는 함수가 virtual 키워드를 포함하지 않기 때문에 부모의 methodB에서 methodA를 호출하는 경우 참조할 함수는 부모의 methodA밖에 존재하지 않는다. 쉽게 말해 virtual table에서 어떤 함수를 호출해야 할지 확인할 방법도 없는 상태에서 parent의 methodB가 알고있는 methodA는 parent의 methodA밖에 없기 때문에 parent의 methodA를 호출하게 되는것이다.

그럼 이를 자바와 같이 바꾸려면 어떻게 해야할까? 이것도 간단하다. Parent를 virtual class로 만들어주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Parent {
public:
virtual void methodA() {
std::cout<<"This is Parent A Method"<<std::endl;
}

virtual void methodB() {
std::cout<<"This is Parent B Method and I am Calling Method A"<<std::endl;
methodA();
}

virtual void whoAmI() {
std::cout<<"I am instance Of A"<<std::endl;
}
};

class Child : public Parent {
public:
void methodA() override {
std::cout<<"This is Child A Method and I am Calling Parents Method A"<<std::endl;
Parent::methodA();
}

void methodB() override {
std::cout<<"This is Child B Method and I am Calling Parents Method B"<<std::endl;
Parent::methodB();
}

void whoAmI() override {
std::cout<<"I am instance Of B"<<std::endl;
}
};
1
2
3
4
This is Child B Method and I am Calling Parents Method B
This is Parent B Method and I am Calling Method A
This is Child A Method and I am Calling Parents Method A
This is Parent A Method

그럼 자바에서 다음과 같은 출력을 만들려면 어떻게 해야할까?

1
2
3
This is Child B Method and I am Calling Parents Method B
This is Parent B Method and I am Calling Method A
This is Parent A Method

사실 이는 근본적으로 dynamic dispatching 때문에 불가능하다. 그렇기 때문에 이를 우회하는 여러 방법들을 사용해야한다.

방법1.
오버라이딩을 포기한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ParentClass {
private void methodA() {
System.out.println("This is Parent A Method");
}

public void methodB() {
System.out.println("This is Parent B Method and I am Calling Method A");
methodA();
}
}

public class ChildClass extends ParentClass{
public void methodA() {
System.out.println("This is Child A Method and I am Calling Parents Method A");
}

@Override
public void methodB() {
System.out.println("This is Child B Method and I am Calling Parents Method B");
super.methodB();
}
}

방법 2. final로 선언한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ParentClass {
final void methodA() {
System.out.println("This is Parent A Method");
}

public void methodB() {
System.out.println("This is Parent B Method and I am Calling Method A");
methodA();
}
}

public class ChildClass extends ParentClass{
@Override
public void methodB() {
System.out.println("This is Child B Method and I am Calling Parents Method B");
super.methodB();
}
}

방법 3. 꼼수를 부린다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ParentClass {
public void methodA() {
interMethodA();
}

private void interMethodA() {
System.out.println("This is Parent A Method");
}

public void methodB() {
System.out.println("This is Parent B Method and I am Calling Method A");
interMethodA();
}
}

public class ChildClass extends ParentClass{
@Override
public void methodA() {
System.out.println("This is Child A Method and I am Calling Parents Method A");
super.methodA();
}

@Override
public void methodB() {
System.out.println("This is Child B Method and I am Calling Parents Method B");
super.methodB();
}
}

방법 4. is-A가 아닌 has-A로 만든다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ParentClass {
public void methodA() {
System.out.println("This is Parent A Method");
}

public void methodB() {
System.out.println("This is Parent B Method and I am Calling Method A");
methodA();
}
}

public class ChildClass {
private ParentClass parentClass = new ParentClass();

public void methodA() {
System.out.println("This is Child A Method and I am Calling Parents Method A");
parentClass.methodA();
}

public void methodB() {
System.out.println("This is Child B Method and I am Calling Parents Method B");
parentClass.methodB();
}
}

오늘도 나의 무지함에 반성하며 열심히 공부해야겠다..

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/08/Languages/Java/Inheritance/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.