다형성과 추상 클래스
**다형성(Polymorphism)**은 같은 인터페이스(메서드 이름)를 통해 서로 다른 타입의 객체가 각자의 방식으로 동작하는 것입니다. Python의 다형성은 덕 타이핑이라는 독특한 방식으로 구현됩니다.
다형성 개념
# 다형성의 핵심: 같은 메서드 이름, 다른 동작
class Korean:
def greet(self) -> str:
return "안녕하세요!"
class English:
def greet(self) -> str:
return "Hello!"
class Japanese:
def greet(self) -> str:
return "こんにちは!"
def say_hello(person) -> None:
"""타입에 관계없이 greet()만 있으면 된다"""
print(person.greet())
people = [Korean(), English(), Japanese()]
for person in people:
say_hello(person) # 다형성: 동일한 호출, 다른 결과
덕 타이핑 (Duck Typing)
"꽥꽥 소리를 내고, 오리처럼 걷는다면, 그것은 오리다."
— James Whitcomb Riley
Python은 객체의 타입이 아니라 **행동(메서드/속성)**으로 객체를 판단합니다. 특정 타입을 상속받지 않아도 필요한 메서드만 갖고 있으면 됩니다.
class Duck:
def quack(self) -> str:
return "꽥꽥!"
def walk(self) -> str:
return "뒤뚱뒤뚱"
class Person:
def quack(self) -> str:
return "저는 오리 흉내를 냅니다: 꽥꽥!"
def walk(self) -> str:
return "두 발로 걷습니다"
class Robot:
def quack(self) -> str:
return "삑삑! (오리 소리 시뮬레이션)"
def walk(self) -> str:
return "기계적으로 걷습니다"
def make_it_quack(duck_like) -> None:
"""quack()과 walk()를 가진 것은 무엇이든 사용 가능"""
print(duck_like.quack())
print(duck_like.walk())
print()
for creature in [Duck(), Person(), Robot()]:
make_it_quack(creature)
덕 타이핑의 실제 활용
# 파일 객체처럼 동작하는 모든 것을 처리
import io
def process_text(file_like) -> str:
"""read() 메서드만 있으면 됨 — 실제 파일인지 상관없음"""
return file_like.read().upper()
# 실제 파일
with open("/dev/null", "r") as f:
pass # 파일이 없을 수 있으므로 스킵
# StringIO (메모리 내 파일)
memory_file = io.StringIO("hello world")
print(process_text(memory_file)) # HELLO WORLD
# 직접 만든 파일처럼 동작하는 클래스
class FakeFile:
def __init__(self, content: str):
self._content = content
def read(self) -> str:
return self._content
fake = FakeFile("python is awesome")
print(process_text(fake)) # PYTHON IS AWESOME
추상 클래스 (Abstract Class)
추상 클래스는 직접 인스턴스화할 수 없고, 반드시 서브클래스에서 구현해야 하는 메서드(추상 메서드)를 정의합니다. Python의 abc 모듈을 사용합니다.
from abc import ABC, abstractmethod
class Shape(ABC):
"""추상 기반 클래스 — 직접 인스턴스화 불가"""
def __init__(self, color: str = "black"):
self.color = color
@abstractmethod
def area(self) -> float:
"""서브클래스에서 반드시 구현해야 함"""
...
@abstractmethod
def perimeter(self) -> float:
"""서브클래스에서 반드시 구현해야 함"""
...
# 추상 클래스도 구체적 메서드를 가질 수 있음
def describe(self) -> str:
return (f"{self.__class__.__name__} | 색상: {self.color} | "
f"넓이: {self.area():.2f} | 둘레: {self.perimeter():.2f}")
def scale(self, factor: float) -> None:
raise NotImplementedError(f"{self.__class__.__name__}의 scale() 미구현")
# 추상 클래스 직접 인스턴스화 시도
try:
s = Shape() # TypeError!
except TypeError as e:
print(f"오류: {e}")
추상 메서드 구현
import math
class Circle(Shape):
def __init__(self, radius: float, color: str = "black"):
super().__init__(color)
if radius <= 0:
raise ValueError("반지름은 양수여야 합니다.")
self.radius = radius
def area(self) -> float: # 추상 메서드 구현 필수
return math.pi * self.radius ** 2
def perimeter(self) -> float: # 추상 메서드 구현 필수
return 2 * math.pi * self.radius
def scale(self, factor: float) -> None:
self.radius *= factor
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)
def scale(self, factor: float) -> None:
self.width *= factor
self.height *= factor
class RegularPolygon(Shape):
def __init__(self, sides: int, side_length: float, color: str = "black"):
super().__init__(color)
if sides < 3:
raise ValueError("변의 수는 3 이상이어야 합니다.")
self.sides = sides
self.side_length = side_length
def area(self) -> float:
return (self.sides * self.side_length ** 2) / (4 * math.tan(math.pi / self.sides))
def perimeter(self) -> float:
return self.sides * self.side_length
shapes: list[Shape] = [
Circle(5, "red"),
Rectangle(4, 6, "blue"),
RegularPolygon(6, 3, "green"), # 정육각형
]
for shape in shapes:
print(shape.describe())
# 다형성: 모든 Shape 서브클래스에 동일한 인터페이스
total_area = sum(s.area() for s in shapes)
largest = max(shapes, key=lambda s: s.area())
print(f"\n가장 큰 도형: {largest.__class__.__name__} (넓이: {largest.area():.2f})")
ABCMeta와 ABC
from abc import ABCMeta, abstractmethod
# 방법 1: ABCMeta를 메타클래스로 직접 사용
class Animal(metaclass=ABCMeta):
@abstractmethod
def speak(self) -> str:
...
# 방법 2: ABC를 상속 (권장, 더 간결)
class Vehicle(ABC):
@abstractmethod
def move(self) -> str:
...
@classmethod
@abstractmethod
def create_default(cls) -> "Vehicle":
"""추상 클래스 메서드"""
...
@staticmethod
@abstractmethod
def fuel_type() -> str:
"""추상 정적 메서드"""
...
@property
@abstractmethod
def speed(self) -> float:
"""추상 프로퍼티"""
...
class ElectricCar(Vehicle):
def __init__(self, max_speed: float):
self._speed = max_speed
def move(self) -> str:
return "조용히 전기로 달립니다."
@classmethod
def create_default(cls) -> "ElectricCar":
return cls(max_speed=150.0)
@staticmethod
def fuel_type() -> str:
return "전기"
@property
def speed(self) -> float:
return self._speed
car = ElectricCar.create_default()
print(car.move()) # 조용히 전기로 달립니다.
print(car.fuel_type()) # 전기
print(car.speed) # 150.0
플러그인 시스템 구현
from abc import ABC, abstractmethod
from typing import ClassVar
class PaymentProcessor(ABC):
"""결제 프로세서 추상 클래스 — 플러그인 인터페이스"""
_processors: ClassVar[dict[str, type]] = {}
def __init_subclass__(cls, processor_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if processor_name:
PaymentProcessor._processors[processor_name] = cls
@abstractmethod
def process_payment(self, amount: float, currency: str) -> dict:
...
@abstractmethod
def refund(self, transaction_id: str, amount: float) -> dict:
...
@classmethod
def get_processor(cls, name: str) -> "PaymentProcessor":
if name not in cls._processors:
available = list(cls._processors.keys())
raise ValueError(f"알 수 없는 프로세서: {name}. 사용 가능: {available}")
return cls._processors[name]()
class KakaoPayProcessor(PaymentProcessor, processor_name="kakaopay"):
def process_payment(self, amount: float, currency: str) -> dict:
return {
"status": "success",
"processor": "KakaoPay",
"amount": amount,
"currency": currency,
"transaction_id": f"KP-{hash(amount):x}",
}
def refund(self, transaction_id: str, amount: float) -> dict:
return {"status": "refunded", "transaction_id": transaction_id, "amount": amount}
class TossProcessor(PaymentProcessor, processor_name="toss"):
def process_payment(self, amount: float, currency: str) -> dict:
return {
"status": "success",
"processor": "Toss",
"amount": amount,
"currency": currency,
"transaction_id": f"TOSS-{hash(amount):x}",
}
def refund(self, transaction_id: str, amount: float) -> dict:
return {"status": "refunded", "transaction_id": transaction_id, "amount": amount}
# 런타임에 프로세서 선택
for processor_name in ["kakaopay", "toss"]:
processor = PaymentProcessor.get_processor(processor_name)
result = processor.process_payment(10_000, "KRW")
print(result)
추상 클래스 vs 인터페이스
Python에는 Java처럼 interface 키워드가 없습니다. 대신 두 가지 방식으로 인터페이스를 표현합니다.
# 방법 1: 추상 메서드만 있는 ABC (인터페이스처럼 사용)
class Drawable(ABC):
@abstractmethod
def draw(self) -> str: ...
@abstractmethod
def resize(self, factor: float) -> None: ...
class Saveable(ABC):
@abstractmethod
def save(self, path: str) -> bool: ...
@abstractmethod
def load(self, path: str) -> bool: ...
# 다중 추상 클래스 상속으로 인터페이스 구현
class SVGImage(Drawable, Saveable):
def __init__(self, content: str):
self.content = content
def draw(self) -> str:
return f"SVG 렌더링: {self.content[:20]}..."
def resize(self, factor: float) -> None:
print(f"SVG 크기 {factor}배 조정")
def save(self, path: str) -> bool:
print(f"{path}에 저장")
return True
def load(self, path: str) -> bool:
print(f"{path}에서 로드")
return True
# 방법 2: Protocol (Python 3.8+) — 다음 챕터에서 다룸
실전 예제: 도형 클래스 계층
from abc import ABC, abstractmethod
import math
from typing import Iterator
class Shape2D(ABC):
"""2D 도형 추상 기반 클래스"""
def __init__(self, x: float = 0.0, y: float = 0.0):
self.x = x # 중심 x 좌표
self.y = y # 중심 y 좌표
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
@abstractmethod
def contains(self, px: float, py: float) -> bool:
"""점 (px, py)가 도형 내부에 있는지"""
...
def distance_to(self, other: "Shape2D") -> float:
"""두 도형의 중심 간 거리"""
return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)
def __lt__(self, other: "Shape2D") -> bool:
return self.area() < other.area()
def __eq__(self, other: object) -> bool:
if not isinstance(other, Shape2D):
return NotImplemented
return abs(self.area() - other.area()) < 1e-9
def __repr__(self) -> str:
return f"{self.__class__.__name__}(center=({self.x}, {self.y}), area={self.area():.2f})"
class Circle(Shape2D):
def __init__(self, cx: float, cy: float, radius: float):
super().__init__(cx, cy)
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
def perimeter(self) -> float:
return 2 * math.pi * self.radius
def contains(self, px: float, py: float) -> bool:
return math.sqrt((px - self.x) ** 2 + (py - self.y) ** 2) <= self.radius
class Rect(Shape2D):
def __init__(self, cx: float, cy: float, width: float, height: float):
super().__init__(cx, cy)
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)
def contains(self, px: float, py: float) -> bool:
return (abs(px - self.x) <= self.width / 2 and
abs(py - self.y) <= self.height / 2)
class Canvas:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
self._shapes: list[Shape2D] = []
def add(self, shape: Shape2D) -> None:
self._shapes.append(shape)
def total_area(self) -> float:
return sum(s.area() for s in self._shapes)
def find_containing(self, px: float, py: float) -> list[Shape2D]:
return [s for s in self._shapes if s.contains(px, py)]
def largest(self) -> Shape2D | None:
return max(self._shapes) if self._shapes else None
def sorted_by_area(self) -> list[Shape2D]:
return sorted(self._shapes)
def __len__(self) -> int:
return len(self._shapes)
def __iter__(self) -> Iterator[Shape2D]:
return iter(self._shapes)
canvas = Canvas(100, 100)
canvas.add(Circle(0, 0, 5))
canvas.add(Rect(10, 10, 8, 6))
canvas.add(Circle(20, 20, 3))
print(f"도형 수: {len(canvas)}")
print(f"전체 넓이: {canvas.total_area():.2f}")
print(f"가장 큰 도형: {canvas.largest()}")
print(f"넓이 순 정렬: {canvas.sorted_by_area()}")
print(f"(0,0) 포함 도형: {canvas.find_containing(0, 0)}")
고수 팁
1. __subclasshook__으로 가상 서브클래스 등록
from abc import ABC, abstractmethod
class Printable(ABC):
@abstractmethod
def print(self) -> None:
...
@classmethod
def __subclasshook__(cls, subclass):
"""print 메서드를 가진 클래스라면 모두 Printable로 인식"""
if cls is Printable:
if hasattr(subclass, "print") and callable(subclass.print):
return True
return NotImplemented
class MyDocument:
def print(self) -> None: # Printable을 상속하지 않아도
print("문서 출력")
print(issubclass(MyDocument, Printable)) # True — __subclasshook__ 덕분
print(isinstance(MyDocument(), Printable)) # True
2. 추상 클래스에서 기본 구현 제공
class Validator(ABC):
@abstractmethod
def validate(self, value) -> bool:
"""기본 구현 없이 순수 추상"""
...
def validate_all(self, values: list) -> list[bool]:
"""추상 메서드를 사용하는 구체적 메서드"""
return [self.validate(v) for v in values]
def assert_valid(self, value) -> None:
if not self.validate(value):
raise ValueError(f"유효성 검사 실패: {value!r}")
정리
- 다형성: 같은 인터페이스, 다른 구현 — Python의 핵심 강점
- 덕 타이핑: 타입이 아닌 행동으로 판단 —
isinstance()체크 최소화 - 추상 클래스(ABC): 인터페이스 강제 + 구체적 메서드 공유
@abstractmethod: 서브클래스 구현 강제- 추상 클래스 vs Protocol: ABC는 명시적 상속 필요, Protocol은 구조적 서브타이핑 (다음 챕터)