객체지향프로그래밍(15) - 상속(5), 다중 상속

다중 상속의 특징과 문제

Featured image

🔚 짧게 하는 복습

✅ 1. is-a 관계의 정의와 예시를 안다.

✅ 2. has-a 관계의 정의와 예시를 안다.

✅ 3. 계층 구조의 정의를 안다

혹시 기억이 안 난다면, 다시 돌아가자


객체지향 프로그래밍은 현실 세계의 모습들을 최대한 비슷하게 하려고 만들어졌다고 했다.

그런데 현실 세계에서 is-a 관계든 has-a 관계든 한 가지의 종류로만 정의되는가?

나라는 존재는 동물이기도 하면서, 학생이기도 하다.

동물과 학생은 is-a 관계라고 크게 볼 수도 있지만, 공통되는 성질을 공유한다고 하기에는 너무 다른 클래스이다.

만약 나라는 존재가 동물의 성질을 상속하면서 학생이라는 성질도 상속할 수 있을까?

C++에서는 가능하다. 다중 상속에 대해 알아보겠다.


다중 상속이란?

다중 상속이란 두 가지 이상의 상위 클래스들을 상속받는 것을 의미한다.

이게 무슨 말일까? Class Diagram으로 살펴보자.

여기서 나오는 C는 상위 클래스의 A와 B 두 클래스의 모든 성질을 상속받게 된다.

그리고 클래스 C는 클래스 A, B를 다중 상속한다고 표현한다.

예시 코드를 작성해보자.

코드를 보면 Minsu라는 클래스가 animal과 human 클래스의 모든 성질을 상속받은 것을 알 수 있다.

그리고 생성자 부분이 조금 헷갈릴 수 있는데, 우리가 배운 원칙을 적용하여 그대로 해석하면 된다.

처음에 객체의 클래스를 가서 원하는 생성자를 찾고, 있다면 그 생성자를 순서대로 실행시켜 주면 된다.

만약 생성자가 해당하는 것이 없다면, 위로 가서 상위 클래스의 디폴트 생성자를 호출하면 되는 것이다.

언제나 자신이 속한 클래스에서부터 아래에서 위로, 이 원칙만 알면 된다.


다중 상속의 장점

여러 부모 클래스로부터 상속받아서 다양한 기능을 통합할 수 있다.

이게 얼마나 큰 장점이냐 하면, 주요하게 쓰이는 메소드를 따로 만들어서 클래스로 4~5개 정도 만들어 놓았다고 하자.

그 뒤에 파생되는 모든 클래스를 이 조합 4~5개를 계속 다중 상속시켜서 재사용성을 높일 수 있다.

이야기만 들으면 엄청나게 좋은 방법 같다.

하지만, 오류가 나기 쉽고 다루기 어렵기에 몇몇 언어에서는 자체적으로 금지해놓고 있다. (읽어볼 거리 참고)

어떤 문제가 발생할 수 있는지 알아보자.


다이아몬드 문제

다중 상속의 가장 유명한 문제점으로 다이아몬드 문제라는 것이 있다.

A 클래스를 B, C가 상속하고, D 클래스가 B, C를 다중 상속하는 구조를 가진다고 해보자.

아래와 같은 Class Diagram을 가지게 된다.

이렇게 됐을 때, 간단하게 문제를 보여주는 예시 코드를 작성해보겠다.

class A {
public:
    A() { std::cout << "A constructor called" << std::endl; }
};

class B : public A {
public:
    B() { std::cout << "B constructor called" << std::endl; }
};

class C : public A {
public:
    C() { std::cout << "C constructor called" << std::endl; }
};

class D : public B, public C {
public:
    D() { std::cout << "D constructor called" << std::endl; }
};

D의 생성자를 호출하면, A 클래스의 생성자는 무슨 순서로 호출되어야 할까? 라는 질문이 다이아몬드 문제이다.

이게 무슨 말인가…. 생각해보자.

