inheritance와 polymorphism

Challenge and Frustration·2021년 9월 28일
0

첫번째 게시글로 C++ 클래스 개념부터 시작하고 싶은데 시간이 없는 관계로 현재 공부하고 있는 inheritance와 polymorphism부터 시작한다.


inheritance

우리 말로는 '상속'이다. 상속이란 가진 것을 물려준다는 것을 의미한다.
상속은 C++에서 클래스와 클래스 간에 이뤄진다.
부모 역할을 하는 클래스는 super class, 자식 역할을 하는 클래스를 sub class라고 부른다.

그렇다면 클래스는 다른 클래스에게 뭘 물려줄까??
바로 클래스의 property와 behavior이다. 모든 멤버가 상속된다는 것이다. sub class는 super class의 멤버 변수, 멤버 함수 모두 물려받아서 가진다.

상속을 왜 할까?

  1. 코드 재사용
    어떤 클래스가 다른 클래스의 멤버를 그대로 가질때 코드를 다시 타이핑하는 번거로움을 피할 목적으로 상속을 사용한다.
    헌데 상속을 하게 되면 두 클래스가 서로 강하게 coupling 된다.
    즉, 하나의 변경이 다른 하나에 강하게 영향을 미쳐 캡슐화에 문제가 생길 수 있다는 것이다.
    그 말은 곧 프로그램의 확장성과 유연성이 떨어진다는 의미이다.

  2. 관계에 대한 표현
    상속 관계의 클래스들은 어떤 관계일까?
    이것은 마치 사람-남자의 관계이다.
    남자는 남자만의, 여자와 다른 어떤 특징을 가진다.
    하지만 남자는 사람의 모든 특징을 가진다(상속한다).
    즉 '남자이면 사람이다'라는 명제는 참인 것이다.
    이런 관계를 상속이라는 개념으로 표현하는 것이다.
    이때 사람은 super class 남자는 sub class가 된다.
    이런 관계를 is-a 관계라고 한다.(남자 is a 사람)

자, 이제 우리는 상속을 단순 코드 재사용을 위해서보다는 어떤 관계를 표현하기 위해 사용한다는 것을 배웠다.
그렇다면 이런 관계의 표현은 왜 하는 것일까??
우리는 문제를 해결하기 위해 프로그래밍을 하는 것인데 딱히 쓸모는 없어보인다.
그렇다면 이제 사례를 들어 어떤 문제 해결을 도와주는지 확인해보자.


inheritance의 필요성

어떤 회사의 급여관리 프로그램이 있다고 하자.
이 회사는 처음에 3명의 정직원으로 구성되어 있었다.
그래서 C++을 이용해 하나의 클래스(정직원의 정보를 담는)를 정의하고 이 클래스의 객체들을 저장하고 급여를 계산하는 클래스(여러 객체를 저장하고 제어를 위한)를 만들어 프로그램을 구성했다.

class EmployeeHandler
{
private:
	PermanentWorker * empList[50];
	int empNum;
public:
	EmployeeHandler()
	: empNum(0)
	{
	}		
	
	void AddEmployee(PermanentWorker * emp)
	{
		empList[empNum++] = emp;
		return;
	}
	
	void ShowAllSalaryInfo() const
	{
		for(int i=0;i<empNum;i++)
			empList[i]->ShowWorkerInfo();
		return;	
	}
	
	void ShowTotalSalary() const
	{
		int sum = 0;
		for(int i=0;i<empNum;i++)
			sum += empList[i]->GetPay();
		cout<<sum<<endl;
		return;	
	}
};

헌데 이 회사가 성장을 해서 고용의 형태가 다양해진 것이다.
이젠 정직원 뿐만 아니라 계약직도 생겼고 영업직도 생겼으며 알바생도 생겼다. 각각은 급여계산 방식도 모두 다르다. 고로 하나의
이제 이 클래스에는 문제가 있다.

여러 직무를 저장할 수 없다.
현재는 한 직무만 저장 가능하다. 그럼 새로운 직무가 추가될 때마다 control class를 변경할 것인가??

그리하여 프로그래머는 생각이 들었다. 여러 클래스를 다루는(앞으로의 클래스 증가도 고려하여) 하나의 control class가 필요하겠다고.
이럴때 사용하는 것이 바로 상속이다.

inheritance의 성질 - pointer적 관점에서
parent class의 객체 포인터로 sub class의 객체를 저장할 수 있다.
단, 이 포인터로는 parent class의 property와 behavior만을 사용할 수 있다.

위의 성질에 의하면,
super class의 객체 포인터로 sub class의 객체를 저장할 수 있으니 모든 직무의 상위 class를 만들어서 이 class의 객체 포인터 배열로 데이터를 저장하면 된다.

