본문 바로가기
[알아두면 좋을 것들]

상속에 대한 이해

by Hevton 2020. 12. 29.
반응형

글 끝까지 읽어야 함 ㅎㅎ.. 나의 일대기인데, 아래로 갈수록 잘 정리되어있음.

 

자바를 공부한 지 정말 오래 되었지만, 어쩌면 이제까지 '상속'의 내부 구조에 대해서 깊이 탐구해 본 적이 없던 것 같다.

 

이번 계기로 구조에 대해 조금 더 생각해보게 되었는데, 기존에 생각하고 있던 내용과 조금 다르긴 하지만 그렇다고 해서 개념이 흔들린다거나 그럴 필요는 없다. 사용도 잘만 했었다 ㅎㅎ..

 

 

일반적으로 Class B가 Class A를 상속하게 되었을 때, 이런 코드를 가질 수 있다.

class A {
    public void printR() {
        System.out.println("R");
    }
}
class B extends A { 
        
}

여태까지는 단순하게, 클래스 B가 A를 상속하게 되면 B { } 안에 보이지 않는 형태로 B 클래스의 자리에 A의 멤버들이 채워져 있다고 생각했다.

뭐랄까.. A의 printR() 정의문이 B { } 안에 B 클래스의 소유로 자리잡아 있지만 보이진 않는 형태?

 

말하자면 복사되어 있는 형태라고 봤다.

여태 생각하고 있던 구조

여태까지 잘만 코딩해왔듯이, 이렇게 생각해도 이용적으로 문제는 없었다. 단지 JVM 상에서의 내부적인 과정을 아예 몰랐던 것 뿐.

 

 

 

파이썬 & 루비 공부를 하면서 전체적인 객체지향 구조에 대해 다시 한번 알아보게 되었는데, 그 덕에 아래와 같은 구조로 상속을 조금 더 이해하게 되었다.

이해를 돕기 위한 그림일 뿐, 부모 객체의 모든 멤버가 자식 객체의 메모리 영역 내부에 존재하진 않는다. (그림의 목적은 '참조', '공유'의 의미다)

1번 처럼, 자식 클래스 안에 보이지 않는 형태로 부모 클래스의 멤버들이 모두 복사되어 있는 형식으로 간단히 생각하고 있었는데 조금은 다르다. 바로 위 그림도 명확한 그림은 아니다. 하지만 어느 정도는 맞다.

 

 

확실한 것은 자식 클래스가 부모 클래스를 상속하게 되면, 부모의 모든 멤버들을 '사용' 할 수 있게 가져와진다는 상속의 개념이다.

위 그림에서 Class B에서 int x를 정의하진 않았지만, B 객체의 x멤버필드로 접근하면 접근이 가능하다. 또한, Class B에서 helloA()를 정의하진 않았지만, B 객체의 helloA() 멤버메소드로 접근하면 접근이 가능하다. 이런 기능적 사실이 중요하다.

 

즉 위 구조에서 B 객체를 생성한 뒤 변수 x나 helloA() 대해 접근하게 되면, 객체 B에는 해당 멤버가 없는 것을 안 뒤에 상속한 부모 객체의 변수 x나 helloA()를 찾아 접근하게 되는 것이고, 만약 위 그림처럼 Class B에서 printA()를 오버라이드했다고 치면, 부모 객체까지 가기도 전에 자식객체에서 이미 찾았으니 오버라이드 된 함수를 사용할 수 있게 되는 것이다.

 

우선 개념적으로는 이러한데, 조금 더 내부적인 명확한 메모리 구조를 이해하려면 많은 노력이 필요할 것 같다. 하지만 처음 맨 위의 1번 그림으로 알고 있다가, 2번 그림으로의 약간의 전진을 하게 된 것에 감사해야겠다. 위의 개념적인 설명은 맞지만 2번 그림의 메모리 구조가 정확하지는 않다. 상속 후 객체 생성 시에 부모와 자식간의 멤버 필드와 멤버 메소드의 메모리 구조에 대해 차이가 있다고 한다.

 