클래스 D는 클래스 B와 클래스 C를 다중 상속받았다.

이는 클래스 D가 클래스 B와 클래스 C의 모든 멤버들을 포함하게 되는데, 이러한 멤버들은 각각의 생성자를 통해 초기화되어야 한다.

클래스 D의 생성자가 호출되면, 클래스 B와 클래스 C의 생성자들이 호출되어 해당 클래스들의 멤버 변수들이 초기화된다.

이후 클래스 D의 생성자가 실행되어 클래스 D의 멤버 변수들이 초기화된다.

따라서 클래스 D의 생성자가 호출되기 위해서는 먼저 클래스 B와 클래스 C의 생성자가 호출되어야 하며, 이것이 다이아몬드 상속 구조에서의 생성자 호출 순서이다.

그런데 여기서 중요한 점은 B와 C가 A를 상속하고 있다는 사실이다.

만일 B가 먼저 호출되어 A를 생성하면, C에서 A를 다시 생성할 필요가 없을까?

그렇다고 C가 먼저 호출되어 A를 생성하면, B에서는 A를 다시 생성할 필요가 없을까?

아니면 둘 다 호출되어야할까? 그렇다면 무슨 순서로 호출이 되어야 할까?

정답은 어떤 순서이든 B에서도 A가 호출되고 C에서도 다른 A가 호출된다.

즉, 다른 A 2개를 D에서 한 번에 상속받아야 한다.

그렇기에 프로그램에서는 무엇을 상속받아야 하는지 결정할 수 없고, 오류를 나타내는 것이다.

이는 거대한 프로그램에서는 의도치 않은 큰 버그를 만들어 낼 수 있다.


메소드의 모호함

다이아몬드 같은 구조적인 문제가 아니더라도, 충분히 실수의 여지가 많다.

아래와 같은 코드를 보자.

class dog{
    public :
    void bark() {std::cout<<"bow wow"<<std::endl;}
};

class cat{
    public :
    void bark() {std::cout<<"meow meow"<<std::endl;}
};

class pet : public dog, public cat{
};

int main(){
    pet navi;

    navi.bark();
}

이 코드에서 navi의 bark는 무슨 메소드를 호출해야 할까?

자신에게 없으니 상위 클래스의 메소드를 봐야 하는데 dog일까 cat일까?

실제로 실행해보면 이렇게 나온다.

error: request for member 'bark' is ambiguous

컴퓨터도 모호하기 때문에, 이렇게 작성하지 말라는 뜻이다.

물론 이런 경우에는 범위 지정자 ::를 통해서

dog::bark()
//혹은 cat::bark()

로 설정할 수는 있다.

하지만 여전히 코드의 복잡도 증가, 가독성 저하, 유지보수 방해 등 많은 문제점이 있다.


그래서 다중 상속은 쓰면 안 됩니까?

당연히 아니다, 다중 상속은 C++의 특징 중 하나이고 이미 많은 곳에서 다중 상속의 방법을 사용하고 있다.

하지만 객체지향의 설계에 미숙한 우리가 아직은 다루기 어려운 것도 사실이다.

아무래도 실수해서 버그를 만들어내기 딱 좋은 구조이다 보니 당분간, 적어도 이 강의를 하는 동안만이라도 자제하자.


📖 오늘의 핵심(다 알기 전까지는 넘어가지 말자❗)

✅ 1. 다중 상속의 정의와 사용법을 안다.

✅ 2. 다중 상속의 특징 중 하나인 다이아몬드 문제를 안다.

✅ 3. 다중 상속을 쓰기 어려운 이유를 안다.

⚠️ 일단은 최대한 다중 상속을 하지 않는 OOD(Object Oriented Design)을 만들려고 노력하자.

💣 과제, 없음

🔜 더 공부해보기,

읽어볼 거리(1) - 자바는 다중 상속을 허용하지 않습니다.

읽어볼 거리(2) - 다중 상속과 더 다양한 OOD 패턴