클래스에 대한 얘기를 하면 빠지지 않고 등장하는 것이 바로 상속이다.


이 상속 관계는 객체지향 프로그래밍에서 무척 중요하다.


객체지향 프로그래밍은 절차형 프로그래밍의 한계점에 의해 탄생한 프로그래밍 기법이다.

객체를 통해서 현 실세계를 표현하는 프로그래밍으로 현재 대부분 모든 프로그래밍 기법이다.


물론 객체지향 프로그래밍이라고 해서 모든 것을 표현하진 못한다고 한다. 특히 최근에 객체지향 프로그래밍의 한계점으로

새로운 프로그래밍 기법이 연구되고 있고 속속 등장하고 있다고는 하지만 이 얘기는 여기서 마무리하고 다시 본 이야기로

돌아가서 이런 객체지향 프로그래밍에서 클래스란 모두가 알다시피 어떤 실체의 틀을 말한다. 그리고 그 실체의 틀에 

필요한 정보를 넣어서 완성시킨 형상이 바로 객체다.


하지만 이 객체들은 진화한다. 현실 세계에도 계속 새로운 물건이 등장하고 그 물건을 개선하여 다시 새롭게 탄생한다.

이러한 현실 세계를 나타내기 위하여 컴퓨터로 나타내는 객체지향 세계에서도 진화라는 개념이 등장해야 했다.


그래서 나온 것이 상속이다.


글이 길어졌다. 이제부턴 짧게 작성해야겠다.


상속은 기존 것을 받아서 프로그래머 입맛대로 바꾸는 것을 말한다.

슈퍼 주전자를 만들고 싶다. 이 때, 슈퍼 주전자를 완전히 무에서 만들어내는 것보다 기존의 주전자에서 만들어내는게

훨씬 더 비용적으로 편하고 좋다. 그렇기에 기존 주전자를 받아서 바꾸고 싶거나 버리거나 추가할 부분을 추가하여 변형한다.


만약 주전자의 뚜껑을 열 때, 기존의 주전자는 사람의 손으로 직접 열지만 슈퍼 주전자는 스위치를 통해 연다고 하자.

이렇게 새롭게 변형이 일어나는 부분을 프로그래밍 코드로 표현하는 방법을 '오버라이딩'이라고 한다.


한 마디로 부모 클래스 메서드를 자식이 필요에 맞게 조정하는 것이다.


단!! 반환 타입, 이름, 매개변수 타입과 개수까지 모두 같아야 한다.

오버라이딩과 오버로딩은 엄연히 구분되어 있다.


아래 코드의 Move()가 바로 오버라이딩 대상이다.



만약 위의 클래스에서 다음과 같은 코드를 수행한다면 그 결과는 어떻게 될까?


다음은 위 코드의 결과화면이다.


정상적으로 상속되어서 오버라이딩된 것을 볼 수 있다. 하지만 !!!


하지만 이 방법은 큰 문제가 있다.

단순히 저 객체만을 사용하려 한다면 발생되지 않을 문제이지만, Shape 클래스에서 상속받은 자식 클래스들이 둘 이상일

경우에 발생하는 문제로 다음과 같은 함수를 호출하는데 발생한다.

이 함수는 Shape 클래스 타입을 파라메터로 받기에 Shape 클래스를 상속받은 모든 자식 클래스는 이 함수에 접근 가능하다. 

하지만, 

다음과 같은 코드를 보자.

이 코드의 결과는 무엇일까?

Triangle 객체를 생성해서 넣었기에 출력 결과는 Triange Move()가 나올 것 같지만, 결과는 다음과 같다.


실제 객체는 Triangle이지만 함수의 파라미터 타입으로 인해 함수는 Triangle의 부모 클래스인 Shape 객체로 인식하게 된다.


그럼 어떻게하면 Shape 타입이지만 Triange 객체에 의도대로 접근할 수 있을까?

바로 클래스 Upcasting을 활용하면 된다.


Upcasting을 활용하면 Shape 클래스를 상속받은 모든 클래스들은 이 함수를 사용할 수 있는 인터페이스 함수로 바꿀 수 있다.

그 방법은 Triangle 객체를 다음과 같이 생성한다.


다음은 위 코드의 출력 결과이다.


???

이게 무슨일이람? 

무언가 이상하다.


첫 번째로 Shape Move()가 아닌 Triangle Move()가 출력됐어야 했고, 그리고 부모 클래스인 Shape의 소멸자가 수행되기 전에

Triangle의 소멸자가 수행되어야 했는데 그러지 않는다. 이와같은 현상으로 인해 자식 클래스인 Triangle 객체가 해제되지

