read

열여덟번째 열혈강의


friend

오랜 기간 적지 못했던 C++이지만 의지로 책 마무리를 짓겠다는 마음 가짐으로 진행해 가겠다.

지난 번 상속을 하며, OOP 단계별 프로젝트 5단계를 한다고 했다.

여기서 우리는 기능 제공 클래스를 추가해 줄 것이다. 먼저 복습을 간단히 해보자.

컨트롤 클래스의 특징

  • 프로그램 전체의 기능을 담당한다. 따라서 기능적 성격이 강한 클래스다.
  • 컨트롤 클래스만 봐도 프로그램의 전체 기능과 흐름을 파악할 수 있다.

반면 컨트롤 클래스가 아닌 대부분의 클래스를 Entity 클래스 라 하는데, 특징은 다음과 같다.

Entity 클래스의 특징

  • 데이터적 성격이 강하다. 따라서 파일 및 데이터 베이스에 저장되는 데이터를 소유하고 있다.
  • 프로그램의 기능을 파악하는데 도움을 주지는 못한다.
  • 그러나 프로그램상에서 관리되는 데이터의 종류를 파악하는 데는 도움이 된다.

먼저 우리가 지금껏 하던 프로젝트의 주요기능들을 보자.

  • 계좌개설
  • 입금
  • 출금
  • 계좌정보 전체 출력

이러한 기능은 전역함수를 통해 구현했는데, 객체지향에서는 전역 개념이 존재하지 않는다. 물론 전역함수나 변수가 허용하지만 이것은 객체지향을 위한 것은 아니므로 가급적 사용을 금한다.

그럼 설계를 해보도록 하자.

  • AccountHandler 라는 이름의 컨트롤 클래스를 정의하고, 앞서 정의한 전역변수를 멤버함수로 한다.
  • Account 객체의 저장을 위해 선언한 배열과 변수도 이 클래스의 멤버에 포함시킨다.
  • AccountHandler 클래스 기반으로 main을 변경한다.

위 사항들을 잘 체크해서 코딩을 해보도록 하자. 예제 코드는 다음과 같다.

#include <iostream>
#include <cstring>

using namespace std;
const int NAME_LEN = 20;

enum {MAKE=1, DEPOSIT, WITHDRAW, INQUIRE, EXIT};

class Account {
private:
int accID;
int balance;
char* cusName;

public:
Account(int ID, int money, char* name);
Account(const Account& ref);

int getAccID() const;
void deposit(int money);
int withDraw(int money);
void showAccInfo() const;
~Account();
};

Account::Account(int ID, int money, char* name) : accID(ID), balance(money){
cusName = new char[strlen(name) + 1];
strcpy(cusName, name);
}

Account::Account(const Account& ref) : accID(ref.accID), balance(ref.balance){
cusName = new char[strlen(ref.cusName) + 1];
strcpy(cusName, ref.cusName);
}

int Account::getAccID() const {return accID;}

void Account::deposit(int money) {
balance+=money;
}

int Account::withDraw(int money) {
if(balance<money)
return 0;

balance -= money;
return money;
}

void Account::showAccInfo() const {
cout << "계좌ID : " << accID << endl;
cout << "이름 : " << cusName << endl;
cout << "잔액 : " << balance << endl;
}

Account::~Account() {
delete []cusName;
}

class AccountHandler{
private:
Account* accArr[100];
int accNum;
public:
AccountHandler();
void showMenu() const;
void makeAccount();
void depositMoney();
void withDrawMoney();
void showAllAccInfo() const;
~AccountHandler();
};

void AccountHandler::showMenu() const {
cout << " ===== MENU ===== " << endl;
cout << "1. 계좌개설 " << endl;
cout << "2. 입금 " << endl;
cout << "3. 출금 " << endl;
cout << "4. 계좌정보 전체 출력 " << endl;
cout << "5. 종료 " << endl;
}

