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

[PYTHON] 다중 상속 모델의 독성, MRO 해결 방법과 3가지 결정적 차이 분석

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

MRO(Method Resolution Order)
MRO (Method Resolution Order)

 

객체 지향 프로그래밍(OOP)에서 다중 상속은 양날의 검과 같습니다. 파이썬은 유연성을 극대화하기 위해 다중 상속을 허용하지만, 상속 계층이 깊어지고 복잡해질수록 어떤 부모 클래스의 메서드를 먼저 호출할 것인지에 대한 논리적 충돌이 발생합니다. 이를 해결하기 위해 파이썬은 **MRO(Method Resolution Order)**라는 규칙을 사용하며, 그 배후에는 **C3 선형화(C3 Linearization)** 알고리즘이 존재합니다. 하지만 숙련된 개발자조차 MRO의 작동 방식을 오해하여 런타임에 예측 불가능한 버그를 만들거나, TypeError: Cannot create a consistent method resolution order라는 치명적인 에러를 마주하곤 합니다. 본 포스팅에서는 다중 상속의 미궁 속에서 안전하게 코드를 설계하는 방법과 MRO 문제를 해결하는 7가지 실전 전략을 다룹니다.


1. MRO와 다이아몬드 상속의 핵심 차이 및 문제 해결 요약

파이썬의 상속 결정 방식이 구버전과 신버전에서 어떻게 달라졌는지, 그리고 실무에서 마주하는 핵심 차이점을 표로 정리했습니다.

비교 항목 구형 방식 (Depth-First) 신형 방식 (C3 Linearization) 실무적 영향 및 해결책
탐색 알고리즘 단순 깊이 우선 탐색 C3 선형화 (C3 Linearization) 상속 순서의 단조성(Monotonicity) 보장
다이아몬드 문제 최상위 클래스가 중복 호출됨 최상위 클래스가 마지막에 단 한 번 호출 super() 호출의 안정성 확보
제약 사항 거의 없음 (순서 꼬임 발생 가능) 일관성 없는 상속 시 에러 발생 상속 계층 설계 시 논리적 타당성 검증 강제
메서드 호출 순서 좌측 부모 우선 끝까지 탐색 로컬 우선순위와 단조성 결합 mro() 메서드를 통해 런타임 확인 가능

2. 실무 개발자를 위한 MRO 문제 해결 및 설계 Sample Examples (7+)

복잡한 상속 구조에서 발생할 수 있는 문제를 진단하고 해결하는 7가지 이상의 실전 사례입니다.

Ex 1. MRO 확인과 기본 다이아몬드 상속 구조

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

class B(A):
    def greet(self):
        print("Hello from B")
        super().greet()

class C(A):
    def greet(self):
        print("Hello from C")
        super().greet()

class D(B, C):
    def greet(self):
        print("Hello from D")
        super().greet()

# MRO 확인: D -> B -> C -> A -> object
print(D.mro())
D().greet()
# 결과: D, B, C, A 순서로 호출됨 (A가 마지막에 한 번만 호출되는 것이 핵심)

Ex 2. "Cannot create a consistent MRO" 에러 해결 방법

# 잘못된 상속 순서 예시 (에러 유발)
class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass

try:
    # A는 (X, Y) 순서인데 C가 (A, B)를 상속받으려 하면
    # B의 (Y, X) 순서와 충돌하여 MRO를 생성할 수 없음
    class C(A, B): pass
except TypeError as e:
    print(f"MRO Conflict: {e}")

# 해결책: 상속받는 부모 클래스들의 내부 상속 순서를 일치시켜야 함

Ex 3. Mixin 클래스를 활용한 MRO 제어

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

class BaseHandler:
    def handle(self):
        print("Handling basic request")

class AdvancedHandler(LoggingMixin, BaseHandler):
    def handle(self):
        self.log("Starting handle")
        super().handle()

# Mixin을 상속 리스트 앞에 배치하여 우선순위 할당

Ex 4. super()와 인자 전달의 문제점 (협력적 다중 상속)

class Base:
    def __init__(self, **kwargs):
        # MRO의 마지막 클래스는 인자를 받지 않는 object.__init__을 호출하므로
        # 남은 인자가 없도록 처리해야 함
        pass

