[Python] 인터페이스, 프로토콜, 추상 베이스 클래스
객체지향 프로그래밍은 결국 인터페이스에 관한 것이다. 파이썬에서 타이핑(typing)을 이해하는 가장 좋은 방법은 인터페이스가 제공하는 기법을 이해하는 것이다.
타이핑의 분류
- 덕 타이핑:
isinstance()
로 검사하지 않는 모든 파이썬 버전 - 구스 타이핑: 객체가 ABC 형인지 런타임에 검사하는 것,
isinstance()
로 ABC에 대해서 검사하는 파이썬 2.6~ - 정적 타이핑: C, 자바, 자료형 힌트와 외부 자료형 검사기를 사용하는 파이썬 3.5~
- 정적 덕 타이핑: Go,
typing.Protocol
형 힌트와 외부 자료형 검사기를 사용하는 파이썬 3.8~
두 종류의 프로토콜
동적 프로토콜
- 파이썬에서 늘 사용해 온 비공식 프로토콜, 동적 프로토콜은 암묵적이며 관례적으로 정의됨
- 객체는 동적 프로토콜의 일부만 구현해도 어느정도 쓸만 함
- 정적 자료형 검사기가 검증 불가능
정적 프로토콜
typing.Protocol
의 서브클래스로 명시적으로 정의됨, 정적 자료형 검사기가 검증 가능- 정적 프로토콜은 프로그램이 필요하지 않더라도 프로토콜 클래스에 선언된 메서드를 객체가 모두 제공해야 함
- 정적 자료형 검사기로 검증 가능
두 프로토콜 모두 클래스가 상속 등을 통해 프로토콜을 지원함을 선언할 필요는 없다.
덕 타이핑
파이썬은 ABC로 공식 정의된 Sequence
인터페이스에 속하는 모든 메서드가 구현되어 있지 않더라도 약간이라도 시퀀스를 닮은 객체는 시퀀스처럼 처리함 -> 덕 타이핑
멍키 패칭
기존에 존재하는 클래스나 모듈의 속성(메서드 or 변수)를 런타임에 동적으로 변경하는것
-> 파이썬은 타입보다는 행동(메서드/속성)을 중시하니, 런타임에 행동을 바꿔버리는 것이 가능하다는 철학
목업 API 만들 때 활용하기 좋음 -> 오ㄷㅂ
import requests
# 원래 코드
def fetch_data():
response = requests.get("https://api.example.com/data")
return response.json()
# 테스트 시 멍키 패칭
def mock_get(*args, **kwargs):
class MockResponse:
def json(self):
return {"message": "This is a mock response"}
return MockResponse()
requests.get = mock_get # 멍키 패칭!
print(fetch_data()) # {'message': 'This is a mock response'}
- API가 아직 완성 안 됐을 때 이런식으로 멍키패칭 해놓을 수 있다.! (헐랭~ 왜 지금 알려줌~)
방어적 프로그래밍과 조기 실패
이러한 동적 언어에서는 조기 실패(Early Failures)로 프로그램의 안정성과 유지보수성을 높인다.
-> 런타임 초반에 타입, 속성, 조건 등을 검사해서 오류를 빨리 터뜨리는 것
def process_user(user: dict):
if not isinstance(user, dict):
raise TypeError("user must be a dictionary")
if "name" not in user:
raise ValueError("user must have a 'name' field")
- 이렇게 해주지 않으면 나중에
user["name"]
에서 KeyError가 발생함 - 어디서 잘못된건지 파악하기 어려워짐
이러한 예시가 있고 try/except
도 있겠다, 조기 실패를 구현하는 방식은 정말 다양하니 필요할 때 찾아보자~
구스 타이핑
타입이 같다고 보는 기준이 이름이 아니라 구조(속성, 메서드)가 같은지로 판단하는 타입 정의 방식
구조적 타입을 기반으로 런타임에서도 검사 가능하게 만든 구현체-
- 공식적인 API 인터페이스나 플러그인 시스템 -> ABC
- ex) Python 표준 라이브러리나 플러그인 아키텍처에서 인터페이스 강제할 때
- 확장성과 유연성 + 타입 검사기 활용 -> Protocol
- ex) FastAPI, Pydantic, 데이터 파이프라인, ML 파이프라인 설계 시
둘 다 프로토콜이지만 관점이 다름
확장성과 타입 유연성이 필요하면 Protocol, 강제성과 런타임 검사가 필요하면 ABC
ABC - 런타임 프로토콜
- 파이썬 인터프리터 레벨에서 동작하는 추상 베이스 클래스
- 런타임 레벨에서 인터페이스 강제
- 상속 강제 (
@abstractmethod
구현 안 하면 인스턴스 생성 불가) isinstance()
,issubclass()
같은 런타임 검사 기본 제공
from abc import ABC, abstractmethod
class Flyer(ABC):
@abstractmethod
def fly(self) -> None: ...
class Bird(Flyer):
def fly(self):
print("Bird flying!")
bird = Bird()
print(isinstance(bird, Flyer)) # ✅ True (런타임 기본)
Protocol - 정적 프로토콜
- 타입 힌트 + 구조적 타이핑
- mypy, pyright 등 정적 타입 검사기에서 동작
- 인터프리터 레벨에서는 강제력 X ->
@runtim_checkable
붙이면 런타임 검사도 가능 : 정적 + 런타임 둘 다 O - 타입 시스템을 위한 인터페이스
from typing import Protocol, runtime_checkable
@runtime_checkable
class Flyer(Protocol):
def fly(self) -> None: ...
class Bird:
def fly(self):
print("Bird flying!")
bird = Bird()
print(isinstance(bird, Flyer)) # ✅ True (runtime_checkable 덕분)
Q. Protocol에서 @runtime_checkable
붙여서 런타임 검사 가능하면 ABC 쓸 필요 없지 않나?
A. @runtime_checkable
를 써도 런타임에서 강제성은 없음. isinstance()
나 issubclass()
로 검사가 가능해지는 거임.
반면 ABC는 런타임 검사가 기본 기능, 런타임 강제성이 있고, 반드시 추상클래스를 상속해야하기 때문에 개발자 실수를 강하게 막음
References
- 『 전문가를 위한 파이썬(2판) 』, 루시아누 하말류, 한빛미디어, Part 1 - Chapter 13
- ABC랑 Protocol 차이가 어려웠다.