지난 편에서는 부모 클래스의 메서드를 자식 클래스의 입맛에 맞게 재정의하고 확장하는 메서드 오버라이딩과 super() 키워드의 올바른 사용법을 배웠습니다. 파이썬은 한 부모 클래스로부터만 기능을 물려받는 단일 상속뿐만 아니라, 필요에 따라 둘 이상의 부모 클래스로부터 동시에 기능을 물려받는 '다중 상속(Multiple Inheritance)' 기능을 지원합니다.

C++이나 파이썬 같은 언어에서 제공하는 다중 상속은 여러 클래스의 유용한 기능들을 한 번에 조합할 수 있어 매우 매력적으로 보입니다. 하지만 다중 상속을 무분별하게 사용하다 보면, 프로그래밍 언어 역사에서 유명한 '다이아몬드 상속 문제(The Diamond Problem)'와 마주치게 되며 어떤 부모의 메서드가 실행될지 예측할 수 없는 복잡한 혼란에 빠지게 됩니다.

저 역시 초보 시절, 기능이 탐나 여러 부모 클래스를 무작정 상속받았다가 똑같은 이름의 메서드가 엉뚱한 부모에서 먼저 실행되는 바람에 수백 줄의 코드를 헤매며 버그를 찾았던 기억이 있습니다. 오늘은 다중 상속의 위험성과 이를 파이썬이 어떻게 명쾌하게 해결하는지, 그 핵심 원리인 MRO(Method Resolution Order, 메서드 탐색 순서)를 완벽하게 정리해 드립니다.


1. 다중 상속과 '다이아몬드 상속 문제'란 무엇인가?

다중 상속은 class Child(ParentA, ParentB): 형태로 두 개 이상의 부모 클래스를 괄호 안에 나열하여 선언합니다.

문제는 다음과 같은 구조에서 발생합니다.

  1. 최상위 부모 클래스 A가 존재하고, A에 greeting() 메서드가 있습니다.

  2. B와 C 클래스가 각각 A를 상속받아 greeting() 메서드를 각자의 방식으로 오버라이딩합니다.

  3. 마지막으로 D 클래스가 B와 C를 동시에 다중 상속받습니다.

이때 다이아몬드 모양의 상속 관계가 형성되는데, 만약 D 객체에서 greeting()을 호출하면 파이썬은 B의 메서드를 실행해야 할까요, 아니면 C의 메서드를 실행해야 할까요? 이것이 바로 객체지향 프로그래밍의 고전적인 난제인 '다이아몬드 상속 문제'입니다.


2. 파이썬의 명쾌한 해결책: MRO(Method Resolution Order)

파이썬은 이 모호함을 해결하기 위해 내부적으로 아주 명확한 메서드 탐색 순서(MRO) 규칙을 가지고 있습니다.

파이썬 3부터는 C3 선형화(C3 Linearization) 알고리즘을 도입하여, 어떤 복잡한 다중 상속 구조에서도 겹침 없이 하나의 일관된 메서드 실행 순서를 계산해 냅니다.

실제 코드로 다이아몬드 상속 구조를 만들어 탐색 순서를 확인해 보겠습니다.

Python
class ClassA:
    def greeting(self):
        print("Class A의 인사")

class ClassB(ClassA):
    def greeting(self):
        print("Class B의 인사")

class ClassC(ClassA):
    def greeting(self):
        print("Class C의 인사")

# ClassB와 ClassC를 동시에 상속받는 ClassD
class ClassD(ClassB, ClassC):
    pass

# D 객체 생성 및 테스트
d = ClassD()
d.greeting()  # 출력: Class B의 인사

d.greeting()을 실행하면 Class B의 인사가 출력됩니다. 파이썬이 ClassB를 ClassC보다 먼저 탐색했기 때문입니다.


3. mro() 메서드와 __mro__ 속성으로 탐색 순서 직접 확인하기