class Component(Base):
    def __init__(self, name, **kwargs):
        self.name = name
        super().__init__(**kwargs)

class Position(Base):
    def __init__(self, x, y, **kwargs):
        self.x = x
        self.y = y
        super().__init__(**kwargs)

class Player(Component, Position):
    def __init__(self, name, x, y):
        super().__init__(name=name, x=x, y=y)

p = Player("Hero", 10, 20)
print(f"Player {p.name} at {p.x}, {p.y}")

Ex 5. __mro__ 속성을 활용한 동적 가속기(Dispatcher) 설계

def get_handler(obj):
    # 객체의 MRO를 순회하며 적절한 처리기를 동적으로 찾는 패턴
    for cls in type(obj).mro():
        if cls.__name__ == "SpecialHandler":
            return cls.process
    return None

Ex 6. 다중 상속 시 의도적인 특정 메서드 차단 (Shadowing)

class Forbidden:
    def action(self):
        raise NotImplementedError("Action is blocked in this subclass")

class Working:
    def action(self):
        print("Performing action")

class LimitedSub(Forbidden, Working):
    # Forbidden이 앞에 있으므로 Working.action은 가려짐
    pass

Ex 7. 추상 베이스 클래스(ABC)와 MRO의 상호작용

from abc import ABC, abstractmethod

class Interface(ABC):
    @abstractmethod
    def run(self): pass

class Implementation(Interface):
    def run(self):
        print("Running implementation")

class CustomApp(Implementation):
    def run(self):
        print("CustomApp before")
        super().run()
# ABC를 상속받아도 MRO 규칙은 동일하게 적용되어 구체 클래스를 먼저 찾음

3. 복잡한 모델 구조에서 MRO가 일으키는 3가지 실무적 문제

  1. 예기치 않은 부모 메서드 스킵: super()를 사용하지 않는 클래스가 계층 구조 중간에 끼어들면, 그 이후의 MRO 체인이 끊어져 상위 부모 클래스의 메서드들이 실행되지 않는 결함이 발생합니다.
  2. 인자 전달 불일치: super().__init__()을 호출할 때 각 부모 클래스가 기대하는 인자가 다르면, MRO 순서에 따라 엉뚱한 클래스에 인자가 전달되어 TypeError가 발생합니다. 이는 **kwargs를 활용한 협력적 상속으로 해결해야 합니다.
  3. 성능 저하: 상속 계층이 극도로 깊거나 복잡한 다중 상속을 사용하는 경우, C3 알고리즘이 선형화된 리스트를 계산하는 데 시간이 소요되며, 메서드 호출 시마다 MRO 리스트를 검색하는 미세한 오버헤드가 발생합니다.

4. 결론: 안전한 다중 상속 설계를 위한 해결책

다중 상속은 파이썬이 제공하는 강력한 도구이지만, 그 내부의 **MRO 작동 원리를 모르는 상태에서의 사용은 '기술 부채'를 넘어선 '런타임 폭탄'**과 같습니다. 복잡한 구조를 설계할 때는 다음 원칙을 준수하십시오.

  • 가능하면 다중 상속 대신 컴포지션(Composition)을 사용하십시오.
  • 다중 상속이 불가피하다면, 기능을 담당하는 Mixin 클래스를 상속 목록의 앞에 배치하십시오.
  • 모든 클래스의 __init__에서 **kwargs를 받아 super()에 전달하는 협력적 상속 패턴을 유지하십시오.

오늘 살펴본 7가지 사례와 C3 알고리즘의 특성을 이해한다면, 아무리 복잡한 클래스 계층 구조에서도 논리적 일관성을 잃지 않는 견고한 아키텍처를 구축할 수 있을 것입니다.


참고 출처

  • Python Standard Library Documentation - The Python 2.3 Method Resolution Order
  • C3 Linearization Algorithm - Wikipedia and original paper by Barrett et al.
  • "Fluent Python" (2nd Edition) - Luciano Ramalho (O'Reilly Media)
  • Effective Python: 90 Specific Ways to Write Better Python - Brett Slatkin
728x90