상속과 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)" 원칙을 기억하세요.