내 클래스가 어떤 순서로 부모 메서드들을 탐색하는지 추측할 필요가 없습니다. 파이썬 클래스는 탐색 경로를 보여주는 mro() 메서드와 __mro__ 속성을 기본으로 제공합니다.

Python
# ClassD의 메서드 탐색 순서(MRO) 출력
print(ClassD.mro())

출력 결과를 보면 순서가 다음과 같이 명확히 나옵니다.

[ClassD, ClassB, ClassC, ClassA, object]

파이썬의 MRO 탐색 원칙은 크게 두 가지입니다.

  1. 좌측 우선 원칙: 상속받을 때 괄호 안에 먼저 적은 부모 클래스(ClassB)를 나중에 적은 클래스(ClassC)보다 먼저 탐색합니다.

  2. 깊이보다 너비 우선(부모보다 자식 우선): 부모 클래스(ClassA)로 바로 올라가지 않고, 같은 계층에 있는 자식 클래스들(ClassB, ClassC)을 모두 확인한 뒤 최상위 부모로 이동합니다.

이 규칙 덕분에 파이썬은 다이아몬드 구조에서도 충돌 없이 한 단방향으로만 안전하게 메서드를 탐색해 나갑니다.


4. 다중 상속과 super()가 만나면 일어나는 마법

다중 상속 구조에서 super()를 사용하면 단순히 바로 위 부모를 가리키는 것이 아니라, MRO 순서상 바로 다음에 위치한 클래스를 호출하게 됩니다.

Python
class Base:
    def __init__(self):
        print("Base 생성자")

class ParentA(Base):
    def __init__(self):
        super().__init__()
        print("ParentA 생성자")

class ParentB(Base):
    def __init__(self):
        super().__init__()
        print("ParentB 생성자")

class Child(ParentA, ParentB):
    def __init__(self):
        super().__init__()
        print("Child 생성자")

c = Child()

이 코드를 실행하면 출력 순서는 다음과 같습니다.

  1. Base 생성자

  2. ParentB 생성자

  3. ParentA 생성자

  4. Child 생성자

Child에서 super().__init__()을 부르면 MRO 순서에 따라 ParentA -> ParentB -> Base 순으로 연쇄 호출됩니다. 각 클래스에서 super()를 빠짐없이 써주었기 때문에 Base 생성자가 중복 실행되지 않고 딱 한 번만 안전하게 실행되는 이점을 얻을 수 있습니다.


5. 실무에서의 다중 상속 잔재: 믹스인(Mixin) 패턴

다중 상속이 MRO로 잘 해결된다고 해도, 너무 깊은 다중 상속은 여전히 코드를 읽기 어렵게 만듭니다. 따라서 실무에서는 완전히 대등한 두 클래스를 상속받기보다는 '믹스인(Mixin)' 형태의 제한된 다중 상속을 주로 사용합니다.

  • 믹스인(Mixin): 독자적으로 객체를 생성하지 않고, 다른 클래스에 '특정 조미료 같은 부가 기능(예: JSON 변환 기능, 로깅 기능)'만 덧붙여 제공하기 위해 만드는 작은 클래스입니다.

복잡한 상속 체인을 만들기보다는, 필요한 부가 기능만 믹스인 클래스로 조립하여 사용하는 것이 유지보수 면에서 훨씬 깔끔하고 안전합니다.


핵심요약

  • 다중 상속은 여러 부모로부터 기능을 동시에 물려받는 기법으로, 메서드 호출의 모호성이 발생하는 다이아몬드 상속 문제를 유발할 수 있습니다.

  • 파이썬은 C3 선형화 알고리즘에 기반한 MRO(Method Resolution Order)를 통해 겹침 없는 일관된 탐색 순서를 결정하며, 클래스.mro()로 이를 확인할 수 있습니다.

  • 다중 상속 시 super()는 MRO 체인의 다음 클래스를 호출하므로 안전한 연쇄 초기화가 가능하며, 실무에서는 복잡한 상속보다 믹스인(Mixin) 패턴을 권장합니다.