본문으로 건너뛰기
Advertisement

상속과 super()

**상속(Inheritance)**은 기존 클래스의 속성과 메서드를 재사용하고 확장하는 객체지향 프로그래밍의 핵심 메커니즘입니다. 코드 재사용성을 높이고 "is-a" 관계를 명확하게 표현합니다.


상속 기본

class Animal:
"""기반 클래스(부모 클래스, 슈퍼클래스)"""

def __init__(self, name: str, age: int):
self.name = name
self.age = age

def eat(self) -> str:
return f"{self.name}이(가) 먹습니다."

def sleep(self) -> str:
return f"{self.name}이(가) 잡니다."

def describe(self) -> str:
return f"{self.name} (나이: {self.age})"

def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r}, age={self.age})"


class Dog(Animal):
"""파생 클래스(자식 클래스, 서브클래스)"""

def __init__(self, name: str, age: int, breed: str):
super().__init__(name, age) # 부모의 __init__ 호출
self.breed = breed

def bark(self) -> str:
return f"{self.name}: 왈왈!"

def fetch(self) -> str:
return f"{self.name}이(가) 공을 가져옵니다."

def describe(self) -> str:
"""메서드 오버라이딩: 부모 메서드를 재정의"""
base = super().describe() # 부모 메서드 호출
return f"{base}, 품종: {self.breed}"


class Cat(Animal):
def __init__(self, name: str, age: int, indoor: bool = True):
super().__init__(name, age)
self.indoor = indoor

def meow(self) -> str:
return f"{self.name}: 야옹~"

def describe(self) -> str:
indoor_str = "실내" if self.indoor else "실외"
return f"{super().describe()}, {indoor_str} 고양이"


# 사용 예시
dog = Dog("바둑이", 3, "진돗개")
cat = Cat("나비", 2)

print(dog.eat()) # 바둑이이(가) 먹습니다. (부모 메서드 상속)
print(dog.bark()) # 바둑이: 왈왈! (자식 메서드)
print(dog.describe()) # 바둑이 (나이: 3), 품종: 진돗개

print(cat.meow()) # 나비: 야옹~
print(cat.describe()) # 나비 (나이: 2), 실내 고양이

# isinstance와 issubclass
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True (상속 관계)
print(issubclass(Dog, Animal)) # True
print(issubclass(Cat, Dog)) # False

super() 사용법

super()는 부모 클래스를 참조하는 프록시 객체를 반환합니다. MRO(Method Resolution Order)를 따라 다음 클래스를 참조합니다.

class Vehicle:
def __init__(self, brand: str, model: str, year: int):
self.brand = brand
self.model = model
self.year = year
self._is_running = False

def start(self) -> str:
self._is_running = True
return f"{self.brand} {self.model} 시동을 켰습니다."

def stop(self) -> str:
self._is_running = False
return f"{self.brand} {self.model} 시동을 껐습니다."

def status(self) -> str:
state = "운행 중" if self._is_running else "정지"
return f"{self.brand} {self.model} ({self.year}년) — {state}"


class ElectricVehicle(Vehicle):
def __init__(self, brand: str, model: str, year: int, battery_kwh: float):
super().__init__(brand, model, year) # 부모 초기화
self.battery_kwh = battery_kwh
self._charge_level = 100.0 # 퍼센트

def start(self) -> str:
if self._charge_level < 5:
return "충전이 부족합니다. 충전 후 시도하세요."
result = super().start() # 부모 메서드 결과 활용
return f"{result} (배터리: {self._charge_level:.0f}%)"

def charge(self, kwh: float) -> str:
max_charge = self.battery_kwh - (self._charge_level / 100 * self.battery_kwh)
actual_charge = min(kwh, max_charge)
self._charge_level = min(100.0, self._charge_level + (actual_charge / self.battery_kwh * 100))
return f"{actual_charge:.1f} kWh 충전 완료. 현재: {self._charge_level:.0f}%"

def status(self) -> str:
base = super().status()
return f"{base} | 배터리: {self._charge_level:.0f}%"


class HybridVehicle(ElectricVehicle):
def __init__(self, brand: str, model: str, year: int, battery_kwh: float, fuel_liters: float):
super().__init__(brand, model, year, battery_kwh)
self.fuel_liters = fuel_liters
self._fuel_level = 100.0

def status(self) -> str:
base = super().status()
return f"{base} | 연료: {self._fuel_level:.0f}%"


