다형성(Polymorphism) 예제
Deer 클래스
Deer 클래스를 아래와 같이 정의해보자. ▼
class Deer {
private:
char name[10]; // 사슴 이름
int age; // 사슴 나이
double weight; // 사슴 무게
double height; // 사슴 키
public:
Deer(const char* name, int age, double weight, double height, double antlersLength); // 생성자
char* getDeerName() const; // 사슴 이름 getter
int getDeerAge() const; // 사슴 나이 getter
double getDeerWeight() const; // 사슴 몸무게 getter
double getDeerHeight() const; // 사슴 키 getter
void showDeerInfo() const; // 사슴 정보 모두 출력
};
Deer 클래스는 사슴의 이름, 나이, 무게, 키를 갖는다. ▼
DeerList 클래스
그리고 이 사슴들을 리스트로 묶어보는 DeerList 클래스를 아래와 같이 정의했다. ▼
class DeerList {
private:
Deer* deerList[20]; // 사슴 포인터 리스트
int deerNumber; // 사슴 수
public:
DeerList() : deerNumber(0) {};
// 사슴 추가
void addDeer(Deer* deer) {
deerList[deerNumber++] = deer;
}
// 모든 사슴 정보 보여주기
void showAllDeerInfo() const {
for (int i = 0; i < deerNumber; i++) {
deerList[i]->showDeerInfo();
}
}
// 사슴 무게 총 합 출력
void showTotalWeight() const {
int sum = 0;
for (int i = 0; i < deerNumber; i++) {
sum += deerList[i] -> getDeerWeight();
}
cout << "total weight: " << sum << endl;
}
// 사슴 리스트 소멸자
~DeerList()
{
for (int i = 0; i < deerNumber; i++) {
delete deerList[i];
}
}
};
사슴들은 사슴 포인터 배열로 관리되며, DeerList는 배열을 관리하는 메소드를 갖는다. ▼
WaterDeer와 Elk 클래스
그런데 사슴과에 속하는 고라니(WaterDeer)와 순록(Elk)을 추가했다고 해보자. ▼
class WaterDeer {
private:
char WaterDeerName[10]; //고라니 이름
int WaterDeerAge; // 고라니 나이
double WaterDeerWeight; // 고라니 무게
double WaterDeerHeight; // 고라니 키
public:
WaterDeer(const char* WaterDeerName, int WaterDeerAge, double WaterDeerWeight, double WaterDeerHeight);
char* getWaterDeerName() const; // 고라니 이름 getter
int getWaterDeerAge() const; // 고라니 나이 getter
double getWaterDeerWeight() const; // 고라니 몸무게 getter
double getWaterDeerHeight() const; // 고라니 키 getter
void showWaterDeerInfo() const; // 고라니 정보 모두 출력
};
class Elk {
private:
char ElkName[10]; // 순록 이름
int ElkAge; // 순록 나이
double ElkWeight; // 순록 무게
double ElkHeight; // 순록 키
double ElkAntlerLength; // 순록 뿔 길이
public:
Elk(const char* WaterDeerName, int WaterDeerAge, double WaterDeerWeight, double WaterDeerHeight);
char* getElkName() const; // 순록 이름 getter
int getElkAge() const; // 순록 나이 getter
double getElkWeight() const; // 순록 몸무게 getter
double getElkHeight() const; // 순록 키 getter
void showElkInfo() const; // 순록 정보 모두 출력
};
고라니와 순록은 사슴으로서 겹치는 부분이 많기 때문에 사슴을 상속받는 방식으로 재구성했다. ▼
class Deer {
private:
char name[10]; // 사슴 이름
int age; // 사슴 나이
double weight; // 사슴 무게
double height; // 사슴 키
public:
Deer(const char* name, int age, double weight, double height, double antlersLength); // 생성자
char* getDeerName() const; // 사슴 이름 getter
int getDeerAge() const; // 사슴 나이 getter
double getDeerWeight() const; // 사슴 몸무게 getter
double getDeerHeight() const; // 사슴 키 getter
};
class WaterDeer : public Deer{
private:
double canineLength; // 고라니 송곳니 길이
public:
WaterDeer(const char* WaterDeerName, int WaterDeerAge, double WaterDeerWeight, double WaterDeerHeight, double canineLength);
void showWaterDeerInfo() const; // 고라니 정보 모두 출력
};
class Elk : public Deer {
private:
double antlerLength; // 순록 뿔 길이
public:
Elk(const char* WaterDeerName, int WaterDeerAge, double WaterDeerWeight, double WaterDeerHeight, double antlerLength);
void showElkInfo() const; // 순록 정보 모두 출력
};
문제점 발생
위처럼 재구성하게 되면 DeerList에서 사슴의 정보를 모두 출력하는 void showAllDeerInfo()
메소드에 문제가 생긴다.
고라니에게는 송곳니 길이(canineLength)가 있고, 순록에게는 뿔 길이(antlerLength)가 있기 때문이다.
각 정보에 맞게 출력하기 위해서는 고라니 배열 따로, 순록 배열 따로 두어 두 배열을 모두 순회하며 정보를 출력해야하는 번거로움이 발생하게 된다. ▼
class DeerList {
private:
WaterDeer* waterDeerList[20]; // 고라니 포인터 리스트
Elk* elkList[20];
int waterDeerNumber; // 사슴 수
int elkNumber
public:
DeerList() : deerNumber(0) {};
// 사슴 추가
void addWaterDeer(WaterDeer* waterDeer) {
waterDeerList[waterDeerNumber++] = waterDeer;
}
void addElk(Elk* elk) {
elkList[elkNumber++] = elk;
}
// 모든 사슴 정보 보여주기
void showAllDeerInfo() const {
for (int i = 0; i < waterDeerNumber; i++) {
waterDeerList[i]->showWaterDeerInfo();
}
for (int i = 0; i < elkNumber; i++) {
elkList[i]->showElkInfo();
}
}
// 사슴 무게 총 합 출력
void showTotalWeight() const {
int sum = 0;
for (int i = 0; i < deerNumber; i++) {
sum += waterDeerList[i] -> getDeerWeight();
}
for (int i = 0; i < deerNumber; i++) {
sum += elkList[i] -> getDeerWeight();
}
cout << "total weight: " << sum << endl;
}
// 사슴 리스트 소멸자
~DeerList()
{
for (int i = 0; i < waterDeerNumber; i++) {
delete waterDeerList[i];
}
for (int i = 0; i < elkNumber; i++) {
delete elkList[i];
}
}
};
이렇게 되면 얼추 해결된 듯 보이지만, 나중에 다른 사슴들을 추가하게 되면, 계속해서 코드를 추가해야한다. ▼
이말은 즉슨 코드의 의존성이 존재하게 된다는 것이다.
DeerList라는 클래스를 구성하기 위해서는 다른 서브 클래스들의 구조를 모두 알고 있어야 한다.
서브 클래스의 코드가 수정되면, DeerList의 코드도 같이 수정해야하는 상황이 일어난다.
이렇게 되면 클래스가 독립적이지 않기에 모듈화도 지켜지지 않게 된다.
해결
overriden virtual function
이름을 보여주는 부분이나 무게 총 합 출력과 같은 부분은 부모 클래스인 Deer 클래스 포인터를 통해 하나로 구현이 되지만, 모든 사슴 정보를 보여주는 부분은 고라니 클래스와 순록 클래스의 정보 차이 때문에 따로 구현을 해줘야한다.
하지만 하나의 메소드로 통일해서 사용할 수 있다면 모든게 해결되고, 이를 도와주는 것이 virtual function이다.
class Deer {
private:
char name[10]; // 사슴 이름
int age; // 사슴 나이
double weight; // 사슴 무게
double height; // 사슴 키
public:
Deer(const char* name, int age, double weight, double height, double antlersLength); // 생성자
char* getDeerName() const; // 사슴 이름 getter
int getDeerAge() const; // 사슴 나이 getter
double getDeerWeight() const; // 사슴 몸무게 getter
double getDeerHeight() const; // 사슴 키 getter
virtual void showDeerInfo() const; // 사슴 정보 보여주기
};
class WaterDeer : public Deer{
private:
double canineLength; // 고라니 송곳니 길이
public:
WaterDeer(const char* WaterDeerName, int WaterDeerAge, double WaterDeerWeight, double WaterDeerHeight, double canineLength);
virtual void showDeerInfo() const; // 가상 함수 재정의를 통한 고라니 정보 보여주기
};
class Elk : public Deer {
private:
double antlerLength; // 순록 뿔 길이
public:
Elk(const char* WaterDeerName, int WaterDeerAge, double WaterDeerWeight, double WaterDeerHeight, double antlerLength);
virtual void showDeerInfo() const; // 가상 함수 재정의를 통한 순록 정보 보여주기
};
가상 함수로 정의해놓게 되면, DeerList 클래스에 고라니와 순록을 따로 둘 필요없이 Deer 포인터를 통해 호출하면 런타임 시간에 알맞은 자식 클래스를 호출하여 메소드를 수행한다. ▼
class DeerList {
private:
Deer* deerList[20]; // 사슴 리스트
int deerNumber; // 사슴 수
public:
DeerList() : deerNumber(0) {};
// 사슴 추가
void addDeer(Deer* Deer) {
deerList[deerNumber++] = Deer;
}
// 모든 사슴 정보 보여주기
void showAllDeerInfo() const {
for (int i = 0; i < deerNumber; i++) {
deerList[i]->showDeerInfo();
}
}
// 사슴 무게 총 합 출력
void showTotalWeight() const {
int sum = 0;
for (int i = 0; i < deerNumber; i++) {
sum += deerList[i] -> getDeerWeight();
}
cout << "total weight: " << sum << endl;
}
// 사슴 리스트 소멸자
~DeerList()
{
for (int i = 0; i < deerNumber; i++) {
delete deerList[i];
}
}
};
이렇게 실행시간에 알맞은 클래스의 메소드를 호출하는 것을 동적 바인딩이라고 한다.
이를 통해 showDeerInfo()라는 하나의 인터페이스를 통해 모든 서브 클래스들의 정보를 받아올 수 있게 된다.
마치며
위와 같이 하나의 인터페이스를 정하고 이에 맞추어 서브 클래스들을 개발하게 되면, 유지보수가 용이해진다.
객체지향 프로그래밍이라는 수업을 들었을 때는 overriding이나 동적 바인딩과 같은 것을 어디에 쓰는지 전혀 이해가 안되었는데, 소프트웨어 공학에서 이에 대한 이야기가 자세히 나와서 이제야 이해가 된 느낌이다.
'CS > 소프트웨어 공학' 카테고리의 다른 글
13. Use Case Realization (0) | 2024.01.31 |
---|---|
12. Configuration and Version Management (0) | 2024.01.31 |
10. 객체 지향 프로그래밍 (Object-Orientation Programming) (0) | 2024.01.31 |
9. 요구사항 문서화 (0) | 2024.01.29 |
8. 요구사항 포착 (0) | 2024.01.26 |