void AccountHandler::makeAccount() {
int id;
char name[NAME_LEN];
int balance;

cout << "[계좌개설]" << endl;
cout << "계좌 ID : "; cin >> id;
cout << "이름 : "; cin >> name;
cout << "입금액 : "; cin >> balance;
cout << endl;

accArr[accNum++] = new Account(id, balance, name);
}

void AccountHandler::depositMoney() {
int money;
int id;
cout << "[입 금]" << endl;
cout << "계좌ID : "; cin >> id;
cout << "입금액 : "; cin >> money;

for(int i = 0; i < accNum; i++){
if(accArr[i]->getAccID() == id){
    accArr[i]->deposit(money);
    cout << "입금완료" << endl;
    return;
}
}
cout << "유효하지 않은 ID 입니다." << endl << endl;
}

void AccountHandler::withDrawMoney() {
int money;
int id;
cout << "[출 금]" << endl;
cout << "계좌ID : "; cin >> id;
cout << "출금액 : "; cin >> money;
for(int i = 0; i < accNum; i++){
if(accArr[i]->getAccID() == id){
    if(accArr[i]->withDraw(money) == 0){
        cout << "잔액부족" << endl << endl;
        return;
    }

    accArr[i]->withDraw(money);
    cout << "출금완료" << endl;
    return;
}
}
cout << "유효하지 않은 ID 입니다." << endl << endl;
}

AccountHandler::AccountHandler() : accNum(0) {}

void AccountHandler::showAllAccInfo() const {
for(int i = 0; i < accNum; i++){
accArr[i]->showAccInfo();
cout << endl;
}
}

AccountHandler::~AccountHandler() {
for(int i = 0; i < accNum; i++){
delete accArr[i];
}
}

int main(){
AccountHandler manager;
int choice;

while(1){
manager.showMenu();
cin >> choice;
cout << endl;

switch(choice){
    case MAKE:
        manager.makeAccount();
        break;
    case DEPOSIT:
        manager.depositMoney();
        break;
    case WITHDRAW:
        manager.withDrawMoney();
        break;
    case INQUIRE:
        manager.showAllAccInfo();
        break;
    case EXIT:
        return 0;
    default:
        cout << "Illigal selection.." << endl;
}
}
return 0;
}

상속과 다형성


아마 이 책의 전체 중 가장 중요한 단원이 아닐까 라고 소개한다. 다형성의 문제다. C++에서 객체지향의 성격을 제대로 알아보자.

앞서, IS-A 관계에 대해 말한 적 있다. 이제 이 관계에 초점을 두고, 가보도록 하자.

객체 포인터 변수라고 있다. 객체의 주소 값을 저장하는 포인터 변수 인데, 클래스를 기반으로도 포인터 변수를 선언 할 수 있음은 이미 알 고 있다. 예를 하나 들어보면,

Person* ptr;
pt = new Person();

을 보면, 포인터 ptr은 Person 객체를 가리키게 된다. 그런데 Person형 포인터는 Person 객체 뿐 아니라, Person을 상속하는 유도 클래스의 객체도 가리킬 수 있다. 예를 들어 보자.

class Student : public Person{
  ...
};

Person형 포인터 변수는 다음과 같이 Student 객체도 가리킬 수 있다.

Person* ptr = new Student();

그리고 Student 클래스를 상속하는 유도 클래스 PartTimeStudent가 다음과 같이 정의되어 있다고 하면,

class PartTimeStudent : public Student{
  ...
};

Person형 포인터 변수 혹은 Student 포인터 변수는 다음과 같이 할 수 있다.

Person* ptr = new PartTimeStudent(); Student* ptr = new PartTimeStudent();

그럼 위 내용을 한번 정리해 보자.

PartTimeStudent는 Person을 직접 상속하진 않지만, Person을 상속하는 Student를 상속함으로써, Person 클래스를 간접 상속하고 있다.

C++에서, AAA형 포인터 변수는 AAA 객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다.(객체의 주소 값을 저장할 수 있다.)

그럼 왜 가능한지가 궁금하다. 그리고 어떤 의미를 가지는지 궁금하다.