ev = ElectricVehicle("Tesla", "Model 3", 2024, 75.0)
print(ev.start()) # Tesla Model 3 시동을 켰습니다. (배터리: 100%)
print(ev.charge(20.0)) # 0.0 kWh 충전 완료. 현재: 100%
print(ev.status())

hybrid = HybridVehicle("Toyota", "Prius", 2024, 8.8, 43.0)
print(hybrid.status())

메서드 오버라이딩 (Overriding)

class Shape:
def __init__(self, color: str = "black"):
self.color = color

def area(self) -> float:
raise NotImplementedError(f"{self.__class__.__name__}.area()를 구현해야 합니다.")

def perimeter(self) -> float:
raise NotImplementedError(f"{self.__class__.__name__}.perimeter()를 구현해야 합니다.")

def describe(self) -> str:
return (f"{self.__class__.__name__} — "
f"색상: {self.color}, 넓이: {self.area():.2f}, 둘레: {self.perimeter():.2f}")


class Circle(Shape):
def __init__(self, radius: float, color: str = "black"):
super().__init__(color)
self.radius = radius

def area(self) -> float:
import math
return math.pi * self.radius ** 2

def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius


class Rectangle(Shape):
def __init__(self, width: float, height: float, color: str = "black"):
super().__init__(color)
self.width = width
self.height = height

def area(self) -> float:
return self.width * self.height

def perimeter(self) -> float:
return 2 * (self.width + self.height)


class Triangle(Shape):
def __init__(self, a: float, b: float, c: float, color: str = "black"):
super().__init__(color)
if a + b <= c or a + c <= b or b + c <= a:
raise ValueError("삼각형 부등식을 만족하지 않습니다.")
self.a, self.b, self.c = a, b, c

def area(self) -> float:
s = self.perimeter() / 2
return (s * (s - self.a) * (s - self.b) * (s - self.c)) ** 0.5

def perimeter(self) -> float:
return self.a + self.b + self.c


shapes: list[Shape] = [
Circle(5, "red"),
Rectangle(4, 6, "blue"),
Triangle(3, 4, 5, "green"),
]

for shape in shapes:
print(shape.describe())

total_area = sum(s.area() for s in shapes)
print(f"\n전체 넓이 합: {total_area:.2f}")

다중 상속 (Multiple Inheritance)

Python은 다중 상속을 지원합니다. 한 클래스가 여러 부모 클래스를 상속받을 수 있습니다.

class Flyable:
def fly(self) -> str:
return f"{self.__class__.__name__}이(가) 납니다."

def altitude(self) -> str:
return "고도: 알 수 없음"


class Swimmable:
def swim(self) -> str:
return f"{self.__class__.__name__}이(가) 수영합니다."

def depth(self) -> str:
return "수심: 알 수 없음"


class Walkable:
def walk(self) -> str:
return f"{self.__class__.__name__}이(가) 걷습니다."


class Duck(Flyable, Swimmable, Walkable):
"""오리: 날 수도 있고, 수영도 하고, 걷기도 함"""

def __init__(self, name: str):
self.name = name

def quack(self) -> str:
return f"{self.name}: 꽥꽥!"

def fly(self) -> str: # 오버라이드
return f"{self.name}이(가) 짧은 거리를 납니다."


donald = Duck("도날드")
print(donald.fly()) # 도날드이(가) 짧은 거리를 납니다.
print(donald.swim()) # Duck이(가) 수영합니다.
print(donald.walk()) # Duck이(가) 걷습니다.
print(donald.quack()) # 도날드: 꽥꽥!

MRO (Method Resolution Order)

Python은 다중 상속 시 C3 선형화 알고리즘을 사용하여 메서드를 어떤 순서로 탐색할지 결정합니다.

class A:
def method(self) -> str:
return "A"


class B(A):
def method(self) -> str:
return f"B → {super().method()}"


class C(A):
def method(self) -> str:
return f"C → {super().method()}"


class D(B, C):
def method(self) -> str:
return f"D → {super().method()}"


d = D()
print(d.method()) # D → B → C → A

# MRO 확인
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

# mro() 메서드로도 확인 가능
print(D.mro())

다이아몬드 상속 문제와 해결

class Base:
def __init__(self):
print("Base.__init__")
self.base_value = "base"


class Left(Base):
def __init__(self):
print("Left.__init__")
super().__init__()
self.left_value = "left"


