본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 다중 상속의 미학, MRO 결정 알고리즘과 super() 호출 순서의 3가지 핵심 차이 해결 방법

by Papa Martino V 2026. 4. 2.
728x90

MRO와 C3 Linearization
MRO와 C3 Linearization

 

파이썬(Python)은 다중 상속을 허용하는 강력한 언어입니다. 하지만 여러 부모 클래스를 상속받을 때 발생하는 메서드 호출의 모호성, 즉 '다이아몬드 문제(Diamond Problem)'는 개발자들을 종종 혼란에 빠뜨립니다. 이를 해결하기 위해 파이썬은 MRO(Method Resolution Order)라는 엄격한 순서 결정 알고리즘을 사용합니다. 본 포스팅에서는 단순히 super()가 부모를 부른다는 기초 개념을 넘어, 파이썬 내부에서 C3 Linearization 알고리즘이 어떻게 경로를 계산하는지, 그리고 실무에서 발생할 수 있는 상속 충돌을 어떻게 우아하게 해결하는지 심층적으로 다룹니다.


1. MRO와 C3 Linearization: 내부 동작 원리

파이썬 2.3부터 도입된 C3 Linearization 알고리즘은 다음의 3가지 원칙을 보장합니다.

  • 자식 우선(Child First): 자식 클래스는 항상 부모 클래스보다 먼저 검색됩니다.
  • 단조성(Monotonicity): 부모 클래스들 간의 우선순위가 상속 구조 내에서 일관되게 유지되어야 합니다.
  • 로컬 우선순위(Local Precedence): 상속 목록에 나열된 순서대로 부모 클래스를 방문합니다.

2. MRO와 super() 호출 메커니즘 비교 분석

super()는 단순히 '부모'를 찾는 것이 아니라, 현재 클래스의 MRO 리스트에서 다음 순번을 찾는 함수입니다. 이 미세한 차이가 복잡한 다중 상속 구조에서 실행 순서를 결정짓습니다.

비교 항목 MRO (Method Resolution Order) super() Call Mechanism
정의 클래스 계층 구조의 선형화된 검색 경로 MRO 경로상의 다음 메서드를 호출하는 동적 위임
결정 시점 클래스가 정의되는 시점 (정적 계산) 런타임에 인스턴스의 MRO를 바탕으로 결정
다이아몬드 문제 중복 방문을 방지하는 알고리즘 제공 중복 호출 없이 모든 부모를 정확히 1회 방문
확인 방법 ClassName.mro() 또는 .__mro__ super(CurrentClass, self).method()
주요 특징 상속 선언 시 에러 발생 시(Inconsistent MRO) 차단 협력적 다중 상속(Cooperative Inheritance) 지원

3. 실무 최적화 및 해결을 위한 7가지 Sample Examples

실제 엔지니어링 환경에서 MRO가 어떻게 동작하는지 7가지 실전 코드를 통해 분석합니다.

Example 1: 다이아몬드 상속 구조의 실행 순서 파악

전형적인 다이아몬드 구조에서 super()가 어떻게 중복 호출을 피하는지 보여줍니다.

class A:
    def __init__(self):
        print("A entry")
        print("A exit")

class B(A):
    def __init__(self):
        print("B entry")
        super().__init__()
        print("B exit")

class C(A):
    def __init__(self):
        print("C entry")
        super().__init__()
        print("C exit")

class D(B, C):
    def __init__(self):
        print("D entry")
        super().__init__()
        print("D exit")

# 실행 결과: D -> B -> C -> A 순으로 진입
d = D() 
print(D.mro())

Example 2: 믹스인(Mixin) 클래스와 MRO의 결합

로그 기록이나 권한 체크 등 공통 기능을 믹스인으로 주입할 때의 상속 순서 제어 방법입니다.

class LoggingMixin:
    def log(self, message):
        print(f"[LOG]: {message}")

class BaseWorker:
    def work(self):
        print("Working...")

class SmartWorker(LoggingMixin, BaseWorker):
    def work(self):
        self.log("Starting work")
        super().work()

worker = SmartWorker()
worker.work() # LoggingMixin이 BaseWorker보다 앞에 위치함

Example 3: Inconsistent MRO 에러 해결 방법

상속 순서가 꼬여 파이썬이 클래스 생성을 거부할 때 이를 교정하는 방법입니다.

class X: pass
class Y(X): pass
# class Z(X, Y): pass # TypeError 발생: MRO 순서 모순

# 올바른 해결: 구체적인 자식(Y)을 추상적인 부모(X)보다 앞에 둡니다.
class Z(Y, X): pass 
print(Z.mro())

Example 4: super()에 인자를 전달하는 고급 기법

다중 상속 구조에서 각 부모 클래스가 요구하는 인자가 다를 때 **kwargs를 활용하는 패턴입니다.

class Base:
    def __init__(self, **kwargs):
        print("Base init")

class Left(Base):
    def __init__(self, left_val, **kwargs):
        print(f"Left init: {left_val}")
        super().__init__(**kwargs)

class Right(Base):
    def __init__(self, right_val, **kwargs):
        print(f"Right init: {right_val}")
        super().__init__(**kwargs)

class Combined(Left, Right):
    def __init__(self):
        super().__init__(left_val="L", right_val="R")

# 모든 인자가 MRO를 타고 정확히 전달됨
c = Combined()

Example 5: 특정 부모를 건너뛰는 super() 바인딩

특정 계층을 건너뛰고 상위 부모를 명시적으로 지칭하고 싶을 때(주의해서 사용)의 문법입니다.

class Parent:
    def greet(self):
        print("Hello from Parent")

class Child(Parent):
    def greet(self):
        print("Hello from Child")
        # Parent의 greet를 직접 호출하지 않고 MRO를 우회
        super(Child, self).greet() 

Example 6: 동적 클래스 생성 시 MRO 확인

type() 함수를 사용해 동적으로 클래스를 만들 때 MRO가 어떻게 구성되는지 확인합니다.

DynamicClass = type('DynamicClass', (B, C), {})
print(f"Dynamic MRO: {DynamicClass.mro()}")
# [DynamicClass, B, C, A, object]

Example 7: 추상 클래스(ABC)와 super()의 조합

인터페이스 정의와 기본 구현이 섞인 환경에서 super()를 통해 기본 로직을 호출하는 방식입니다.

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def draw(self):
        print("Drawing Shape Base")

class Circle(Shape):
    def draw(self):
        super().draw() # 추상 메서드의 기본 구현 호출 가능
        print("Drawing Circle")

Circle().draw()

4. 결론: 왜 MRO를 이해해야 하는가?

파이썬의 super()는 단순히 부모 클래스로의 상향 링크가 아니라, 전체 상속 그래프를 일렬로 세운 뒤 '다음 사람'을 찾는 지침서입니다. 이를 이해하지 못하면 다중 상속 시 특정 부모의 메서드가 누락되거나, 의도치 않은 순서로 실행되어 디버깅이 불가능한 '좀비 코드'가 양산될 수 있습니다. 선형화된 순서를 이해하는 것은 대규모 프레임워크(Django, FastAPI 등)의 내부 구조를 이해하는 필수 역량입니다.


5. 참고 문헌 및 자료 출처

  • Python Software Foundation. "The Python 2.3 Method Resolution Order".
  • Raymond Hettinger. "Python’s super() considered super!".
  • Guido van Rossum. "Method Resolution Order in Python 3".
  • C3 Linearization Algorithm (Wikipedia).
728x90