프로그래밍 공부
작성일
2024. 2. 29. 14:27
작성자
WDmil
728x90

9.5 다중 상속

다중 상속은 객체지향 프로그래밍에서 다소 꺼려지는 부분이다. 다중 상속은 복잡하기만 하고 불필요할 수도 있다.


9.5.1 복수 클래스로부터의 상속

문법적으로는 여러 클래스를 동시에 부모로 두는 것이 그리 복잡하지 않다. 다음처럼 베이스 클래스의 이름을 나열하기만 하면 된다.

 

class Baz : pulbic Foo, pulbic Bar
{
	// 기타 정의...
};

이렇게 부모 이름을 나열함으로써 Baz로부터 생성된 객체는 다음과 같은 특징을 가지게 된다.

  • Baz객체는 Foo와 Bar의 모든 public멤버와 메서드를 지원한다.
  • Baz객체는 메서드 구현부에서 Foo와 Bar의 protected멤버와 메서드에 접근할 수 있다.
  • Baz객체는 Foo또는 Bar로 업케스팅 될 수 있다.
  • 새로운 Baz객체를 생성할 때 자동으로 Foo와 Bar의 디폴트 생성자가 호출된다. 호출 순서는 상속 목록에 나열된 순서를 따른다.
  • Baz 객체를 삭제하면 Foo와 Bar의 소멸자가 생성자 호출의 역순으로, 즉 상속 목록에 나열된 순서의 반대 순서로 호출된다.

Dogbird라는 클래스에서 두 개의 부모 클래스, Dog와 Bird를 상속받고 있다. 개(dog)면서 새(brid)인 것은 기괴한 상황이지만, 다중 상속 자체가 기괴하다는 것을 보이려는 의도이다.

class Dog
{
public:
    virtual void bark() { cout << "Woof!" << endl; }
};

class Bird
{
public:
    virtual void chirp() { cout << "Chirp!" << endl; }
};
class DogBird : public Dog, public Bird
{
};

DogBird클래스의 구조는 다음과 같다.

복수의 부모를 가진 클래스 객체라고 해서 특별히 이용 방법이 다르지는 않다. 사실 사용자로서는 클래스가 두 개의 부모를 가졌다는 사실 자체를 몰라도 된다. 알아야 할 것은 두 부모 클래스가 지원하는 프로퍼티와 행동이다. 이 경우 DogBird객체는 Dog와 Bird클래스의 모든 public메서드를 지원한다.

int main()
{
    DogBird myConfusedAnimal;
    myConfusedAnimal.bark();
    myConfusedAnimal.chirp();
}

위 코드의 실행 결과는 다음과 같다.

Woof!
Chirp!

9.5.2 이름 충돌과 모호한 베이스 클래스

다중 상속이 문제를 일으키는 상황은 어렵지 않게 만들 수 있다. 다음 예제는 다중 상속을 이용할 때 드시 고려해야할 특이 상황을 설명한다.


9.5.2.1 모호한 이름

만약 Dog클래스와 Bird클래스가 같은 메서드 eat()를 가졌다면 어떻게 될까? Dog과 Bird는 아무런 관계가 없음으로 어느 한 쪽의 메서드가 다른 쪽 메서드를 오버라이딩 할 수도 없다. 단지 DogBird클래스에 동시에 존재할 뿐이다.

 

사용자가 DogBird객체에 대해 eat()메서드를 호출하지 않는 한 문제가 발생하지 않는다. eat()메서드가 중복되어 있더라도 컴파일은 정상적으로 된다. 하지만 사용자가 eat()메서드를 사용하면 컴파일러가 eat()메서드의 모호함에 대해 에러 메세지를 출력한다.

 

컴파일러 입장에서는 어느 버전의 eat()메서드를 호출해야 할지 알 수 없다. 다음 코드는 이와 같은 모호한 에러를 발생시킨다.

class Dog
{
public:
    virtual void bark() { cout << "Woof!" << endl; }
    virtual void eat() { cout << "The dog has eaten." << endl; }
};

class Bird
{
public:
    virtual void chirp() { cout << "Chirp!" << endl; }
    virtual void eat() { cout << "The bird has eaten." << endl; }
};
class DogBird : public Dog, public Bird
{
};

int main()
{
    DogBird myConfusedAnimal;
    myConfusedAnimal.eat(); // 에러! 모호한 메서드인 eat()를 호출했다.
    return 0;
}

이러한 모호성에 대한 대책은 객체를 업캐스팅하거나 모호성이 해소되도록 스코프 지정 연산자를 사용하는 것이다. 업 캐스팅은 노출하고 싶지 않은 파생 클래스의 특징을 컴파일러로부터 숨겨주고, 스코프 지정 연산자는 프로그래머가 직접 어느 메서드를 이용해야 할지 컴파일러에 알려준다.

 

다음 코드는 Dog버전의 eat()가 호출되게 하고있다.

    static_cast<Dog&>(myConfusedAnimal).eat(); // 슬라이싱, Dog::eat()호출
    myConfusedAnimal.Dog::eat();                // 명시적으로 Dog::eat()호출

파생 클래스의 메서드 구현부에서도 부모 클래스의 이름을 스코프 지정 연산자(::)로 지정함으로써 메서드의 모호성을 해소할 수 있다.

 

예를 들어 DogBird클래스가 eat()를 오버라이딩하면 모호한 메서드로 인한 컴파일 에러를 방지할 수 있다. 이때 메서드 안에서는 어느 부모의 메서드를 호출할지 선택 가능하다.