그럼 우선 확인 예제를 한가지 풀고 넘어가도록 하자.

#include <iostream>

using namespace std;

class Person{
public:
    void Sleep() const {
        cout << "Sleep" << endl;
    }
};

class Student : public Person{
public:
    void Study() const {
        cout << "Study" << endl;
    }
};

class PartTimeStudent : public Student{
public:
    void Work() const{
        cout << "Work" << endl;
    }
};

int main(void){
    Person* ptr1 = new Student();
    Person* ptr2 = new PartTimeStudent();
    Student* ptr3 = new PartTimeStudent();
    ptr1->Sleep();
    ptr2->Sleep();
    ptr3->Study();
    delete ptr1;
    delete ptr2;
    delete ptr3;
    return 0;
}

위 예제를 통해 정리가 되었으면 이제 질문을 던지자.

위와 같이 논리적이지 않은 기능을 넣은 이유가 뭘까?

그러나 객체 포인터의 다음과 같은 특성은 상속의 IS-A관계를 통해 논리적으로 이해가 가능하다.

C++에서, AAA형 포인터 변수는 AAA 객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다.(객체의 주소 값을 저장할 수 있다.)

이를 한번 풀어 보자.

학생은 사람이다.

알바학생은 학생이다.

알바학생은 사람이다.

이는 IS-A관계롤 풀어 썼음을 알 수 있다. 또한,

학생은 사람의 일종이다.

알바학생은 학생의 일종이다.

알바학생은 사람의 일종이다.

와 같이 풀어 쓸 수도 있다.

실제로 객체지향에서는 위 문장과 같이, Student 객체와 PartTimeStudent 객체를 Person 객체의 일종 으로 간주한다. 또한, PartTimeStudent 객체를 Student 객체의 일종 으로 간주하게 된다. 따라서 위 예제와 같은 특성이 가능한 것이다.


함수 오버라이딩


이전에 한번 다루었던 급여관리 확장성 문제 를 기억하는가?

위 포인터 특성은 바로 이 확장성의 실마리가 된다.

당시에 추가해야 한다고 생각했던 것이 영업직, 임시직에 대한 내용이였는데, 이는 다음과 같은 형태를 가진다.

정규직, 영업직, 임시직 모두 고용의 한 형태이다.

영업직은 정규직의 일종이다.

그러므로 클래스를 다음과 같이 만들어보자.

  • 고용인 Employee
  • 정규직 PermanentWorker
  • 영업직 SalesWorker
  • 임시직 TemporaryWorker

그럼 관계가 그려지게 된다.

고용인 밑에 정규직과 임시직이 있게 되고 영업직은 정규직 밑에 가게 된다.

따라서 앞서 나왔던 문제의 해결로써, EmployeeHandler 클래스가 저장 및 관리하는 대상이 Employee 객체가 되게 하면, 이후에 Employee 클래스를 직접 혹은 간접적으로 상속하는 클래스가 추가되었을 때, EmployeeHandler 클래스에는 변화가 발생하지 않는다.

그럼 코딩을 한번 해보도록 하자. 위 문장을 이해하는 것이 포인트다.

#include <iostream>

using namespace std;

class Employee{
private:
    char name[100];
public:
    Employee(char* name){
        strcpy(this->name, name);
    }
    void ShowYourName() const {
        cout << "name : " << name << endl;
    }
};

class PermanentWorker : public Employee{
private:
    int salary;
public:
    PermanentWorker(char* name, int money) : Employee(name), salary(money){}
    int GetPay() const{
        return salary;
    }
    void ShowSalaryInfo() const{
        ShowYourName();
        cout << "salary : " << GetPay() << endl << endl;
    }
};

class EmployeeHandler{
private:
    Employee* empList[50];
    int empNum;
public:
    EmployeeHandler() : empNum(0){}
    void AddEmployee(Employee* 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];
        }
    }
};

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;
}

아마 출력과 주석이 매우 거슬릴 것이다. 그러나 일단 흐름을 알아보자.

