CS/Software Engineering

[소프트웨어공학/Software Engineering] Polymorphism 다형성

binaryroot 2026. 4. 10. 11:36
728x90

지난 글에서 밝혔던 바와 같이 이번 시간에는 polymorphism에 대해서 알아보려고 한다.

 

Polymorphism

다형성이라고 하며, 하난의 메시지에 대해 서로 다른 객체가 서로 다른 방식으로 응답할 수 있는 기능이다.

메시지를 보내는 객체는 어느 객체가 받을지 알 필요가 없다.

  • one interface, multiple implementations
  • 연산 오버라이딩(상속) + 동적 바인딩 을 통해 구현된다.

클래스 차원의 encapsulation(캡슐화)를 구현한 구조이다.

  • superclass가 subclass들을 encapsulation 한다.
    • 메시지를 보내는 객체는 그 component 중에서 superclass의 interface만 알고 있으면 된다.
    • 내부의 상속 구조에 대해 전혀 알 필요가 없다. (누가 누굴 상속하고 있는지도 알 필요 없음)

장점

  • 다른 component의 재사용(reuse)에 용이하다.
  • component 단위로 모듈화(module)되어 독립적으로 구현되므로 서로 간의 의존성이 감소한다.
    • 코드의 quality 향상과 maintanance에 유리하다.

 

아래는 Polymorphism의 예시이다.

 

TaxiDriver + TransportationCompany

class TaxiDriver
{
private:
    char driverName[10];
    int driverAge;
    int baseSalary;
    int bonusMoney;

public:
    TaxiDriver(const char* driverName, int driverAge, int baseSalary, int bonusMoney);
    char* getDriverName() const;
    int getDriverAge() const;
    int getSalary() const;
    void showDriverInfo() const;
};

class TransportationComany
{
private:
    TaxiDriver* driverList[50];
    int numDrivers;

public:
    TransportationComany() : numDrivers(0) {}

    void addDriver(TaxiDriver* driver)
    {
        driverList[numDrivers++] = driver;
    }

    void showAllDriversInfo() const
    {
        for (int i = 0; i < numDrivers; i++)
            driverList[i]->showDriverInfo();
    }

    void showTotalSalary() const
    {
        int sum = 0;
        for (int i = 0; i < numDrivers; i++)
            sum += driverList[i]->getSalary();

        cout << "total salary: " << sum << endl;
    }

    ~TransportationComany()
    {
        for (int i = 0; i < numDrivers; i++)
            delete driverList[i];
    }
};

 

BusDriver

class BusDriver
{
private:
    char driverName[10];
    int driverAge;
    int workingHours;
    int payPerHour;

public:
    BusDriver(const char* driverName, int driverAge, int workingHours, int payPerHour);
    char* getDriverName() const;
    int getDriverAge() const;
    int getSalary() const;
    void showDriverInfo() const;
};

 

아래와 같은 구조로 코드가 이루어진 것을 알 수 있다.

 

아래는 상속 구조를 나타내는 코드이다.

728x90
class Driver
{
private:
    char driverName[10];
    int driverAge;

public:
    Driver(const char* driverName, int driverAge);
    char* getDriverName() const;
    int getDriverAge() const;
};

class TaxiDriver : public Driver
{
private:
    int baseSalary;
    int bonusMoney;

public:
    TaxiDriver(const char* driverName, int driverAge, int baseSalary, int bonusMoney);
    int getSalary() const;
    void showDriverInfo() const;
};

class BusDriver : public Driver
{
private:
    int workingHours;
    int payPerHour;

public:
    BusDriver(const char* driverName, int driverAge, int workingHours, int payPerHour);
    int getSalary() const;
    void showDriverInfo() const;
};

 

 

아래의 코드를 확인해보자.

void DriverManager::addDrivers()
{
    TransportationComany transportationComany;

    TaxiDriver* pNewTaxiDriver = NULL;
    BusDriver* pNewBusDriver = NULL;

    pNewTaxiDriver = new TaxiDriver("Kim", 33, 300, 50);
    transportationComany.addTaxiDriver(pNewTaxiDriver);

    pNewTaxiDriver = new TaxiDriver("Lee", 45, 350, 60);
    transportationComany.addTaxiDriver(pNewTaxiDriver);

    pNewBusDriver = new BusDriver("Park", 36, 40, 5);
    transportationComany.addBusDriver(pNewBusDriver);

    pNewBusDriver = new BusDriver("Yoo", 40, 30, 6);
    transportationComany.addBusDriver(pNewBusDriver);

    transportationComany.showAllDriversInfo();
    transportationComany.showTotalSalary();
}

 

  • 코드 의존성 존재
    • 외부 코드(TransportationCompany)에서 inheritance에 참가한 모든 subclass의 구조를 알아야 사용 가능하다.
    • 새로운 subclass 생성 혹은 삭제 시, TransportationCompany class도 수정해야 한다.
  • 모듈화 구조 설계
    • 요구사항이 변경되어 inheritance에 참가한 class들이 수정되더라도 외부 코드는 영향을 받지 않도록 수정이 필요하다.
      • 외부 코드에서는 모든 객체를 Driver class로 취급하도록 수정한다.

