[Fluent Python] (14장 상속: 득과 실) Python 다중 상속(Multiple Inheritance)과 MRO(Method Resolution Order) 완벽 이해하기 (260728)

WonTerry·2026년 7월 28일

Python

목록 보기
21/28

Python 다중 상속(Multiple Inheritance)과 MRO(Method Resolution Order) 완벽 이해하기

객체지향 프로그래밍(OOP)에서 다중 상속(Multiple Inheritance)은 하나의 클래스가 여러 부모 클래스로부터 기능을 상속받는 기능이다.

다중 상속은 매우 강력한 기능이지만, 메서드 호출 순서를 잘 이해하지 못하면 예상과 다른 결과가 발생한다. 이러한 문제를 해결하기 위해 파이썬은 MRO(Method Resolution Order)라는 규칙을 사용한다.

이번 글에서는 다중 상속에서 발생하는 다이아몬드 문제(Diamond Problem)와 이를 해결하는 MRO를 초보자도 이해할 수 있도록 살펴본다.


다이아몬드 문제 코드


class Root:           # 가장 최상위 부모 클래스
    def ping(self):
        print(f'{self}.ping() in Root')
        print("-" * 30)

    def pong(self):
        print(f'{self}.pong() in Root')
        print("-" * 30)

    def __repr__(self):     
        cls_name = type(self).__name__
        return f'<instance of {cls_name}>'     # 객체를 출력(print)할 때 보기 좋게 표현하는 함수

class A(Root):      # Root를 상속받는 첫 번째 자식 클래스
    def ping(self):
        print(f'{self}.ping() in A')
        print("-" * 30)
        super().ping()      

    def pong(self):
        print(f'{self}.pong() in A')
        print("-" * 30)
        super().pong()

class B(Root):
    def ping(self):
        print(f'{self}.ping() in B')
        print("-" * 30)
        super().ping()

    def pong(self):
        print(f'{self}.pong in B')
        print("-" * 30)

class Leaf(A, B):
    def ping(self):
        print(f'{self}.ping() in Leaf')
        print("-" * 30)
        super().ping()

실행

leaf1 = Leaf()
leaf1.ping()


<instance of Leaf>.ping() in Leaf
------------------------------
<instance of Leaf>.ping() in A
------------------------------
<instance of Leaf>.ping() in B
------------------------------
<instance of Leaf>.ping() in Root
------------------------------

1. 다중 상속(Multiple Inheritance)

일반적인 단일 상속은 부모가 하나뿐이다.

class Parent:
    pass

class Child(Parent):
    pass

반면 다중 상속은 부모가 둘 이상이다.

class Parent1:
    pass

class Parent2:
    pass

class Child(Parent1, Parent2):
    pass

이처럼 하나의 클래스가 여러 부모의 기능을 동시에 사용할 수 있다.


2. 다이아몬드 문제(Diamond Problem)

다중 상속에서 가장 유명한 문제가 다이아몬드 문제(Diamond Problem)이다.

상속 구조는 다음과 같다.

        Root
       /    \
      A      B
       \    /
        Leaf

LeafAB를 모두 상속받고 있으며, AB는 모두 Root를 상속받는다.

이 구조를 그림으로 보면 다이아몬드 모양이 되기 때문에 Diamond Problem이라고 부른다.


3. 초보자가 가장 많이 하는 생각

대부분 처음에는 다음과 같이 생각한다.

Leaf.ping()

        │
   ┌────┴────┐
   ▼         ▼
A.ping()   B.ping()
   │         │
   ▼         ▼
Root      Root

즉,

  • A가 Root를 호출
  • B도 Root를 호출

하므로

Root의 메서드가 두 번 실행될 것 같다.

하지만 실제 실행 결과는 그렇지 않다.


4. 예제 코드

class Root:
    # 가장 최상위 부모 클래스
    def ping(self):
        print("ping() in Root")


class A(Root):

    def ping(self):
        print("ping() in A")

        # 부모 클래스를 호출하는 것이 아니라
        # MRO에서 다음 클래스를 호출한다.
        super().ping()


class B(Root):

    def ping(self):
        print("ping() in B")

        # 역시 MRO의 다음 클래스를 호출한다.
        super().ping()


class Leaf(A, B):

    def ping(self):
        print("ping() in Leaf")

        # MRO에 따라 A의 ping()이 호출된다.
        super().ping()


leaf = Leaf()
leaf.ping()

5. 실행 결과

ping() in Leaf
ping() in A
ping() in B
ping() in Root