이때 super class의 이름은 Employee 정도로 하면 적당할 것이다.

class EmployeeHandler
{
private:
	Employee * empList[50];
	int empNum;
public:
	EmployeeHandler()
	: empNum(0)
	{
	}		
	
	void AddEmployee(Employee * emp)
	{
		empList[empNum++] = emp;
		return;
	}
	
	void ShowAllSalaryInfo() const
	{
		for(int i=0;i<empNum;i++)
			empList[i]->ShowWorkerInfo();
		return;	
	}
	
	void ShowTotalSalary() const
	{
		int sum = 0;
		for(int i=0;i<empNum;i++)
			sum += empList[i]->GetPay();
		cout<<"salary sum: "<<sum<<endl;
		return;	
	}
};

자 이제 배열의 datatype의 문제는 해결이 되었다.
이제 하나의 배열로 모든 sub class의 주소를 저장할 수 있게 되었다.

헌데 다른 문제가 존재한다.
단, 이 포인터로는 parent class의 property와 behavior만을 사용할 수 있다.
이 성질에 따르면 employee * 형으로는 sub class의 method를 실행할 수 없다.

이 문제를 해결하기 위해 등장하는 개념이 바로 polymorphism이다.


polymorphism

우리 말로 다형성이다. 한국말로 해도 무슨 말인지 잘 모르겠다. 영어 그대로 보면 poly + morphism이 되는데
poly는 고분자 화합물 같은 곳에 쓰이는 말로 '많다'의 의미를 지닌다.
morphism은 '사상'인데 이데올로기가 아니라 mapping의 의미이다.
즉, 많이 맵핑된다는 의미이다.
위키에는 다음과 같이 나와있다.

하나의 인터페이스와 여러가지 실체를 연결하는 것

이같은 성질을 구현할 수 있다면 employee 객체에서 ShowWorkerInfo()와 GetPay()를 활용해서 다양한 임직원 정보 확인, 급여 계산을 할 수 있는 것이다.

이를 위해서 상속에서의 중요한 개념 두가지가 등장한다.

  1. overriding
    sub class는 super class로부터 상속한다.
    헌데 sub class와 super class와 동일한 기능을 수행하지만 디테일의 차이가 있다면 어떡하겠는가??
    완전히 다른 새로운 함수를 정의하는 것은 낭비이다.
    그러지 않고 상속받은 것에서 약간만 고쳐쓰면 좋을 것이다.
    이를 위해 제공하는 기능이 overriding이다.
    overriding을 통해 메소드를 정의하면 super class의 메소드는 가려지게 된다.

  2. virtual function
    참조자가 어떤 객체를 참조할 때, 해당 객체의 behavior는 참조형에 의해 정해진다고 위에서 말했다.
    이런 한계를 깨는 것이 가상함수이다.
    가상함수는 함수의 호출에 있어서 참조형에 의존하는 것이 아니라 실제 객체의 클래스에 의존한다.
    가상함수를 호출할 때는 가장 마지막에 정의된 가상함수가 호출된다.

class AAA
{
	int num1;
public:
	virtual void Func1()
	{
		cout<<"Func1()"<<endl;
	}
	
	virtual void Func2()
	{
		cout<<"Func2()"<<endl;
	}	
};

class BBB : public AAA
{
	int num2;
public:
	virtual void Func1()
	{
		cout<<"BBB::Func1()"<<endl;
	}	
	
	void Func3()
	{
		cout<<"Func3()"<<endl;
	}
};

BBB의 객체에서 Func1()은 가상함수이므로 Func1() 중에 가장 마지막에 정의된(클래스 정의 시점에 가장 마지막으로 정의된 Func1(), 클래스 자신의 Func1()) BBB::Func1()이 호출된다.
그리고 Func2()는 AAA의 객체에서 마지막으로 정의되었으므로 BBB의 객체에서 AAA::Func2()가 호출된다.


conclusion

객체 포인터를 이용하면 모든 subclass의 객체를 하나의 super class의 포인터(혹은 참조자)로 표현할 수 있다.

overriding을 이용한다면, 다양한 기능(함수의 몸체)을 구현하는 데 있어 하나의 메소드 이름이면 된다.
즉, 하나의 이름으로 여러 기능을 구현할 수 있다는 말이 된다.

virtual function을 이용하면 참조형에 구속받지 않고 객체의 클래스에 정의된 메소드를 호출할 수 있다.

이제 새로운 직무가 생긴다고 하더라도 컨트롤 클래스에 변화를 줄 필요가 없어졌다.

0개의 댓글