→ Polymorphism 구조 필요

 

Polymorphism 구조

  • Operation overriding + dynamic binding 으로 구현된다.
    • operating overriding
      • Subclass가 superclass 함수의 prototype과 동일한 함수를 재정의하는 것이다.
    • dynamic binding
      • runtime 시 포인터 변수가 실제 가리키는 객체 타입을 기준으로 호출할 함수를 결정한다.
        • → 'virtual' 키워드 사용

polymorphism 구조를 따른다면 다이어그램은 아래와 같이 변하게 된다.

 

외부 코드(TransportationCompany class)에 영향이 없고, 유지보수 및 재사용에 용이하도록 코드를 수정해보면 아래와 같다.

void DriverManager::addDrivers()
{
    TransportationComany transportationComany;

    Driver* pNewDriver = NULL;

    pNewDriver = new TaxiDriver("Kim", 33, 300, 50);
    transportationComany.addDriver(pNewDriver);

    pNewDriver = new TaxiDriver("Lee", 45, 350, 60);
    transportationComany.addDriver(pNewDriver);

    pNewDriver = new BusDriver("Park", 36, 40, 5);
    transportationComany.addDriver(pNewDriver);

    pNewDriver = new BusDriver("Yoo", 40, 30, 6);
    transportationComany.addDriver(pNewDriver);

    transportationComany.showAllDriversInfo();
    transportationComany.showTotalSalary();
}

 

이런식으로 polymorphism 구조를 이용한다면 TruckDriver 클래스를 추가 정의 하는 것에 큰 어려움이 없을 것이다.

class TruckDriver
{
private:
    char driverName[10];
    int driverAge;
    int workingHours;
    int truckWeight;

public:
    TruckDriver(const char* driverName, int driverAge, int workingHours, int truckWeight);
    char* getDriverName() const;
    int getDriverAge() const;
    int getSalary() const;
    void showDriverInfo() const;
};

 

최종 polymorphism 구조는 아래와 같다.

 

Polymorphism 의 두번째 예시를 통해서 다형성에 대해서 더 알아보자.

Shape 예제이다.

 

기본 구조에서는 Rectangle, Circle, Ellipse 각각이 독립적으로 존재한다.

class Rectangle { void draw(); };
class Circle { void draw(); };
class Ellipse { void draw(); };

 

이 구조의 문제는 이전 Driver 예제와 동일하다.

각 타입을 따로 관리해야하며, 새로운 도형이 추가될 때마다 코드 수정이 불가피하다.

 

이를 해결하기 위하여 공통 부모 클래스 Shape를 정의한다.

class Shape {
public:
    virtual void draw() = 0;
};

그리고 각 도형은 이를 상속받아 구현한다.

class Rectangle : public Shape {
public:
    virtual void draw();
};

class Circle : public Shape {
public:
    virtual void draw();
};

class Ellipse : public Shape {
public:
    virtual void draw();
};

 

핵심은 draw() 함수가 virtual 함수라는 점이다.

이를 통해서 동적 바인딩이 발생하는 것을 알 수 있다.

Shape* shapes[5];

shapes[0] = new Circle();
shapes[1] = new Ellipse();
shapes[2] = new Rectangle();
for (int i = 0; i < num_shapes; i++)
    shapes[i]->draw();

 

  • 배열 타입은 Shape*
  • 실제 객체는 Circle, Rectangle, Ellipse 이다.

하나의 인터페이스로 다양한 객체를 처리할 수 있다.

새로운 도형.. 예를 들어 Triangle이 추가되더라도, 기존 코드 수정 없이 확장이 가능하다.

 

 

polymorphism에 대해서 예시와 함께 알아보았다.

개발을 할 때 그냥 무작정 코드를 써내려 갔었는데, 다형성을 지키면서 개발을 했더라면 개발 속도도 줄고, 재사용이 쉬웠을 것 같다는 아쉬움이 생각나는 개념이었다.

 

다음 글에서는 객체지향의 장점과 추상클래스/인터페이스 에 대해서 정리해보고자 한다.

728x90