class Right(Base):
def __init__(self):
print("Right.__init__")
super().__init__()
self.right_value = "right"


class Diamond(Left, Right):
def __init__(self):
print("Diamond.__init__")
super().__init__() # MRO에 따라 Left → Right → Base 순서로 호출
self.diamond_value = "diamond"


# Base.__init__은 딱 한 번만 호출됨 (super() 덕분)
d = Diamond()
# 출력:
# Diamond.__init__
# Left.__init__
# Right.__init__
# Base.__init__

print(d.base_value) # base
print(d.left_value) # left
print(d.right_value) # right
print(d.diamond_value) # diamond

super()와 다중 상속의 올바른 조합

class LogMixin:
"""로깅 기능을 추가하는 믹스인"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._log: list[str] = []

def log(self, message: str) -> None:
import datetime
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
self._log.append(f"[{timestamp}] {message}")

def get_logs(self) -> list[str]:
return list(self._log)


class TimestampMixin:
"""생성/수정 시각을 추가하는 믹스인"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
import datetime
self.created_at = datetime.datetime.now()
self.updated_at = self.created_at

def touch(self) -> None:
import datetime
self.updated_at = datetime.datetime.now()


class BaseModel:
def __init__(self, id: int, name: str):
self.id = id
self.name = name

def __repr__(self) -> str:
return f"{self.__class__.__name__}(id={self.id}, name={self.name!r})"


class User(LogMixin, TimestampMixin, BaseModel):
def __init__(self, id: int, name: str, email: str):
super().__init__(id=id, name=name)
self.email = email

def update_email(self, new_email: str) -> None:
old = self.email
self.email = new_email
self.touch()
self.log(f"이메일 변경: {old}{new_email}")


user = User(1, "김철수", "kim@example.com")
user.update_email("kim.new@example.com")
user.log("프로필 조회")

print(user)
print(f"생성: {user.created_at.strftime('%H:%M:%S')}")
print("로그:", user.get_logs())
print("MRO:", [c.__name__ for c in User.__mro__])

is-a 관계 설계 원칙

# 올바른 상속 — is-a 관계
class Employee:
def __init__(self, name: str, salary: float):
self.name = name
self.salary = salary

def work(self) -> str:
return f"{self.name}이(가) 일합니다."

def get_pay(self) -> float:
return self.salary


class Manager(Employee): # Manager IS-A Employee ✅
def __init__(self, name: str, salary: float, department: str):
super().__init__(name, salary)
self.department = department
self._team: list[Employee] = []

def add_to_team(self, employee: Employee) -> None:
self._team.append(employee)

def get_pay(self) -> float:
bonus = self.salary * 0.2 # 매니저 보너스 20%
return self.salary + bonus

def team_size(self) -> int:
return len(self._team)


class Engineer(Employee): # Engineer IS-A Employee ✅
def __init__(self, name: str, salary: float, tech_stack: list[str]):
super().__init__(name, salary)
self.tech_stack = tech_stack

def work(self) -> str:
techs = ", ".join(self.tech_stack)
return f"{self.name}이(가) {techs}로 개발합니다."


# 다형성 활용
team: list[Employee] = [
Engineer("Alice", 5_000_000, ["Python", "FastAPI"]),
Engineer("Bob", 4_500_000, ["Java", "Spring"]),
Manager("Carol", 7_000_000, "백엔드"),
]

total_payroll = sum(e.get_pay() for e in team)
print(f"총 급여: {total_payroll:,.0f}원")
for emp in team:
print(f" {emp.work()} — 급여: {emp.get_pay():,.0f}원")

실전 예제: 직원 시스템

from abc import ABC, abstractmethod
from datetime import date


class Employee(ABC):
"""직원 기반 추상 클래스"""

def __init__(self, emp_id: str, name: str, hire_date: date):
self.emp_id = emp_id
self.name = name
self.hire_date = hire_date

@abstractmethod
def calculate_pay(self) -> float:
"""급여 계산 — 서브클래스 구현 필수"""
...

@property
def years_of_service(self) -> float:
delta = date.today() - self.hire_date
return delta.days / 365.25

def get_info(self) -> str:
return (f"사번: {self.emp_id} | {self.name} | "
f"입사: {self.hire_date} ({self.years_of_service:.1f}년)")


