Skip to main content

ABC와 Protocol

학습 목표

  • ABCabstractmethod로 추상 클래스를 정의할 수 있다
  • Protocol로 구조적 서브타이핑을 구현할 수 있다
  • ABC(명목적)와 Protocol(구조적) 서브타이핑의 차이를 이해한다

왜 중요한가

인터페이스 설계는 대규모 프로젝트에서 코드의 계약(Contract)을 명확히 하는 핵심 도구입니다. ML 프레임워크에서 Dataset__getitem____len__을 반드시 구현해야 하는 것이 바로 인터페이스 강제의 예입니다.

ABC (Abstract Base Class)

ABC는 상속을 통해 인터페이스를 강제합니다 (명목적 서브타이핑, Nominal Subtyping).
from abc import ABC, abstractmethod

class BaseModel(ABC):
    """모든 모델이 구현해야 하는 인터페이스"""

    @abstractmethod
    def fit(self, X, y):
        """모델을 학습합니다."""
        pass

    @abstractmethod
    def predict(self, X):
        """예측을 수행합니다."""
        pass

    def score(self, X, y):
        """정확도를 계산합니다 (공통 구현)."""
        predictions = self.predict(X)
        correct = sum(p == t for p, t in zip(predictions, y))
        return correct / len(y)

# 추상 메서드를 구현하지 않으면 인스턴스 생성 불가
# model = BaseModel()  # TypeError!

class LinearModel(BaseModel):
    def fit(self, X, y):
        print("선형 모델 학습")
        self.weights = [0.5] * len(X[0])

    def predict(self, X):
        return [sum(w * x for w, x in zip(self.weights, row)) for row in X]

model = LinearModel()  # 모든 추상 메서드 구현 -> 인스턴스 생성 가능
model.fit([[1, 2], [3, 4]], [1, 0])

추상 프로퍼티

from abc import ABC, abstractmethod

class Shape(ABC):
    @property
    @abstractmethod
    def area(self):
        pass

    @property
    @abstractmethod
    def perimeter(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

    @property
    def perimeter(self):
        return 2 * 3.14159 * self.radius

Protocol (Python 3.8+)

Protocol은 상속 없이 구조만으로 타이핑합니다 (구조적 서브타이핑, Structural Subtyping). “덕 타이핑(Duck Typing)의 타입 힌트 버전”입니다.
from typing import Protocol, runtime_checkable

@runtime_checkable
class Predictable(Protocol):
    """predict 메서드를 가진 객체"""
    def predict(self, X: list) -> list:
        ...

class MyModel:
    """Predictable을 상속하지 않지만, predict 메서드가 있음"""
    def predict(self, X: list) -> list:
        return [0] * len(X)

class RandomPredictor:
    def predict(self, X: list) -> list:
        import random
        return [random.choice([0, 1]) for _ in X]

# 타입 체커가 구조적으로 확인
def evaluate(model: Predictable, X: list, y: list) -> float:
    preds = model.predict(X)
    return sum(p == t for p, t in zip(preds, y)) / len(y)

# 상속 없이도 Protocol을 만족 (덕 타이핑)
m1 = MyModel()
m2 = RandomPredictor()
print(isinstance(m1, Predictable))  # True (@runtime_checkable 덕분)

evaluate(m1, [[1], [2]], [0, 0])  # 타입 체커 통과
evaluate(m2, [[1], [2]], [0, 0])  # 타입 체커 통과

ABC vs Protocol 비교

특성ABCProtocol
서브타이핑명목적 (상속 필요)구조적 (상속 불필요)
강제 방식상속 + abstractmethod메서드 시그니처만 일치
런타임 검사isinstance 자동 지원@runtime_checkable 필요
유연성낮음 (상속 관계 고정)높음 (느슨한 결합)
적합 사례프레임워크 내부 설계라이브러리 간 호환성
# ABC: "반드시 이 클래스를 상속해야 합니다"
class MyDataset(ABCDataset):  # 상속 필수
    def __getitem__(self, idx):
        ...

# Protocol: "이 메서드만 있으면 됩니다"
class MyDataset:  # 상속 불필요
    def __getitem__(self, idx):  # 메서드만 구현
        ...

AI/ML에서의 활용

from typing import Protocol
from dataclasses import dataclass

# 데이터 로더 프로토콜
class DataLoaderProtocol(Protocol):
    def __iter__(self):
        ...
    def __len__(self) -> int:
        ...

# 평가 메트릭 프로토콜
class Metric(Protocol):
    def update(self, predictions: list, targets: list) -> None:
        ...
    def compute(self) -> float:
        ...
    def reset(self) -> None:
        ...

class Accuracy:
    def __init__(self):
        self.correct = 0
        self.total = 0

    def update(self, predictions, targets):
        self.correct += sum(p == t for p, t in zip(predictions, targets))
        self.total += len(targets)

    def compute(self):
        return self.correct / self.total if self.total > 0 else 0.0

    def reset(self):
        self.correct = 0
        self.total = 0

# 어떤 Metric이든 동일한 인터페이스로 사용
def evaluate_model(model, data, metrics: list[Metric]):
    for metric in metrics:
        metric.reset()
    for X, y in data:
        preds = model.predict(X)
        for metric in metrics:
            metric.update(preds, y)
    return {type(m).__name__: m.compute() for m in metrics}
프레임워크 내부에서 상속 계층을 설계할 때는 ABC를, 외부 코드와의 호환성이나 느슨한 결합이 필요할 때는 Protocol을 사용합니다. 새 프로젝트에서는 Protocol을 먼저 고려하세요.
@runtime_checkable을 사용한 isinstance 검사는 약간의 오버헤드가 있지만, 일반적인 타입 힌트로의 사용은 런타임 성능에 영향이 없습니다 (정적 분석 전용).

체크리스트

  • ABCabstractmethod로 추상 클래스를 정의할 수 있다
  • Protocol로 구조적 서브타이핑을 구현할 수 있다
  • ABC와 Protocol의 차이점을 설명할 수 있다

다음 문서