죽음의 다이아몬드(DDD, the Deadly Diamond of Death) - soo:bak
작성일 :
죽음의 다이아몬드(DDD, the Deadly Diamond of Death)
프로그래밍에서 다중 상속
을 사용할 때 발생할 수 있는 복잡한 문제를 지칭하는 용어.
특히, C++
와 같이 다중 상속을 허용하는 언어에서 다중 상속을 사용할 때 자주 발생하는 문제.
죽음의 다이아몬드 란?
죽음의 다이아몬드
는 클래스 상속 구조에서,
‘한 클래스가 두 개 이상의 클래스로부터 상속을 받고, 이러한 기반 클래스들이 공통의 조상을 가지고 있을 때’ 발생하는 문제이다.
예를 들어, 다음과 같은 클래스 구조에서 :
1
2
3
4
5
Base
/ \
A B
\ /
C
여기서, 클래스 C
는 기반 클래스 A
와 기반 클래스 B
로부터 상속을 받고,
클래스 A
와 클래스 B
는 기반 클래스 Base
로부터 상속을 받는다.
이 때, 만약 Base
클래스에 soobak
이라는 함수가 정의되어 있고, C
클래스의 객체에서 이 함수를 호출하려고 하면,
C
가 A
의 soobak
을 호출해야 하는지, 아니면 B
의 soobak
을 호출해야하는지 명확하지가 않다.
문제의 심화
이 문제는 단순히 ‘어떤 함수를 호출할 것인가?’ 에 대한 결정에 대해서 뿐만 아니라,
Base
클래스의 인스턴스
가 C
객체 내에 두 번 생성될 가능성을 내포하고 있다는 점에서 더욱 큰 문제가 된다.
이는, 리소스 낭비와 데이터의 불일치 등 다양한 문제들을 초래할 수 있으며, 프로그램의 예측 불가능한 동작을 일으킬 수 있다.
C++ 에서의 해결 방법
C++
에서는 이러한 문제를 해결하기 위해 가상 상속(Virtual Inheritance)
을 도입하였다.
가상 상속
을 사용하면, 공통의 조상 클래스가 다중 상속 구조에서 단 한 번만 인스턴스화 된다.
[가상 상속에 대한 예시 코드]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {
public :
void soobak() {
// 구현...
}
};
class A : virtual public Base { // virtual 키워드로 가상 상속
// A 클래스의 구현
};
class B : virtual public Base { // virtual 키워드로 가상 상속
// B 클래스의 구현
};
class C : public A, public B{
// C 클래스의 구현
};
: 여기서, A
와 B
클래스는 Base
로부터 가상으로 상속을 받는다.
이로 인해, C
객체 내에서 Base
클래스의 단일 인스턴스만 존재하게 되며,
C
객체를 통해 Base
의 함수에 접근할 때 모호함이 해결된다.
가상 상속의 주의점
가상 상속을 사용할 때는 몇 가지 주의점이 있다.
우선, 첫 번째로 가상 상속은 일반 상속에 비하여 오버헤드가 더 크므로, 반드시 필요한 경우에만 사용해야 한다.
두 번째로, 가상 기반 클래스의 생성자
는 가장 파생된 클래스에서만 직접 호출할 수 있다
는 점이다.
이는 가상 상속 구조에서 기반 클래스의 초기화 순서를 제어하기 위함이다.
다중 상속과 함수 호출 모호성
다중 상속 구조에서 특정 함수를 호출할 때, 상위 클래스의 동일한 이름의 함수가 존재하면 컴파일러는 어떤 함수를 호출해야 할지 결정하지 못해 모호성이 발생한다.
예를 들어, 클래스 A
와 클래스 B
가 공통 조상 클래스 Base
로부터 동일한 함수 soobak
을 상속받았을 때,
파생 클래스 C
에서 soobak
을 호출하면 컴파일러는 A::soobak
또는 B::soobak
중 어느 것을 선택해야 할지 알 수 없다.
이러한 모호성은 컴파일 타임에 오류를 발생시키며, 이는 스택 프레임 자체의 문제가 아니라 컴파일러가 올바른 함수를 선택하지 못하는 문제이다.
따라서, 다중 상속을 사용할 때는 함수 호출의 모호성을 해결하기 위해 명시적인 함수 지정(예: A::soobak
)이나 가상 상속과 같은 메커니즘을 사용해야 한다.
죽음의 다이아몬드와 메모리 문제
‘죽음의 다이아몬드’ 구조에서는 파생 클래스가 두 상위 클래스를 통해 공통 조상 클래스의 동일한 함수나 데이터를 상속받을 때 문제가 발생한다.
예를 들어, 클래스 C
가 클래스 A
와 B
를 통해 공통 조상 클래스 Base
를 상속받는 경우,
가상 상속을 사용하지 않으면 Base
의 인스턴스가 A
와 B
각각을 통해 두 번 생성된다.
이로 인해 C
객체 내에 Base
의 데이터 멤버가 두 개 존재하게 되어 메모리가 낭비되고,
두 복사본이 서로 다른 상태를 가질 수 있어 데이터 불일치 문제가 발생할 수 있다.
또한, Base
의 함수를 호출할 때 어느 복사본의 함수를 호출해야 할지 모호해지며, 이는 컴파일 타임 오류로 이어진다.
이러한 문제는 스택 프레임의 문제가 아니라, 컴파일러가 함수 호출을 해결하지 못하거나 메모리 중복으로 인한 비효율성에서 비롯된다.
가상 상속과 함수 호출의 명확성
가상 상속은 공통 조상 클래스의 단일 인스턴스만 유지되도록 하여 ‘죽음의 다이아몬드’ 문제를 해결한다.
예를 들어, A
와 B
가 virtual public Base
를 통해 Base
를 상속받으면,
C
객체 내에는 Base
의 단일 인스턴스만 존재한다.
이로 인해 Base
의 함수나 데이터에 접근할 때 모호성이 사라지고, 메모리를 효율적으로 사용할 수 있다.
가상 상속을 사용할 때, 컴파일러는 가상 기본 포인터(vbptr, Virtual Base Pointer)를 사용하여 Base
인스턴스의 정확한 메모리 위치를 런타임에 참조한다.
vbptr
은 각 중간 클래스(A
, B
)에 포함되어 공통 조상 클래스 Base
의 단일 인스턴스를 가리키며,이를 통해 함수 호출을 명확하게 한다.
결과적으로, 컴파일러는 올바른 함수를 호출하도록 코드를 생성하며, 스택 프레임은 단일 Base
인스턴스에 기반한 정확한 함수 호출 정보를 포함하여 중복이나 혼란 없이 실행 흐름이 유지된다.