위 예제에서 EmployeeHandler 객체가 여전히 PermanentWorker 객체를 저장, 관리하고 있다는 점에만 주목하자.

그럼 다음 두 고용형태의 클래스를 추가해보자.

  • 영업직 급여 - ‘기본급여(월 기본급여) + 인센티브’의 형태
  • 임시직 급여 - ‘시간당 급여 * 일한 시간’의 형태

먼저 임시직에 해당하는 클래스를 정의해보자.

class TemporaryWorker : public Employee{
private:
    int workTime;
    int payPerHour;
public:
    TemporaryWorker(char* name, int pay) : Employee(name), workTime(0), payPerHour(pay){}
    void AddWorkTime(int time){
        workTime += time;
    }
    int GetPay() const{
        return workTime * payPerHour;
    }
    void ShowSalaryInfo() const{
        ShowYourName();
        cout << "salary : " << GetPay() << endl << endl;
    }
};

PermanentWorker와 달리 실제 일을 한 시간을 기준으로 급여를 계산하도록 클래스가 정의되었다. 그럼 이어서 영업직에 해당하는 클래스를 정의해보겠다. 여기서 주의할 점은 영업직도 정규직의 일종이므로, 이 클래스는 Employee가 아닌 PermanentWorker를 상속한다는 점이다.

class SalesWorker : public PermanentWorker{
private:
    int salesResult;
    double bonusRatio;
public:
    SalesWorker(char* name, int money, double ratio) : PermanentWorker(name, money), salesResult(0), bonusRatio(ratio){}
    void AddSalesResult(int value){
        salesResult += value;
    }
    int GetPay() const{
        return PermanentWorker::GetPay() + (int)(salesResult * bonusRatio);
    }
    void ShowSalaryInfo() const{
        ShowYourName();
        cout << "salary : " << GetPay() << endl << endl;
    }
};

이 클래스는 PermanentWorker를 상속하여, 기본급여와 관련된 부분을 멤버로 포함했고, 상여금 부분만 멤버로 추가되었다. 그럼 위 클래스를 보고 생각나는 것들을 말해보자.

PermanentWorker 클래스에도 GetPay, ShowSalaryInfo 함수가 있는데, 유도 클래스인 SalesWorker 클래스에도 정의가 되어 있다.

이를 가리켜 함수 오버라이딩 이라고 한다. 그리고 이렇게 함수가 오버라이딩되면 기초 클래스 함수는 오버라이딩을 한 유도 클래스의 함수에 가려진다. 따라서 위 SalesWorker 클래스 내에서 GetPay함수를 호출하면, SalesWorker 클래스에 정의된 GetPay함수가 호출된다. 그럼 다음과 같이 호출 할때,

int main(void){
  SalesWorker seller("Hong", 1000, 0.1);
  cout << seller.GetPay() << endl;
  seller.ShowSalaryInfo();
}

SalesWorkerGetPay 함수가 호출된다. 그런데 SalesWorker 의 GetPay 내에는 PermanentWorker::GetPay() 와 같이 적혀 있는데, 이는 오버라이딩 된 기초 클래스의 GetPay함수를 호출한다는 것이다.

int main(void){
  SalesWorker seller("Hong", 1000, 0.1);
  cout << seller.PermanentWorker::getPay() << endl;
  seller.PermanentWorker::ShowSalaryInfo();
}

여기서 seller.PermanentWorker::ShowSalaryInfo(); 이 것의 의미를 보자.

seller 객체의 PermanentWorker 클래스에 정의된 ShowSalaryInfo 함수를 호출하라.

근데 조금 조잡하다. 보통 이렇게까지 오버라이딩된 기초 클래스 함수를 쓰지는 않으므로 아는 정도로 넘어가자.

그럼 예제에 추가해 보도록 하자.

#include <iostream>

using namespace std;

class Employee{
private:
    char name[100];
public:
    Employee(char* name){
        strcpy(this->name, name);
    }
    void ShowYourName() const {
        cout << "name : " << name << endl;
    }
};

