열다섯번째 열혈강의
이제 상속을 들어가기 전에 마지막으로 OOP 프로젝트 4단계를 진행해 보도록 하자.
이번엔 매우 간단하다. 그러나 중요도는 덜한 것이 아니니 잘 알아두고, 완성도를 높여 보도록 하자.
이번엔 const의 추가로 좀더 완성도를 높여볼 생각이다.
먼저, Account 클래스의 멤버함수 중 일부를 const 선언하면서 Banking System의 버전을 0.3에서 0.4로 업그레이드 시켜보자. const 선언이 가능한 모든 멤버함수를 const 선언할 예정이다.
아래는 결과코드이며 자신이 만들어 놓은 코드와 비교해 볼 수 있도록 넣어 두도록 하곘다.
class Account {
private:
int accID;
int balance;
char* cusName;
public:
Account(int ID, int money, char* name) : accID(ID), balance(money){
cusName = new char[strlen(name) + 1];
strcpy(cusName, name);
}
Account(const Account & ref) : acc(ref.accID), balance(ref.balance){
cusName = new char[strlen(ref.cusName) + 1];
strcpy(cusName, ref.cusName);
}
int GetAccId() const { return accID; }
void Deposit(int money){
balance += money;
}
int Withdraw(int money){
if(balance < money)
return 0;
balance -= money;
return money;
}
void ShowAccInfo() const{
cout << "계좌ID : " << accID << endl;
cout << "이 름 : " << cusName << endl;
cout << "잔 액 : " << balance << endl;
}
~Account(){
delete []cusName;
}
}
객체지향의 전개
상속(inheritance)의 이해
드디어 대제목이 또 한 번 바뀌었다. 이번엔 객체지향의 전개에 상속을 들여다 볼 것이다. 상속은 특히 적용이 중요한 문법이고, 이는 많은 부분에 상속을 적용해야 한다는 것이 아니라 적절한 때에 선별적으로 적용할 수 있어야 한다.
먼저 상속에 들어가기에 앞서, 상속을 이해하기 위한 접근방식에 대해 알아보자.
무엇보다 상속을 설명할 때 간단히 할 수 도 있다. 그러나 이러한 상속의 이점부터 정확히 이해하는 것이 순서다. 먼저 이 책에서는 다음과 같은 순서로 접근한다.
- 1단계 : 문제의 제시
- 상속과 더불어 다형성을 적용해야 하는 문제 제시
- 2단계 : 기본개념 소개
- 상속의 문법적 요소를 하나씩 소개한다.
- 3단계 문제의 해결
- 처음 제시한 문제를 상속을 적용해 해결한다.
상속에 대해 조금더 새로운 관점으로 이해해 보자. 먼저, C++ 의 상속응 그 단어의 의미처럼 물려 받는다는 성격이 강하다. 따라서 과거에는 다음과 같은 생각으로 상속을 보았다.
기존에 정의해 놓은 클래스의 재활용을 목적으로 만들어진 문법적 요소가 상속이다.
물론 상속에는 이러한 이점도 존재하지만, 이는 근본적인 이유 가 되지 못한다. 그래서 이 책의 저자는 꼭 상속이 재활용의 목적으로만 존재하는 문법적 요소가 아님을 강조 한다. 따라서 이런 관점에 편협되지 않게 잘 따라가 보자.
시나리오
먼저 문제의 제시를 위해 예시를 하나 보도록 하자. 이는 OrangeMedia라는 회사가 운용하는 급여관리 시스템 이다.(가정) 이 회사가 처음 시스템을 도입했을 때만 해도 직원의 근무형태는 정규직(Permanent) 하나였다. 따라서 정규직 직원을 관리하기 위한 형태로 설계가 되었다.
class PermanentWorker{
private:
char name[100];
int salary;
public:
PermanentWorker(char* name, int money) : salary(money){
strcpy(this->name, name);
}
int GetPay() const{
return salary;
}
void ShowSalaryInfo() const{
cout << "name : " << name << endl;
cout << "salary : " << GetPay() << endl << endl;
}
};
이 회사의 정규직 급여는 입사 당시 정해진다(급여 인상은 고려하지 말자) 따라서 이름과 급여정보 를 저장할 수 있도록 클래스를 정의 하였다. 다음은 위에서 정의한 클래스의 객체를 저장 및 관리하기 위한 클래스이다. PermanentWorker 객체의 저장을 목적으로 배열을 멤버로 지니고 있으며, 저장된 객체의 급여 정보를 출력하기 위한 함수를 멤버로 지니고 있다.
class EmployeeHandler{
private:
PermanentWorker* empList[50];
int empNum;
public:
EmployeeHandler() : empNum(0){}
void AddEmployee(PermanentWorker* emp){
empList[empNum++] = emp;
}
void ShowAllSalaryInfo() const{
for(int i = 0; i < empNum; i++){
empList[i] -> ShowSalaryInfo();
}
}
void ShowTotalSalary() const{
int sum = 0;
for(int i = 0; i < empNum; i++){
sum += empList[i] -> GetPay();
cout << "salary sum: " << sum << endl;
}
}
~EmployeeHandler(){
for(int i = 0; i < empNum; i++){
delete empList[i];
}
}
};
위의 클래스는 앞서 정의했던 PermanentWorker 클래스와 성격이 다르다는 걸 주목해야 한다. 앞서 정의했던 클래스는 데이터적 성격 이 강하다. 반면, 위에서 정의한 클래스는 기능적 성격 이 강하다. 쉽게 말해서 기록의 보전을 위해서 파일에 저장할 데이터를 가지고 있는 것 은 PermanentWorker 객체인 반면, EmployeeHandler 객체는 프로그램을 구성하는 대표적인 기능들을 처리하는 클래스 이다.
- 새로운 직원정보의 등록
- AddEmployee
- 모든 직원의 이번 달 급여정보 출력
- ShowAllSalaryInfo
- 이번 달 급여의 총액 출력
- ShowTotalSalary
그리고 이렇게 기능의 처리를 실제로 담당하는 클래스 를 가리켜 컨트롤(control) 클래스
또는 핸들러(handler) 클래스
라 한다.
참고로 컨트롤 클래스와 관련해서 보면, PermanentWorker 클래스만 놓고 볼 때, 이 프로그램이 어떠한 기능을 제공하는 지 구체적으로 알지 못한다. 그러나 EmployeeHandler 클래스를 보면 이 프로그램이 어떠한 기능을 제공하는 지 구체적으로 알 수 있다. 이렇든 컨트롤 클래스는 기능 제공의 핵심으로 객체지향 프로그램에서 반드시 존재하는 클래스이다.
그럼 마지막으로 main 함수를 보자.
int main(void){
//직원관리를 목적으로 설게된 컨트롤 클래스의 객체 생성
EmployeeHandler handler;
//직원 목록
handler.AddEmployee(new PermanentWorker("KIM", 1000));
handler.AddEmployee(new PermanentWorker("LEE", 1500));
handler.AddEmployee(new PermanentWorker("JUN", 2000));
//이번 달에 지불해야 할 급여의 정보
handler.ShowAllSalaryInfo();
//이번 달에 지불해야 할 급여의 총합
handler.ShowTotalSalary();
return 0;
}
간단한 출력이 나온다. 언뜻 큰 문제가 없어 보인다. 복잡하지도 않고, 실행결과도 명확하기 때문이다. 하지만 객체지향에서, 아니 모든 소프트웨어의 설계에 있어서 중요시하는 것은 다음과 같다.
- 요구사항의 변경에 대응하는 프로그램의 유연성
- 기능의 추가에 따른 프로그램의 확장성
예를 들어 보자. 프로그램 사용자의 업무형태가 바뀌어서 프로그램 변경을 요구할 수도 있는 일이고, 회사의 업무가 확장되어 프로그램의 기능추가를 요구할 수도 있는 일이다. 그런데 좋은 프로그램은 이러한 변경의 요구에 대처가 가능해야 한다.
가장 안좋은 대답은 이것이다.
그 기능을 변경하려면, 프로그램을 거의 처음부터 다시 만들다시피 해야 하는데요.
사실 요구사항의 변경에 대한 프로그램의 유연성과 확장성의 확보는 쉽지 않은 일이다. 따라서 이를 100% 만족하는 프로그램을 구현하는 것은 생각하기 힘들다. 다만, 조금이라도 더 유연하게 조금이라도 더 확장성이 좋게 프로그램을 디자인하려고 노력할 뿐이다.
사실 엄청 공감되는 말이다. 그리고 위 문장은 마치 나를 보는 것 같아 덜컹했다..
아직 한참은 멀었다는 게 절실히 느껴진다..
문제의 제시
우리가 구현한 프로그램을 사용하던 OrangeMedia에서 다음을 요구했다.
회사가 번창하여 이제 부서도 세분화되었고 직원도 늘어나게 되었습니다. 그러다 보니 직원의 고용형태가 조금 다양해 졌습니다.
이전에는 직원의 고용형태가 정규직(Permanent) 하나였는데, 이제는 다음과 같이 새로운 고용형태가 등장했다.
- 영업직(Sales)
- 조금 특화된 형태의 고용직이다. 인센티브 개념이 도입
- 임시직(Temporary)
- 학생들을 대상으로 하는 임시고용 형태, 흔히 아르바이트라 함
이 중에서 영업직은 판매의 장려를 위해 기본급여뿐 아니라, 판매실적에 따른 인센티브 제도까지 적용받는 고용형태다. 그리고 임시직은 학생들이 방학기간에 일할 수 있도록 돕는 고용형태다. 그런데, 사실 고용의 형태가 다양하더라도 급여의 계산방식만 동일하면 이를 특별히 구분할 필요가 없다. 하지만 다음과 같은 차이가 있다.
- 고용직 급여
- 연봉제! 따라서 매달의 급여가 정해져 있다.
- 영업직 급여
- 기본급여 + 인센티브 의 형태
- 임시직 급여
- 시간당 급여 * 일한 시간 의 형태
따라서 이런 요구사항을 들어주기 위해서는 각각에 맞추어 반영을 해야 한다. 그런데 이를 매우 쉽게 생각할 수도 있다.
영업직을 의미하는 SalesMan 클래스와 임시직을 의미하는 Temporary 클래스를 추가하면 되겠네
하지만, 문제는 전혀 다른 곳에 있다. SalesMan 클래스와 Temporary 클래스를 추가했을 때, 이를 반영하기 위해서 EmployeeHandler 클래스가 어떻게 변경되어야 하는지 보자.
SalesMan 객체와 Temporary 객체의 저장을 위한 배열을 두 개 추가하고, 각각의 배열에 저장된 객체의 수를 별도로 세어야 하니, 정수형 변수도 멤버로 두 개 추가해야 하네
여기서 끝이 아니다.
AddEmployee 함수는 SalesMan 객체용과 Temporary 객체용으로 각각 추가되어야 하고, 급여정보를 출력하는 나머지 두 멤버함수는 총 3개의 배열을 대상으로 연산을 진행해야 하니까, 반복문이 추가로 각각 두 개씩 삽입되어야 하는군
결과적으로는 다 바뀌어야 한다. 즉, 아까 말했던 모두 갈아엎어야 하는 것이다. 따라서 확장성 에 있어서는 좋은 점수를 줄 수가 없는 형태다.
즉, 변경을 최소화하는 형태가 필요하다 특히 변화가 심한 EmployeeHandler 클래스를 변경하지 않아도 된다면, 매우 좋은 점수를 받는다.
이제 여기서 제대로 상속에 대해 들어가 보도록 할 것이다. 참고로 확장성에 대한 얘기는 나중에 할 것이므로 일단 잠시 접어두고 있자.
이 후의 내용은 다음 포스팅에 하도록 하겠다.