않고 남아있기에 메모리 누수 현상까지 발생한다.


바로 이 부분이 이번 포스팅의 핵심이다. 잘 기억하자. 또 그렇게 벌벌 거리지 않게.


 

위 문제는 만약 언어가 Java라면 큰 문제가 없겠지만, 언어가 C++이기에 발생하는 문제점이다.

가상테이블과 객체간의 연결 관계에 대해 깊은 이해를 위한다면 정확히 찾아보는 것이 좋다. 여긴 그냥 관념적인 내용이다.


컴파일 과정에서 현재 생성된 객체의 메서드가 부모 클래스의 메서드인지 자식 클래스의 메서드인지 판단하게 되는데,

만약 Java라면 모든 연결을 동적 바인딩 방식으로 진행하기에 사용자의 의도대로 Triangle 객체를 생성하고 Move() 메서드를

수행하면 그 결과는 "Triangle Move()" 결과나 나온다.


하지만 C++언어는 virtual 키워드를 통해 객체와 메서드간의 연결을 동적 바인딩을 명시해주지 않으면 위의 코드에서는

Triangle 객체를 생성했지만 실제로 Move() 메서드의 결과는 "Shape Move()"이다. 

그 이유는 virtual 키워드를 입력하지 않았기 때문에 컴파일러가 부모와 자식의 오버라이딩 관계를 정적으로 바인딩했기

때문에 발생한다. 그래서 동적 생성인 new 연산자로 생성했다고 해서 동적 바인딩이 이루어지지 않고, 이미 정적 바인딩이

진행되었기에 함수 호출 시 원하는 출력이 나타나지 않는다.


만약 정상적인 출력을 원한다면 virtual 키워드를 부모 클래스의 오버라이딩 함수 앞에 넣어주면 된다.


수정된 클래스이다.

수정된 클래스의 출력 화면이다.


의도대로 Shape Move()가 아닌 Triangle Move()가 출력되었다.

하지만 여전히 Triangle 객체가 해제되진 않고 있어 메모리 누수 현상이 계속된다.

이를 어떻게 해결해야할까?


정답은 똑같이 부모의 소멸자 앞에 virtual 키워드를 삽입하면된다.

그러면 소멸자도 똑같이 동적 바인딩이 이루어지면서 원하는 의도대로 프로그램이 진행된다.


다음은 최종적으로 수정된 클래스이다.

위 코드의 출력 결과이다.


정상적으로 함수 실행이 완료되었고, 객체의 소멸까지 메모리 누수 현상없이 잘 해결되었다.


이처럼 Upcasting을 활용하면 파라미터로 부모 클래스 타입을 가지지만, 자식 객체들이 접근하여 그 함수를 활용할 수 있다.


말이 어려우니까 한 마디로,

만약 Upcasting을 하면 객체지향의 다형성을 제대로 활용할 수 있다. 아래의 코드를 보면 명확히 이해 가능하다.


마지막 코드

출력


play() 함수를 각각의 객체 타입에 맞게 파라미터를 오버로딩 할 필요없이, 상속 관계라면 하나의 함수로 접근 가능하다.

이것이 Upcasting을 사용하는 가장 대표적인 이유다.


아주 미약한 이유로는 Upcasting된 코드를 보면, 상속 관계를 한 눈에 알아볼 수 있어 가독성에 아~~~~주 조금 도움된다.


그리고 Downcasting이란 Upcasting의 반대의 의미이며 절대로 생성할 때 사용하면 안된다. 

애초에 visual studio라면 오류를 내보낸다.

왜냐하면 다음과 같은 코드가 있다.


Triangle* t = new Shape();


기본적으로 Triangle 클래스가 Shape 클래스의 자식 클래스이기에 필요로 하는 정보량이 훨씬 많기에 그만큼 생성해야 한다.

하지만 Shape() 객체로 생성하면 Triangle 클래스가 필요로 하는 정보량을 모두 생성할 수 없다.

그렇기에 Downcasting은 생성할 때, 쓸 수 없다.


단지 Upcating한 객체를 원래대로 되돌릴 때 사용할 뿐이다.


마지막으로 이와같은 Upcasting, Downcasting의 존재는 편리하지만 잘못사용하면 정보의 손실이 발생하는 위험한 방법이다.

그렇기에 이를 보완하기 위한 Dynamic Casting이 존재하지만, 이에 대한 내용은 다음에 알아볼 것.



다시는 이 내용몰라서 당황하지 않게 정말 평생의 미련이 될 뻔했다. 바보야.

솔직하지 않았더라면.



+ Recent posts