놀랍게도

ping() in Root

한 번만 출력된다.


6. 왜 Root는 한 번만 실행될까?

많은 사람들이 상속 구조를 트리(Tree)처럼 생각한다.

Leaf
├── A
│     └── Root
└── B
      └── Root

이렇게 생각하면 Root가 두 번 실행되는 것이 맞다.

하지만 파이썬은 이렇게 동작하지 않는다.

파이썬은 메서드를 호출하기 전에 MRO(Method Resolution Order)를 계산한다.


7. MRO란 무엇인가?

MRO(Method Resolution Order)

파이썬이 메서드를 어떤 순서로 탐색하고 호출할지를 미리 계산한 목록

이다.

다음 코드를 실행해 보자.

print(Leaf.__mro__)

또는

print(Leaf.mro())

실행 결과는

(
Leaf,
A,
B,
Root,
object
)

이다.

즉,

파이썬은 이미 다음과 같은 호출 순서를 만들어 놓는다.

Leaf
 ↓
A
 ↓
B
 ↓
Root
 ↓
object

여기에는 Root가 한 번만 존재한다.


8. super()는 부모를 호출하는 것이 아니다

많은 책에서는

super() = 부모 클래스 호출

이라고 설명한다.

하지만 이는 단일 상속에서는 맞는 설명이지만,

다중 상속에서는 정확하지 않다.

보다 정확한 설명은

super()는 MRO에서 다음 차례의 클래스를 호출한다.

이다.

예를 들어

Leaf에서

super().ping()

을 호출하면

Leaf
 ↓
A

가 된다.

A에서

super().ping()

을 호출하면

A
 ↓
B

가 된다.

여기서 많은 초보자가 놀란다.

"A의 부모는 Root인데 왜 B가 호출되지?"

그 이유는 super()는 부모가 아니라 MRO의 다음 클래스를 호출하기 때문이다.

마지막으로

B에서

super().ping()

을 호출하면

B
 ↓
Root

가 된다.

결국 호출 순서는

Leaf
 ↓
A
 ↓
B
 ↓
Root

가 된다.


9. 호출 흐름

leaf.ping()

        │
        ▼
Leaf.ping()
        │
        ▼
super()
        │
        ▼
A.ping()
        │
        ▼
super()
        │
        ▼
B.ping()
        │
        ▼
super()
        │
        ▼
Root.ping()

한 번도 분기되지 않고,

하나의 직선으로 이동한다.


10. MRO가 필요한 이유

만약 MRO가 없다면

Leaf
├── A
│     └── Root
└── B
      └── Root

처럼 Root가 두 번 실행될 수도 있다.

이는

  • 같은 초기화 코드가 두 번 실행되고
  • 같은 자원이 두 번 생성되며
  • 같은 작업이 중복 수행되는 문제를 발생시킨다.

이를 다이아몬드 문제(Diamond Problem)라고 한다.

파이썬은 C3 Linearization 알고리즘을 이용하여

Leaf
 ↓
A
 ↓
B
 ↓
Root

라는 하나의 선형 순서를 만들고,

각 클래스가 단 한 번만 실행되도록 보장한다.


11. 핵심 정리

  • 다중 상속은 하나의 클래스가 여러 부모 클래스를 상속받는 기능이다.
  • 다중 상속에서는 다이아몬드 문제(Diamond Problem)가 발생할 수 있다.
  • 파이썬은 MRO(Method Resolution Order)를 이용하여 메서드 탐색 순서를 미리 계산한다.
  • super()부모 클래스를 호출하는 것이 아니라 MRO에서 다음 클래스를 호출한다.
  • MRO 덕분에 모든 클래스는 한 번씩만 실행되며, 다이아몬드 문제를 효과적으로 해결할 수 있다.

마무리

처음에는 다중 상속을 보면 상속 구조가 트리처럼 보이기 때문에 메서드 호출도 분기될 것이라고 생각하기 쉽다.

하지만 실제로 파이썬은 상속 트리를 그대로 따라가는 것이 아니라, MRO라는 하나의 선형 호출 순서를 만든 뒤 그 순서를 따라 메서드를 실행한다.

이 원리만 이해하면 super()와 다중 상속이 더 이상 어렵게 느껴지지 않을 것이다.

profile
Hello, I'm Terry! 👋 Enjoy every moment of your life! 🌱 My current interests are Signal processing, Machine learning, Python, Database, LLM & RAG, MCP & ADK, Multi-Agents, Physical AI, ROS2...

0개의 댓글