다중 상속을 사용할 때는 복잡성과 유지보수 측면에서 여러 문제를 초래할 수 있어 주의가 필요하다.
다중 상속에서 상위 클래스가 중복되는 경우, 다이아몬드 상속 문제가 발생한다. 예를 들어, 클래스 A를 상속받는 B와 C 클래스가 있고, D 클래스가 B와 C 모두를 상속받는 상황에서는 D가 A의 인스턴스를 두 개 가지게 되어 문제가 발생한다.
이를 해결하기 위해선 virtaul 상속, 즉 가상 상속을 사용하면 되지만, 일반적으로 가상함수를 사용하면 virtual table이 만들어지면서 메모리를 좀 더 사용하듯이, 가상 상속도 마찬가지로 비용이 비싸기 때문에 성능에 영향을 줄 수 있다.
다중 상속을 통해 많은 클래스들을 상속받으면, 클래스 계층 구조가 복잡해진다.
복잡해지더라도 잘 이해하고 쓸 수 있다면 좋겠지만, 사람이 프로그래밍 하다보면 가독성이 낮은 코드를 읽을 때 생산성이 떨어질 수 밖에 없다. 이는 유지보수가 어려워져서 현업에서 불편함이 발생할 수 있다.
여러 상위 클래스에서 동일한 이름의 함수나 변수를 상속받게 되면 이름 충돌이 발생한다. 모호성이 발생하면서 문제가 발생할 수 있고, 명시적인 범위 지정이 필요하여 불편하다.
상위 클래스 간의 동작이 예상하지 못한 방식으로 상호작용할 수 있어서, 클래스의 기능이 예측 불가능하게 변할 수 있다. 특히, 각 상위 클래스가 가상함수를 재정의할 때, 어떤 함수가 호출될 지 예측하기 어렵게 된다.
책에서는 웬만해서 SI로 MI 를 대체할 방법이 존재한다고 한다. 예를 들면 객체 합성과 같은 방식을 이용하면, 다중상속보다 안전하고 유연한 프로그래밍이 가능하다.
그럼에도 다중 상속이 필요하거나, 다중 상속을 사용하기 최적의 경우라고 판단한다면, 인터페이스 클래스를 만들어서 순수 가상 함수로만 구성하여 다중 상속의 복잡성을 줄이는 방법을 사용하도록 하자.
인터페이스 클래스로부터 public 상속 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것이다. 구체적으로 말하자면, 클래스가 여러 인터페이스 클래스를 public 상속하여 각 인터페이스에서 요구하는 기능을 구현하고, 필요할 경우 구현을 돕는 클래스(Helper class)를 private 상속함으로써 내부적인 코드 재사용을 목적으로 하는 경우이다.
class InterfaceA {
public:
virtual void functionA() = 0; // 순수 가상 함수로 인터페이스 정의
};
class InterfaceB {
public:
virtual void functionB() = 0; // 순수 가상 함수로 인터페이스 정의
};
// Helper 클래스
class Helper {
protected:
int calculateComplexValue(int x, int y) {
// 복잡한 계산을 수행한다고 가정
return (x * y) + (x - y) * 2;
}
};
// 다중 상속 클래스
class ConcreteClass : public InterfaceA, public InterfaceB, private Helper {
public:
void functionA() override {
int value = calculateComplexValue(5, 3); // Helper 기능 활용
std::cout << "ConcreteClass::functionA with calculated value: " << value << std::endl;
}
void functionB() override {
std::cout << "ConcreteClass::functionB" << std::endl;
}
};
이처럼 인터페이스는 public을 통해 상속받고, 그 상속받은 인터페이스를 구현하는데 필요한 구현함수를 private으로 받아와 사용하는 경우가 다중상속이 적법한 경우이다.
그럼에도 다중상속은 가능한 피하는 것이 복잡성을 줄이고 단순한 코드를 만들어 좋은 것 같다.