class DogBird : public Dog, public Bird
{
public:
    void eat()
    {
        Dog::eat(); // 명시적으로 Dog의 eat()메서드 호출
    }
};

모호성을 해소하는 또 다른 방법은 DogBird에서 명시적으로 어떤 버전의 eat()를 상속받을것인지 지정하는 것이다. 다음은 DogBird에 명시적인 상속 메서드 지정을 하는 예이다.

class DogBird : public Dog, public Bird
{
public:
    using Dog::eat; // 명시적으로 Dog버전의 eat()를 상속
};

모호성을 발생시키는 또 다른 방법은 같은 클래스를 두번 상속받는 것이다. 예를 들어 Bird클래스가 어떤 이유로 Dog 클래스를 상속받은 상태에서 DogBird클래스에서 Dog과 Bird를 상속하는 경우다. 이렇게 되면 어느 Dog클래스를 사용해야 할지 모호해지고 컴파일 에러가 발생한다.

class Dog {};
class Bird : public Dog {};
class DogBird : pulbic Bird, pulbic Dog {}; // 에러! Dog클래스가 모호함

모호한 베이스 클래스가 발생하는 상황은 대부분 의도적으로 꾸민 것이거나, 엉망으로 디자인된 클래스 계층에서 비롯된다.

다음 그림은 위 예제의 클래스 계층에서 어느 부분이 모호한지 보여준다.

모호성은 데이터 멤버에도 발생할 수 있다. 만약 Dog과 Bird가 같은 이름의 데이터 멤버를 가지고 있다면 사용자가 해당 멤버에 접근할 떄 컴파일 모호성 에러를 발생시킨다.


9.5.2.2 모호한 베이스 클래스

모호한 베이스 클래스가 발생하는 가장 흔한 시나리오는 부모 클래스들이 같은 부모를 공통으로 가지는 경우다. 예를 들어 다음 그림처럼 Dog와 Bird가 Animal클래스를 공통의 부모 클래스로 가질 수 있다.

이런 종류의 클래스 계층이 C++에서 허용되기는 하지만 이름 충돌이 일어날 가능성이 있다. 예를 들어 Animal클래스가 public메서드 sleep()를 가지고 있다면, 이 메서드는 DogBird객체에서 호출될 수 없다.

 

왜냐하면 컴파일러 입장에서는 Dog의 부모에서 호출해야 할지 Bird의 부모에서 호출해야 할지 알 수 없기 때문이다.

 

이렇게 다이아몬드 형태의 클래스 계층을 가지는 상황에서는 최상위 클래스를 모든 메서드가 퓨어 버추얼인 추상 클래스로 만드는 것이 가장 좋은 방법이다. 그렇게 하면 베이스 클래스에는 호출할 메서드의 구현이 없어지기 때문에, 최소한 베이스 클래스 수준에서는 모호성 문제가 사라진다.

 

다음 예제는 다이아몬드 형태의 클래스 계층을 구현하면서eat() 메서드를 퓨어버추얼로 선언하여 각 파생 클래스가 오버라이딩하여 구현하게 하고있다. DogBird클래스는 여전히 어느 부모의 eat()메서드를 호출해야 할지 명시해야 하지만, Dog과 Bird클래스가 가진 중복 메서드만 처리하면 되고, Dog와 Bird의 공통 부모로 인한 모호성 문제는 더 이상 발생하지 않는다.

class Animal
{
public:
    virtual void eat() = 0;
};
class Dog : public Animal
{
public:
    virtual void eat() override { cout << "The dog has eaten." << endl; }
};

class Bird : public Animal
{
public:
    virtual void chirp() { cout << "Chirp!" << endl; }
    virtual void eat() override { cout << "The bird has eaten." << endl; }
};
class DogBird : public Dog, public Bird
{
public:
    using Dog::eat; // 명시적으로 Dog버전의 eat()를 상속
};

다이아몬드 형태의 클래스 계층에 대응하는 좀 더 잘 정리된 매커니즘으로 버추얼 베이스 클래스가 있다.


9.5.2.3 다중 상속의 이용

지금까지 설명한 문제들을 놓고 보면 프로그래머가 굳이 왜 다중 상속을 이용하려 드는지 궁금할 것이다. 다중 상속이 필요한 가장 흔한 상황은 어떤 클래스 객체가 여기에도 속하고 저기에도 속하는 때다.

 

여러 사물과 is-a관계를 가지는 실세계의 패턴은 코드로 잘 옮겨지지 않는다.

 

다중 상속의 가장 설득력 있고 단순한 사용 예는 막스인(Mixin)클래스를 이용할 떄 이다.

 

또 다른 예로 컴포넌트 기반 클래스를 모델링할 때도 다중 상속이 필요할 수 있다. 비행기 시뮬레이션 을 예로 살펴보면,

Airplain 클래스는 엔진, 동체, 제어부 등의 컴포넌트로 구성된다 Airplane클래스를 구현하는 가장 전형적인 방법은 각 컴포넌트를 개별적인 데이터 멤버로 갖는 것이다. 하지만 다중 상속을 이용할 수도 있다. 즉, Airplane클래스가 엔진 클래스, 동체 클래스, 제어부 클래스 등의 컴포넌트를 상속받음으로써 모든 컴포넌트의 프로퍼티와 행동을 가질 수 있다.

 

이런 식의 패턴은 멀리할 것을 권장하는데, 이런 패턴은 is-a관계에 사용되어야 할 상속 메커니즘을 has-a 관계에 사용함으로써 모호한 has-a관계를 만든다.

 

권장하는 방법은 Airplane클래스가 엔진, 동체, 제어부를 데이터 멤버로 가지는 것이다.

728x90