class PermanentWorker : public Employee{
private:
    int salary;
public:
    PermanentWorker(char* name, int money) : Employee(name), salary(money){}
    int GetPay() const{
        return salary;
    }
    void ShowSalaryInfo() const{
        ShowYourName();
        cout << "salary : " << GetPay() << endl << endl;
    }
};

class EmployeeHandler{
private:
    Employee* empList[50];
    int empNum;
public:
    EmployeeHandler() : empNum(0){}
    void AddEmployee(Employee* 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];
        }
    }
};

class TemporaryWorker : public Employee{
private:
    int workTime;
    int payPerHour;
public:
    TemporaryWorker(char* name, int pay) : Employee(name), workTime(0), payPerHour(pay){}
    void AddWorkTime(int time){
        workTime += time;
    }
    int GetPay() const{
        return workTime * payPerHour;
    }
    void ShowSalaryInfo() const{
        ShowYourName();
        cout << "salary : " << GetPay() << endl << endl;
    }
};

class SalesWorker : public PermanentWorker{
private:
    int salesResult;
    double bonusRatio;
public:
    SalesWorker(char* name, int money, double ratio) : PermanentWorker(name, money), salesResult(0), bonusRatio(ratio){}
    void AddSalesResult(int value){
        salesResult += value;
    }
    int GetPay() const{
        return PermanentWorker::GetPay() + (int)(salesResult * bonusRatio);
    }
    void ShowSalaryInfo() const{
        ShowYourName();
        cout << "salary : " << GetPay() << endl << endl;
    }
};


int main(void){
    // 직원관리를 목적으로 설계된 컨트롤 클래스의 객체생
    EmployeeHandler handler;
    // 정규직 등록
    handler.AddEmployee(new PermanentWorker("KIM", 1000));
    handler.AddEmployee(new PermanentWorker("LEE", 1500));
    // 임시직 등록
    TemporaryWorker* alba = new TemporaryWorker("Jung", 700);
    alba->AddWorkTime(5);
    handler.AddEmployee(alba);
    // 영업직 등록
    SalesWorker* seller = new SalesWorker("Hong", 1000, 0.1);
    seller->AddSalesResult(7000);
    handler.AddEmployee(seller);

    // 이번 달 지불 급여 정보
    handler.ShowAllSalaryInfo();
    // 이번달 지불 급여 총합
    handler.ShowTotalSalary();
    return 0;
}

결과는 아마 그 전과 똑같을 것이다. 이게 중요한 결과라는 걸 놓치면 안된다.

우리는 분명 새로운 클래스를 추가 했는데도 불구하고, 그 결과는 변함이 없다.

그럼 SalesWorker에서 ShowSalaryInfo를 오버라이딩 한 이유가 무엇일까?

주석을 제거하면 컴파일 에러가 나오는데, 여기에 대한 설명과 해결책은 다음 포스팅 정도에 설명할 가상함수로 이어진다.

그런데 이에 앞서 PermanentWorker 클래스와 이를 상속하는 SalesWorker 클래스의 ShowSalaryInfo함수를 다시 보도록 하자.

SalesWorker 클래스의 ShowSalaryInfo 함수는 PermanentWorker 클래스의 ShowSalaryInfo 함수와 완전히 동일한데, 왜 오버라이딩을 했을까?

PermanentWorker 클래스의 ShowSalaryInfo 함수는 상속에 의해 SalesWorker 객체에도 존재하게 된다. 그러나 상속이 되었다 하더라도 PermanentWorker 클래스의 ShowSalaryInfo함수 내에서 호출되는 GetPay 함수는 PermanentWorker 클래스에 정의된 GetPay 함수의 호출로 이어지므로, SalesWorker 클래스에 정의된 GetPay 함수가 호출되도록 SalesWorker 클래스에 별도의 ShowSalaryInfo 함수를 정의해야 한다.

다음 포스팅에서 이어 가상함수를 알아보자.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview