본문으로 건너뛰기

안동민 개발노트

본문 시작

다중 상속과 가상 상속

다중 상속에서 기반 객체가 중복되는 다이아몬드 문제를 재현하고 가상 상속과 생성자 규칙으로 해결합니다.

이전 절 ‘상속의 개념과 구현’에서 다중 상속을 간단히 언급했습니다.

이번 절에서는 다중 상속에서 가장 중요한 이슈인 다이아몬드 상속 문제와, 이를 해결하는 가상 상속(virtual inheritance)을 실제 코드로 다룹니다.

다이아몬드 상속은 같은 기반 클래스가 두 경로로 들어와 이름이 모호해진다

Copier가 Printer와 Scanner를 동시에 상속하면, 두 부모가 각각 Device를 품고 있어 Copier 안에 Device 부분이 두 개 생길 수 있습니다.

  1. Device가 두 번 복사된다

    non-virtual Printer Device copy #1 포함 Scanner Device copy #2 포함 Printer::Device::id Scanner::Device::id Copier c.id가 어느 Device의 id인지 모호하다

  2. 공유 기반은 하나만 남겨야 한다

    해결 방향 Device 공유되어야 할 상태 id virtual public virtual public Printer Device를 공유 경로로 참조 Scanner 같은 Device를 공유 경로로 참조


다이아몬드 상속 문제란?

다음과 같은 상속 구조를 생각해봅시다.

  • DevicePrinter, Scanner가 상속
  • CopierPrinter, Scanner를 모두 상속

이 구조는 상속 그래프가 마름모(다이아몬드) 형태를 이루며, Device가 중복으로 포함될 수 있습니다.

다이아몬드 상속 구조 (문제 상황)
#include <iostream>

class Device {
public:
    int id;
    Device() : id(100) {}
};

class Printer : public Device {};
class Scanner : public Device {};

class Copier : public Printer, public Scanner {};

int main() {
    Copier c;
    // c.id = 10; // 오류: Device::id가 두 경로로 존재하여 모호
}

Copier 안에 Device가 두 개 생기므로, 어떤 id를 의미하는지 컴파일러가 판단할 수 없습니다.


가상 상속으로 문제 해결하기

`virtual public Device`는 최종 객체 안의 Device를 하나로 공유하게 만든다

Printer와 Scanner가 Device를 가상 기반으로 상속하면 Copier 안에는 공유 Device가 하나만 남아 `c.id`가 모호하지 않습니다.

  1. Device 공유 기반 클래스 인스턴스

    1개

  2. Printer 출력 기능

    더한다 Scanner 스캔 기능을 더한다

  3. Copier Printer

    Scanner 기능을 모두 갖지만 Device는 하나만 가진다

코드 위치써야 하는 표현의미
중간 클래스class Printer : virtual public DeviceDevice를 복사하지 말고 공유 기반으로 연결한다.
최종 클래스class Copier : public Printer, public Scanner두 기능을 합치되 Device는 한 번만 가진다.
객체 사용c.id = 7id 경로가 하나라 모호성 없이 접근한다.

기반 클래스 쪽 상속을 virtual로 선언하면, 최종 파생 클래스에서 공유된 하나의 기반 클래스 인스턴스를 사용하게 됩니다.

virtual inheritance로 해결
#include <iostream>

class Device {
public:
    int id;
    Device(int v = 0) : id(v) {
        std::cout << "Device ctor\n";
    }
};

class Printer : virtual public Device {
public:
    Printer() { std::cout << "Printer ctor\n"; }
};

class Scanner : virtual public Device {
public:
    Scanner() { std::cout << "Scanner ctor\n"; }
};

class Copier : public Printer, public Scanner {
public:
    Copier() : Device(42) { // 가상 기반 클래스는 최종 파생 클래스에서 초기화
        std::cout << "Copier ctor\n";
    }
};

int main() {
    Copier c;
    c.id = 7; // 모호성 없음
    std::cout << "id=" << c.id << "\n";
}

핵심은 Printer, Scanner에서 virtual public Device를 쓰는 것입니다.


생성자 호출 순서와 초기화 규칙

가상 상속에서는 생성자 초기화 규칙이 일반 상속과 다릅니다.

  • 가상 기반 클래스(Device)는 최종 파생 클래스(Copier)가 초기화합니다.
  • 중간 클래스(Printer, Scanner)에서 Device(...)를 적어도 최종 객체 생성 시 무시될 수 있습니다.