Class B가 Class A를 상속하여 객체를 생성하게 되면, 기본적으로 부모인 A 객체가 먼저 생성된 뒤 자식객체인 B가 생성된다. 이 때 정확하게 들어가면 상속에 따른 멤버변수와 멤버메소드의 메모리 구조에 대한 약간의 차이가 존재하는 것 같다. (실제로 메모리 영역이 어떻게 자리잡느냐의 차이지 둘 다 개념적으로 사용은 동일하다는 것)

 

그 '차이'에 대한 읽을거리 조금----------------------------------------------------------------------------cafe.naver.com/javachobostudy?iframe_url_utf8=%2FArticleRead.nhn%253Fclubid%3D10286641%2526page%3D1%2526boardtype%3DL%2526articleid%3D171540%2526referrerAllArticles%3Dtrue

 

남궁성의 코드초보스터디(자바 java... : 네이버 카페

전문가가 지도하는 스터디카페에요. 프로그래밍언어(자바 java, C언어)를 제대로 배우고픈 분들 오세요.

cafe.naver.com

자식 인스턴스의 메모리를 할당 할 때 부모의 필드들이 들어갈 공간도 같이 만들어 집니다. (kochuns.blogspot.com/2015/09/blog-post.html )

인스턴스의 필드 멤버 메모리 영역만 겹치는 것일 뿐, 실제 메소드들이 관리되는 vtable과 각 클래스에 대한 description이 관리되는 방식은 또 다릅니다. 무엇보다 이러한 내용은 대충 그러하다- 정도로 이해하는 게 좋아요. JVM 구현에 따라서도 달라질 수 있는 내용이라.

 

정리하면, 인스턴의 각 "필드"만 자식과 조상의 데이터가 함께 있다. 위 링크처럼, 상속을 하면 멤버 필드(변수)들은 자식 객체 안에 자리잡는데, 메소드 들은 별개로 존재한다 이말씀.

클래스에는 멤버 필드만 있는건 아니니 자식 객체의 메모리 안에 부모 객체가 존재 한다고 말할 수 있을지는 의문.

 

 

정리하면 부모의 멤버필드는 복사되어 자식객체 메모리 영역에도 자리잡고(그래도 부모 명의로 자리잡으므로, 자식에서 똑같은 멤버필드를 정의하더라도 구분되는 것 같다), 부모의 멤버메소드는 자식객체 메모리와는 별개로 부모에게 존재하며 자식은 이 메모리영역을 참조하는 말 인 것 같다. (또는 메소드라는 영역이 애초부터 객체로부터 구분되는 영역에 존재하던가..?)

 

+ 상속하더라도 자식소유 부모소유가 분리되는 예제

public class mkaer {
    public static void main(String args[]) {

        child c = new child();

        c.parent_method();
        System.out.println(c.a);

    }
}

class parent {
    int a = 10;

    public void parent_method() {
        this.a = 20;
    }
}

class child extends parent {

    int a = 33;
}

우선 이렇게 정의도 가능하며, 자식 객체 c에서 parent_method()를 호출한다. 자식 객체 자체에도 parent_method가 존재한다면 this.a 는 자식을 가리킬텐데, 그게 아니라 부모로 올라가므로, 부모에서의 this는 부모 자신이다.

출력 결과는 20이 아니라 33이다. parent_method()는 자식의 a변수에 접근할 수 없으므로.

 

 

'메소드 같은 경우엔, 자식 클래스 B의 객체가 있을 때 hello() 라는 함수를 호출하면, 컴퓨터는 자식 클래스의 정의문을 먼저 찾는다.

그리고 해당 자식 클래스가 가진 메소드 중에 hello() 메소드가 존재하는지 확인하고, 없으면 부모 클래스가 갖고 있는 메소드 중에 hello() 메소드가 존재하는지  다시 찾아본다. 그리고 자식 클래스 B의 객체가 있을 때, 부모 클래스의 a라는 변수는 자식 클래스의 인스턴스가 생성될 때 부모 명의로 이미 자식 영역에 메모리가 할당된다..' 는 정도? 만약 자식클래스에서 a 변수가 정의되지 않았을 경우 부모 명의의 변수를 찾게 되는 건 메소드 방식과 동일(메소드랑 변수는 메모리적으로 어떻게 할당 되느냐의 차이 뿐 기능면에서는 같은 작용)

 

더 명확히 이해하려면 더 깊은 공부가 필요하다 ㅜ..ㅜ 현재 JVM의 기본 메모리구조들에 대해서 조차도 명확히 모르고 있는 상황에서 이를 이해하려고 접근하다 보면, 내가 알고 있는 것만으로는 이해가 안되는 구조들 뿐일테니 현재까지의 개념적인 이해마저 흔들릴 수 있다.관점과 추상단계에 따라 달라지는 문제를 여러 관점을 섞어서 맞다 틀리다고 정의하는 순간 다 틀린 답이 될 수 밖에 없다.
A 다음 B인데, A도 모르는데 B를 이해하려고 하다가 큰일난다.

-----------------------------------------------------------------------------------------------------

 

하지만 1번 그림이나 2번 그림이나,

우선 언제나 달라지지 않는 점은 '상속' 이라는 의미에 맞게, 부모를 상속한 자식은 부모의 멤버들을 갖게 된다고 개념적으로 생각 하는 거엔 다름없다.

 

 

즉, 여기까지 정리하면

상속을 하게 되면 1번처럼 부모 멤버가 자식 멤버로 그저 복사되어 자식 객체가 그 자체로 모든 멤버를 갖고 있는 형태라기 보다는

2번 처럼 부모 객체를 생성한 뒤 자식 객체가 부모객체를 참조하여, 부모객체의 멤버들에 접근할 수 있는 관계로 보는 것이 조금 더 정답에 가깝다. (자식소유의 멤버변수나 메소드를 찾아봤는데, 없으면 부모로 올라가고 없으면 또 부모로 올라가는 그런 과정 정도로 이해하면 된다.)

 

근데 이렇게 열심히 파고들다 보니까, 오히려 더 힘들어진 것 같다...

단지 개념적으로 편하게 이해하려면, 그냥 여태 그래왔듯 1번처럼 모든 멤버가 복사되어서 갖게 된다고 생각하는 것도 나쁘지 않은 것 같다.

복사되어 내부적으로 갖고 있고, 재정의 하게 되면 같은 것의 중복이 아니라 덮어씌워진다는 생각으로. 그리고 super을 쓰면 원본을 가져오는 것 뿐이고.

실제로 나처럼 복붙으로 이해하는 사람들도 많은 것 같다.

 

[ 관점과 추상단계에 따라 달라지는 문제를 여러 관점을 섞어서 맞다 틀리다고 정의하는 순간 다 틀린 답이 될 수 밖에 없다. ]

 

+ 2021.01.08 추가 > 자세히 알기 전 까진, 여태 그래왔듯 예전처럼 '복사' 개념으로 이해하는게 편한 것 같다.. 메모리 구조에 대해 명확히 잘 알지도 못하면서 참조 개념으로 생각하니 이해되지 않는 개념들이 생긴다. 알면알수록 어려워지기도 한다 ..

그리고 위에서 객체 면으로 상속을 이해할 수 있는데, 정확히는 '객체' 가 상속되는 게 아니라, '클래스' 가 상속되는 것이고 이를 되새기면 이전까지의 복사의 개념이 잘 자리잡히는데 도움이 된다.

 

 

이에 대한 사람들의 다양한 의견이 있다.

정확한 구조에는 못 미칠 수 있으나 개념적으로 이해하는 데에는 도움이 될 것.

 

stellan.tistory.com/entry/Java-상속

nathanh.tistory.com/121

chanhuiseok.github.io/posts/java-1/

www.tcpschool.com/java/java_inheritance_concept

prathnapatilblog.wordpress.com/java-inheritance/

velog.io/@foeverna/Java34상속Inheritance

mainpower4309.tistory.com/8

stackoverflow.com/questions/26998680/difference-between-finish-and-super-finish-in-java

 

Difference between finish() and super.finish() in Java?

What are the main differences between using finish() and super.finish() in Java? When can/should you use one over the other?

stackoverflow.com

 

+ 추가 이해 코드

public class mkaer {
    public static void main(String args[]) {

        child c = new child();

        c.hello();
        System.out.println(c.a);

        c.parent_print();
        c.child_print();
    }
}

class parent {
    int a = 10;

    public void parent_method() {
        this.a = 20;
    }

    public void parent_print() {
        System.out.println(this.a);
    }
}

class child extends parent {

    public void hello() {
        this.a = 30;
    }

    public void child_print() {
        System.out.println(super.a);
    }
}

출력결과

30

30

30

 

c.hello() => 자식 명의의 a가 없으므로 부모 명의의 a 사용해서 30으로 변경

c.a => 자식 명의의 변수 a가 없으므로 부모 명의의 a변수값 30에 접근해 출력

c.parent_print => 부모 명의의 변수 a의 값 30을 출력

c.child_print = > 부모 명의의 변수 a의 값 30을 출력

 

public class mkaer {
    public static void main(String args[]) {

        child c = new child();

        c.hello();
        System.out.println(c.a);

        c.parent_print();
        c.child_print();
    }
}

class parent {
    int a = 10;

    public void parent_method() {
        this.a = 20;
    }

    public void parent_print() {
        System.out.println(this.a);
    }
}

class child extends parent {
    int a = 99; // 추가
    public void hello() {
        this.a = 30;
    }

    public void child_print() {
        System.out.println(super.a);
    }
}

출력결과

30

10

10

 

자식 메모리 영역 스택에는 자식 명의 변수 a, 부모 명의 변수 a가 존재하게 된다.

 

c.hello() => 자식 명의의 a가 있으므로 자식 명의의 a 사용해서 30으로 변경

c.a => 자식 명의의 변수 a가 있으므로 자식 명의의 a변수값 30에 접근해 출력

c.parent_print => 부모 명의의 변수 a의 값 10을 출력

c.child_print = > 부모 명의의 변수 a의 값 10을 출력

 

public class mkaer {
    public static void main(String args[]) {

        child c = new child();

        c.child_print();
    }
}

class parent {
    int a = 10;

}

class child extends parent {

    public void child_print() {
        System.out.println(super.a); // this.a도 동일.
    }
}

출력결과 10

 

 

+ 추가 내용 & 윤성우 저자님의 참고자료

위 복사, 참조의 대한 개념정리는 맞는 내용도 있지만 틀린 부분도 있다. 틀린 부분은, A->B 상속을 할 때 A 객체를 만들면 B 객체가 생기고 A 객체가 B 객체를 참조하는 게 아니라, B 속성을 가진 A 객체만 생긴다.

 

자식 객체를 생성하면, 부모의 생성자가 먼저 호출되고 자식의 생성자가 호출되는 것은 맞다.

이를 통해 '부모 객체가 생성된 뒤 자식 객체가 생성된다' 라는 표현을 쓰기도 하는데, 정확히는 자식 인스턴스만 생성되는데, 그 안의 부모 멤버들은 부모 생성자로 초기화하고, 자식 멤버들은 자식 생성자로 초기화하는 것 같다. (위의 JAVA정석 카페글에서도 이에 대해 여쭤봤었는데, 정확히는 객체가 생성된다기보다는 멤버가 초기화되는 거라고 해주셨던 것이 있다. 또한 어떤 분께서도, 상속해서 인스턴스를 만들면 각 부모 클래스에 대한 인스턴스가 생성되는게 아니라, 부모 클래스의 속성과 메서드를 갖는 자식 클래스의 인스턴스가 생성된다고 해주셨다.

A->B->C 상속해서 A에 대한 인스턴스를 만들면, A B C인스턴스가 각각 생성되는게 아니라 A인스턴스만 만들어진다.)

+ 물론 정확히 따지고 보면, 당연히 자식 생성자가 먼저 호출되겠는데, 그 안에서 부모 생성자의 초기화 작업을 먼저 마치고 자식 생성자의 초기화 작업을 마치게 된다.

 

예전에 공부했던 윤성우 저자님의 '나는 정말 자바를 공부한 적이 없다고요' 를 다시 공부했는데, 거기서의 상속 설명은 아래와 같다.

Man을 상속하는 BusnessMan 클래스의 인스턴스를 생성하게 되면,

이렇게 부모에서의 모든 멤버를 포함한 인스턴스를 갖게되나, '부모 멤버' 와 '자식 멤버' 라는 이름은 구분되어 있다고 생각하면 될 것 같다.

그리고 내가 위에서 생각했던 방식을 여기 껴넣으면, BusinessMan의 인스턴스를 생성하면, Man의 생성자가 먼저 호출이 되어서 Man의 멤버들을 초기화하고 BusinessMan의 생성자가 호출되어서 BusinessMan의 멤버들이 초기화되는 것.

 

참고로 나와있는 BusinessMan의 멤버, Man의 멤버라는 표시는 단지 잠시동안의 설명을 위한 것이 아닌, 그런 의미를 계속 갖고 있다. (윤성우 저자님께서도 그렇게 계속 설명해주신다. 상위 클래스의 멤버를 초기화해야한다, 상속 관계기 때문에 상위 클래스의 멤버에 접근이 가능하다 등등 이런식으로. 둘은 내부적으로는 구별되어있다고 명시해주신다. (상속으로 묶여있기 때문에 접근 가능)) 이렇게 영상의 많은 설명을 통해 그런 구분은 명백히 있다는 것을 알 수 있었다.

 

이렇게 생각하면, '복사' 와 '참조'의 개념 두 마리 토끼를 모두 잡을 수도 있겠다.

자식과 부모에서의 같은 이름의 변수가 있더라도, 실질적인 멤버의 소유는 다르므로 중복되어 처리되진 않는 것이고. 오버라이딩도 마찬가지. 또한 BusinessMan에서 찾을 수 없는 멤버면, Man의 멤버에서 찾게된다는 것.

 

 

즉, 하위 클래스의 인스턴스를 생성하면 하위 클래스의 생성자가 호출되게 되는데, 그 안에서 제일 먼저 상위 클래스의 생성자를 호출하고 실행하여 상위 클래스에 존재하는 멤버들을 모두 초기화한 뒤, 하위 클래스의 생성자를 통해 하위 클래스에 존재하는 멤버들을 초기화 마치게 되면 상위 클래스와 하위 클래스의 멤버를 모두 초기화한 '하위 클래스의 인스턴스' 가 생성이 된다.

 

 

그리고 윤성우 저자님의 강의를 보면, 기존에 '생성자는 상속되지 않는다' 라는 점에서의 의아함이 생긴다.

부모 생성자도 자식 인스턴스 안에 넣은 채로 설명해주시기 때문.

 

그런데 이후 강의 내용에서는 '생성자는 상속되지 않는다' 라고 말씀해주신다.

 

생각을 좀 해봤는데, '생성자는 상속되지 않는다' 의 개념을 아래 차이를 통해 받아들여야겠다

다른 함수들처럼, 자식에서 부모의 생성자를 이름으로 직접 호출하는 것은 불가능하다. (함수 이름으로) = 상속되진 않는 것.

하지만 super을 이용한 생성자 실행은 가능하다는 점에서 상속되지 않는다는 것 같다.

=> 내 생각이 맞는 듯!!!!! (참고 - cafe.naver.com/javachobostudy?iframe_url_utf8=%2FArticleRead.nhn%253Fclubid%3D10286641%2526articleid%3D171782%2526commentFocus%3Dtrue ) 

- 남궁성님이 다른 답변을 해주시긴 했는데, 해석 관점이 다른거겠지만 혹여 틀렸다 해도 어차피 생성자 상속 표현인 그 부분만 정확하지 않은 표현일 뿐. 나머지는 맞다. 그리고 또 말씀해주신 super와 super()는 다르다..에서 어느정도 어떤 관점인지 느낌도 온다. 관점의 해석 차이일 뿐 어쨌든간에 두 경우에서 말하려는 것은, 생성자는 좀 별개라는 거라고 받아들이면 될 것 같다.

public class mkaer {
    public static void main(String args[]) {

        child c = new child(40);

        c.child_print();
    }
}

class parent {
    int a = 10;

     parent(int b) {
        a = b;
    }

}

class child extends parent {

     child(int b) {
        super(b); // parent(b);는 에러
    }

    public void child_print() {
        System.out.println(super.a);
    }
}

(+ 이게 이해가 안된다면, 실제 메모리영역에서 멤버필드와 달리 메소드는 위에말했듯이 인스턴스 내부 메모리가 아닌 vtable이라는 영역에서 관리한다는 점에서, 거기서도 내부적으로 누구 명의인지도 나열되어 있겠고, 클래스에서 함수를 찾을 때 그 클래스에서 사용가능한 함수 목록을 찾는 그런식인 것 처럼, super를 통해 부모 참조변수에 접근하여 존재하는 부모 명의의 생성자에 접근할 뿐이므로 직접 호출할 순 없는거고 상속은 되지 않는다고 봐도 되겠다)

 

결론 : 윤성우님이 최고다.. 나랑 비슷한사람인것같아.

 

출처 - www.orentec.co.kr/teachlist/JAVA_BASIC_1/teach_sub1.php

 

====== 오렌지 미디어 ======

             목록 서버1   서버2 강의시간 강의교안  Chapter 01. Let's Start JAVA!        01-1. 자바의 세계로 오신 여러분을 환영합니다. 32:59        01-2. 자바 프로그램의 이해와 실행의 원리

www.orentec.co.kr

상속부분 보면 이해 바로 끝..

 

 

정리

클래스 A가 클래스 B를 상속한다. 클래스B에 대한 b 객체를 생성하면, A의 멤버를 갖고 있는 b 객체가 생성된다.

(이론적으로 A의 생성자가 먼저 초기화 완료되고, B의 생성자가 초기화 완료되면서 각자의 클래스의 생성자들은 각자의 클래스의 멤버들을 초기화한다) 이 때, 메모리적으로 b 객체 안에는 A의 멤버필드들이 존재한다(각자의 명의로). 그리고 메소드들은 사실 인스턴스 영역 안의 메모리에 존재하지는 않고 vtable이라는 메모리 영역에서 관리된다.

b 객체에서 멤버변수나 멤버메소드를 호출할 때 먼저 자신의 명의부터 찾고, 없으면 부모의 명의를 찾는 식으로 올라간다.

(메소드를 예를 들면, 객체의 메소드를 호출하면 그 '클래스'의 메소드 중에 해당 메소드가 있는지 확인하고, 없으면 부모것을 찾는 셈. 멤버필드도 이러한 방식)

 

생성자는 상속이 되지 않는다. 하지만 언어레벨에서 허용해준 장치이므로 하위 클래스가 상위 클래스의 생성자를 super()를 통해 사용할 수 있다. 생성자는 유별난 별도의 케이스라고 봐야 한다. (대신 이런 이유로 직접 함수이름으로 호출할 수 없다는 점은 알아야 한다. 자식 객체에서 직접 노출되진 않지만 사용할 수 있는 구조...)

 

윤성우 님의 강의에서의 개념으로 이해하면 된다. '상속' 을 통해 상위 - 하위 클래스의 멤버들의 개념적으로 묶여있는 상태인 것.

생성자나 메소드는 실제로 윤성우 저자님의 강의에서처럼 인스턴스 영역에 존재하진 않겠지만, '개념적'으로 멤버들은 이처럼 모두 묶여있다고 보면 된다.

 

결국 '상속' 시에 개념적으로 모두 묶여있다는 점에서, 멤버필드나 메소드나 생성자나 접근이 가능하다고 이해하고 사용하면 되지 실제 메모리 구조까지 들여다볼 필요는 없다.

 

 

 

참고

m.blog.naver.com/PostView.nhn?blogId=heartflow89&logNo=220961980579&proxyReferer=https:%2F%2Fwww.google.com%2F

 

cafe.naver.com/javachobostudy/171773

반응형