여기서는 재사용 자체보다 같은 종류의 객체를 공통 인터페이스로 다룰 수 있는지를 먼저 봅니다.
부모-자식 관계가 타입 관계를 드러내면 상속이 맞고, 다른 객체를 포함해 쓰는 쪽에 가깝다면 합성이 더 낫습니다.
숏컷 코드
class Animal:
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return "woof"문법
기본형은 상속 선언, 메서드 오버라이드, super() 호출, 합성 대안까지 같이 보면 됩니다.
class Child(Parent):
def method(self):
return super().method()
class Owner:
def __init__(self):
self.member = Parent()상속은 단순 코드 재사용 도구가 아니라, 공통 인터페이스와 대체 가능성을 만드는 도구로 읽는 편이 더 정확합니다.
공통 타입
같은 종류의 객체를 공통 인터페이스로 다루면 상속이 맞다
상속의 핵심은 코드 재사용 자체보다, 여러 클래스를 공통 타입처럼 취급할 수 있게 하는 데 있습니다.
class Animal:
def speak(self) -> str:
raise NotImplementedError
class Dog(Animal):
def speak(self) -> str:
return "woof"
class Cat(Animal):
def speak(self) -> str:
return "meow"이렇게 하면 호출하는 쪽은 구체 클래스보다 공통 인터페이스에 의존할 수 있습니다.
확장 방식
부모 동작을 이어 쓰며 확장할 때는 오버라이드와 super()를 같이 본다
자식 클래스는 부모 메서드를 덮어쓸 수 있고, 필요하면 super()로 부모 구현을 재사용할 수 있습니다.
class User:
def __init__(self, name: str) -> None:
self.name = name
class Admin(User):
def __init__(self, name: str, level: int) -> None:
super().__init__(name)
self.level = level부모 초기화나 공통 전처리를 유지하면서 자식 전용 상태를 더할 때 특히 유용합니다.
합성 경계
"가지고 쓰는 관계"면 상속보다 합성이 더 낫다
모든 재사용 문제를 상속으로 풀면 클래스 계층이 복잡해질 수 있습니다. 단순히 다른 객체를 "가지고 쓰는" 관계라면 합성이 더 낫습니다.
class Engine:
def start(self) -> None:
...
class Car:
def __init__(self) -> None:
self.engine = Engine()is-a 관계인가, has-a 관계인가를 먼저 묻는 습관이 중요합니다.
계층 관리
계층이 깊어질수록 상속보다 합성을 다시 본다
상속은 한두 단계까지는 자연스러워도, 계층이 깊어질수록 추적이 급격히 어려워집니다.
그래서 공통 인터페이스 이점이 분명하지 않다면 합성 쪽을 다시 보는 편이 좋습니다.
주의할 점
부모 __init__를 건너뛰면 필요한 초기화가 누락될 수 있습니다. 자식에서 생성자를 재정의할 때는 super() 호출 여부를 의식해야 합니다.
# ❌ 부모 초기화 누락
class User:
def __init__(self, name):
self.name = name
class Admin(User):
def __init__(self, name, level):
self.level = level
# ✅ 부모 초기화 포함
class Admin(User):
def __init__(self, name, level):
super().__init__(name)
self.level = level참고 링크
2 sources