즉, 가상 기반 클래스 초기화 책임은 최종 파생 클래스에 있다고 기억하면 됩니다.


언제 다중 상속을 써야 할까?

다중 상속은 가능하지만 설계 복잡도를 크게 높입니다.

권장되는 경우

여러 인터페이스 성격의 추상 클래스를 동시에 구현해야 하는 경우.

주의가 필요한 경우

상태(데이터 멤버)를 가진 구체 클래스들을 다중 상속할 때.

대안

상속 대신 합성(Composition)으로 기능을 조합하는 것이 더 단순한 경우가 많습니다.

실제 설계에서는 “다중 상속을 쓸 수 있는가”보다 “중복 상태와 초기화 책임을 통제할 수 있는가”를 먼저 확인해야 합니다.

다음 점검표처럼 다이아몬드 구조, virtual public, 최종 파생 클래스 초기화, 합성 대안을 함께 놓고 판단하면 실수를 줄일 수 있습니다.

다중 상속은 기능 조합보다 중복 상태와 초기화 책임을 먼저 점검한다

두 부모가 상태를 가지면 다이아몬드와 생성 책임이 먼저 문제가 된다.

  1. 인터페이스 조합

    상태 없는 역할만 섞는가

  2. 상태 중복

    부모가 같은 기반 데이터를 갖는가

  3. 초기화 책임

    최종 클래스가 기반을 정하는가

  4. 합성 대안

    멤버로 두는 편이 단순한가

질문괜찮은 신호위험 신호
역할만 섞나?순수 인터페이스 조합구체 부모 둘 다 상태 보유
기반이 겹치나?공통 기반 없음diamond로 멤버 모호
책임이 선명한가?최종 클래스가 초기화중간 부모마다 자원 소유

체크 항목 중 하나라도 애매하다면 다중 상속을 유지하기보다 인터페이스 분리나 합성으로 책임을 나누는 쪽이 더 읽기 쉬운 설계가 됩니다.

가상 상속을 선택했다면 마지막으로 초기화 책임을 확인해야 합니다.

다이아몬드 구조에서는 중간 클래스가 아니라 최종 파생 클래스가 공유 기반 클래스의 생성자를 호출하므로, 객체 생성 순서를 설계도처럼 추적하는 습관이 중요합니다.

가상 기반 클래스는 중간 클래스가 아니라 최종 파생 클래스가 초기화한다

`Printer`와 `Scanner`가 `Device(...)`를 적어도, 최종 객체 `Copier`를 만들 때는 `Copier`가 공유 Device의 생성자 인자를 결정합니다.

  1. Device(42) Copier

    공유 기반을 먼저 초기화

  2. Printer(), Scanner() 중간 클래스 부분

    만든다

  3. Copier() 최종 파생 클래스 멤버

    완성

위치초기화 책임왜 중요한가
Device가상 기반이므로 객체 안에 한 번만 존재id 같은 공유 상태가 두 개로 갈라지지 않는다.
Printer / Scanner자기 멤버만 초기화최종 객체 생성 시 Device 인자를 결정하지 못한다.
CopierCopier() : Device(42)공유 기반 클래스의 실제 초기값을 최종적으로 정한다.

다중 상속 적용 기준 정리

  • 다중 상속은 다이아몬드 문제를 유발할 수 있다.
  • virtual inheritance는 공유 기반 클래스 하나만 유지해 모호성을 제거한다.
  • 가상 기반 클래스 초기화는 최종 파생 클래스가 담당한다.
  • 설계 복잡도가 높아지므로, 필요성과 대안을 함께 검토해야 한다.

다중 상속을 유지할지 고민될 때는 공유 기반 클래스, 가상 상속, 생성자 책임, 합성 대안을 같은 표에서 비교하면 판단이 쉬워집니다.

다중 상속은 diamond, virtual, ctor, compose 네 단어로 최종 판단한다

코드 가능 여부보다 상태 중복과 생성 책임을 표로 확인해야 한다.

  1. diamond

    공통 기반이 두 번 들어오는가

  2. virtual

    공유 기반을 하나로 묶는가

  3. ctor

    최종 파생이 기반을 초기화

  4. compose

    상속보다 멤버 조합이 쉬운가

선택쓸 때다음 확인
그냥 다중 상속상태 없는 역할 조합이름 충돌 없음
virtual 상속공통 기반을 하나만 공유최종 생성자 책임
합성상태와 소유권이 섞임멤버 위임으로 단순화