기본 패턴
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
def perimeter(self) -> float:
return 2 * 3.14159 * self.radius
# Shape() # TypeError: Can't instantiate abstract class
c = Circle(5) # OK
print(c.area()) # 78.53...설명
abstractmethod는 인터페이스 계약을 런타임에 강제한다
ABC를 상속하고 @abstractmethod가 붙은 메서드를 하나라도 구현하지 않으면, 해당 클래스의 인스턴스 생성 시 즉시 TypeError가 발생한다. 이 검사는 타입 검사기가 아닌 Python 인터프리터가 직접 수행하므로, 타입 힌트 없이도 계약 위반을 잡을 수 있다.
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save(self, key: str, value: bytes) -> None: ...
@abstractmethod
def load(self, key: str) -> bytes: ...
def exists(self, key: str) -> bool:
# 추상 메서드가 아닌 일반 메서드는 기본 구현을 제공할 수 있다
try:
self.load(key)
return True
except KeyError:
return False
class IncompleteStorage(Storage):
def save(self, key: str, value: bytes) -> None:
pass
# load 미구현 → 인스턴스 생성 불가
# IncompleteStorage()
# TypeError: Can't instantiate abstract class IncompleteStorage
# with abstract method loadabstractmethod는 property, classmethod, staticmethod와 조합할 수 있다.
class Config(ABC):
@property
@abstractmethod
def name(self) -> str: ...
@classmethod
@abstractmethod
def from_env(cls) -> "Config": ...Protocol은 구조적 서브타이핑으로 상속 없이 인터페이스를 검사한다
typing.Protocol은 명목적 타이핑(ABC)과 달리 덕 타이핑을 타입 시스템 안으로 가져온다. 클래스가 Protocol을 상속하지 않아도, 요구하는 메서드·속성 시그니처를 모두 갖추고 있으면 호환 타입으로 인정된다. 서드파티 라이브러리처럼 상속 계층을 바꿀 수 없는 클래스에 인터페이스를 적용할 때 유용하다.
from typing import Protocol
class Drawable(Protocol):
def draw(self, x: int, y: int) -> None: ...
def get_bounds(self) -> tuple[int, int, int, int]: ...
# Drawable을 상속하지 않는 외부 클래스
class Sprite:
def draw(self, x: int, y: int) -> None:
print(f"Sprite at ({x}, {y})")
def get_bounds(self) -> tuple[int, int, int, int]:
return (0, 0, 100, 100)
def render(obj: Drawable) -> None:
obj.draw(0, 0)
# mypy/pyright는 Sprite가 Drawable을 만족한다고 판단한다
sprite = Sprite()
render(sprite) # 타입 오류 없음ABC와 달리 Protocol은 런타임에 인스턴스 생성을 막지 않는다. 타입 검사기만 프로토콜 호환성을 검증한다.
runtime_checkable은 isinstance 검사를 가능하게 하지만 메서드 시그니처는 검증하지 않는다
@runtime_checkable을 붙이면 isinstance(obj, Protocol)이 동작하지만, 이 검사는 메서드 이름 존재 여부만 확인하고 시그니처(인자 타입, 반환 타입)는 검증하지 않는다. 따라서 런타임 안전성이 완전하지 않다는 점을 인식하고 사용해야 한다.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_json(self) -> str: ...
class GoodImpl:
def to_json(self) -> str:
return '{"ok": true}'
class FakeImpl:
# 시그니처가 다르지만 isinstance 검사는 통과한다
def to_json(self, indent: int = 0) -> str:
return "{}"
class WrongImpl:
def to_json(self) -> int: # 반환 타입이 다름
return 42
print(isinstance(GoodImpl(), Serializable)) # True
print(isinstance(FakeImpl(), Serializable)) # True ← 시그니처 불일치 미감지
print(isinstance(WrongImpl(), Serializable)) # True ← 반환 타입 불일치 미감지
# 이름이 없으면 False
class NoImpl:
pass
print(isinstance(NoImpl(), Serializable)) # False런타임 호환성 보장이 필요하다면 ABC를 사용하거나, 타입 검사기(mypy/pyright)에 의존하는 편이 낫다.
ABC vs Protocol 선택 기준 — 상속 계층 필요 시 ABC, 서드파티 타입 호환 시 Protocol
| 조건 | 선택 |
|---|---|
| 공통 기본 구현(믹스인 메서드)이 필요하다 | ABC |
상속 계층과 isinstance 검사가 모두 필요하다 | ABC |
| 외부 라이브러리 클래스에 인터페이스를 소급 적용한다 | Protocol |
| 상속 없이 덕 타이핑 의도를 타입으로 문서화한다 | Protocol |
| 런타임 강제보다 정적 분석 도구 피드백이 우선이다 | Protocol |
ABC와 Protocol을 혼합하는 패턴도 가능하다.
from abc import ABC, abstractmethod
from typing import Protocol
# 내부 계층: ABC로 강제
class BaseRepository(ABC):
@abstractmethod
def find_by_id(self, id: int): ...
@abstractmethod
def save(self, entity) -> None: ...
# 외부 인터페이스: Protocol로 느슨하게
class RepositoryLike(Protocol):
def find_by_id(self, id: int): ...
def save(self, entity) -> None: ...
# 내부 구현은 ABC 상속, 외부 함수는 Protocol 파라미터
class UserRepository(BaseRepository):
def find_by_id(self, id: int):
return {"id": id}
def save(self, entity) -> None:
pass
def process(repo: RepositoryLike) -> None:
item = repo.find_by_id(1)
print(item)
process(UserRepository()) # OK빠른 정리
| 상황 | 적합한 선택 |
|---|---|
| 미구현 메서드를 런타임에 강제해야 한다 | ABC + @abstractmethod |
| 공통 메서드 기본 구현을 공유해야 한다 | ABC (일반 메서드 포함) |
| 외부 클래스에 소급 인터페이스를 적용한다 | Protocol |
isinstance로 프로토콜 검사가 필요하다 | @runtime_checkable Protocol |
| 추상 프로퍼티가 필요하다 | @property + @abstractmethod |
| 추상 클래스 메서드가 필요하다 | @classmethod + @abstractmethod |
| 타입 검사기 피드백만으로 충분하다 | Protocol |
주의할 점
@abstractmethod 없이 ABC만 상속하면 인스턴스 생성이 막히지 않는다. 강제하려면 최소 하나의 @abstractmethod가 있어야 한다.
# ❌ 잘못된 이해 — 아무 메서드도 강제되지 않음
from abc import ABC
class Base(ABC):
def compute(self) -> int:
return 0 # abstractmethod 없음
class Child(Base):
pass # 아무것도 구현 안 해도
obj = Child() # 에러 없이 생성됨
print(obj.compute()) # 0
# ✅ abstractmethod를 붙여야 구현을 강제할 수 있다
from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def compute(self) -> int: ...
class Child(Base):
pass
# Child() # TypeError 발생