여기서는 둘 다 "인터페이스 비슷한 것"으로 보이더라도, 런타임에 구현을 막아야 하는지와 상속 없이 타입 계약만 표현하면 되는지를 먼저 가릅니다.
구현 강제가 필요하면 ABC, 덕 타이핑 계약을 타입 수준에서 표현하면 Protocol이 맞습니다.
빠른 비교
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2기본 구조
기본형은 추상 메서드 강제, 기본 구현이 있는 ABC, 구조적 타입 계약, 런타임 체크 가능한 Protocol까지 같이 보면 됩니다.
from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable
class Base(ABC):
@abstractmethod
def work(self) -> None: ...
class BaseWithDefault(ABC):
@abstractmethod
def work(self) -> None: ...
def close(self) -> None:
...
class SupportsWork(Protocol):
def work(self) -> None: ...
@runtime_checkable
class SupportsClose(Protocol):
def close(self) -> None: ...ABC는 명목적 상속을 전제로 하고, Protocol은 필요한 메서드 시그니처가 맞는지만 봅니다.
둘 다 계약을 다루지만, 코드베이스에 강제하는 방식이 다릅니다.
강제 방식
구현을 런타임에 막아야 하면 ABC를 쓴다
ABC를 상속하고 @abstractmethod가 붙은 메서드를 하나라도 구현하지 않으면, 해당 클래스는 인스턴스 생성 시 바로 실패합니다.
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save(self, key: str, value: bytes) -> None: ...
class IncompleteStorage(Storage):
pass
# IncompleteStorage() # TypeError즉 "구현 안 된 상태로는 절대 못 쓰게 하겠다"면 ABC가 맞습니다.
class Storage(ABC):
@abstractmethod
def load(self, key: str) -> bytes: ...
def exists(self, key: str) -> bool:
try:
self.load(key)
return True
except KeyError:
return False공통 기본 구현까지 같이 두고 싶다면 이 점도 ABC 쪽이 더 자연스럽습니다.
계약 표현
상속 없이 메서드 계약만 표현하면 Protocol이 맞다
typing.Protocol은 명목적 타이핑(ABC)과 달리, 요구하는 메서드·속성 시그니처를 갖추고 있으면 호환 타입으로 인정합니다.
from typing import Protocol
class Drawable(Protocol):
def draw(self, x: int, y: int) -> None: ...
class Sprite:
def draw(self, x: int, y: int) -> None:
print(x, y)서드파티 라이브러리처럼 상속 계층을 바꿀 수 없는 클래스에 인터페이스를 적용할 때 특히 유용합니다.
타입 검사기 피드백이 핵심이고 런타임 상속 강제는 필요 없다면 Protocol이 더 가볍습니다.
런타임 검사
runtime_checkable은 편의 검사이지 완전한 강제는 아니다
@runtime_checkable을 붙이면 isinstance(obj, Protocol)이 동작하지만, 메서드 이름 존재 여부 위주로만 보고 시그니처까지 완전하게 검증하지는 않습니다.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_json(self) -> str: ...런타임에서 실제로 미구현 인스턴스 생성을 막는 기능은 ABC 쪽에 있습니다.
Protocol은 어디까지나 구조적 타입 계약을 표현하는 도구로 읽는 편이 정확합니다.
주의할 점
@abstractmethod 없이 ABC만 상속하면 인스턴스 생성이 막히지 않습니다. 강제하려면 최소 하나의 @abstractmethod가 있어야 합니다.
# ❌ abstractmethod가 없으면 구현 강제가 안 된다
from abc import ABC
class Base(ABC):
def compute(self) -> int:
return 0
class Child(Base):
pass
obj = Child() # 에러 없이 생성됨
# ✅ abstractmethod를 붙여야 구현을 강제할 수 있다
from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def compute(self) -> int: ...참고 링크
2 sources