class FullTimeEmployee(Employee):
def __init__(self, emp_id: str, name: str, hire_date: date,
monthly_salary: float):
super().__init__(emp_id, name, hire_date)
self.monthly_salary = monthly_salary

def calculate_pay(self) -> float:
base = self.monthly_salary
# 근속 연수 보너스: 5년마다 5%
bonus_rate = (int(self.years_of_service) // 5) * 0.05
return base * (1 + bonus_rate)


class PartTimeEmployee(Employee):
def __init__(self, emp_id: str, name: str, hire_date: date,
hourly_rate: float, hours_worked: float):
super().__init__(emp_id, name, hire_date)
self.hourly_rate = hourly_rate
self.hours_worked = hours_worked

def calculate_pay(self) -> float:
overtime = max(0, self.hours_worked - 160) # 160시간 초과 시 1.5배
regular_pay = min(self.hours_worked, 160) * self.hourly_rate
overtime_pay = overtime * self.hourly_rate * 1.5
return regular_pay + overtime_pay


class Contractor(Employee):
def __init__(self, emp_id: str, name: str, hire_date: date,
project_fee: float, completion_rate: float):
super().__init__(emp_id, name, hire_date)
self.project_fee = project_fee
self.completion_rate = min(1.0, max(0.0, completion_rate))

def calculate_pay(self) -> float:
return self.project_fee * self.completion_rate


employees: list[Employee] = [
FullTimeEmployee("E001", "김정규", date(2015, 3, 1), 4_500_000),
FullTimeEmployee("E002", "이선임", date(2010, 7, 15), 6_000_000),
PartTimeEmployee("P001", "박알바", date(2024, 1, 10), 12_000, 120),
Contractor("C001", "최계약", date(2024, 2, 1), 10_000_000, 0.75),
]

print("=== 급여 명세서 ===")
total = 0
for emp in employees:
pay = emp.calculate_pay()
total += pay
print(f"{emp.get_info()}")
print(f" → 이번 달 급여: {pay:,.0f}원")

print(f"\n총 급여 지출: {total:,.0f}원")

고수 팁

1. Mixin 클래스 — 기능 조합 패턴

class SerializeMixin:
"""JSON 직렬화 기능을 추가하는 믹스인"""
def to_dict(self) -> dict:
return {k: v for k, v in vars(self).items() if not k.startswith("_")}

def to_json(self) -> str:
import json
return json.dumps(self.to_dict(), ensure_ascii=False, default=str)


class ValidateMixin:
"""유효성 검사 기능을 추가하는 믹스인"""
def validate(self) -> list[str]:
errors = []
for attr, value in vars(self).items():
if attr.startswith("_"):
continue
if value is None:
errors.append(f"{attr}은(는) None일 수 없습니다.")
return errors

def is_valid(self) -> bool:
return len(self.validate()) == 0


class Product(SerializeMixin, ValidateMixin):
def __init__(self, name: str, price: float):
self.name = name
self.price = price


p = Product("Python 책", 35000)
print(p.to_json()) # {"name": "Python 책", "price": 35000}
print(p.is_valid()) # True

2. __init_subclass__로 서브클래스 자동 등록

class Command:
_registry: dict[str, type] = {}

def __init_subclass__(cls, command_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if command_name:
Command._registry[command_name] = cls

def execute(self) -> str:
raise NotImplementedError

@classmethod
def dispatch(cls, name: str) -> "Command":
if name not in cls._registry:
raise KeyError(f"알 수 없는 명령: {name}")
return cls._registry[name]()


class HelpCommand(Command, command_name="help"):
def execute(self) -> str:
return "사용법: help, quit, version"


class QuitCommand(Command, command_name="quit"):
def execute(self) -> str:
return "종료합니다."


for cmd_name in ["help", "quit"]:
cmd = Command.dispatch(cmd_name)
print(cmd.execute())

정리

  • 상속: class Child(Parent) — is-a 관계에만 사용
  • super(): MRO에 따라 다음 클래스 참조, 다중 상속에서 안전
  • 오버라이딩: 자식 클래스에서 부모 메서드 재정의
  • 다중 상속: 여러 부모 클래스 상속, Mixin 패턴에 유용
  • MRO: C3 선형화 알고리즘으로 메서드 탐색 순서 결정

상속은 강력하지만 남용하면 코드가 복잡해집니다. "상속보다 구성(Composition over Inheritance)" 원칙을 기억하세요.

Advertisement