1 개요
이 글은 Python 추상화와 추상 베이스 클래스(ABC)의 전체 흐름을 다룬다:
- 추상화란 무엇인가 - OOP에서 추상화의 두 가지 의미
- 추상 클래스와 abc 모듈 - 인터페이스를 코드 수준에서 강제하는 메커니즘
- 설계 계약 - 추상 클래스 상속이 만들어내는 인터페이스 보장
- @abstractmethod 데코레이터 - 추상 메서드, 추상 프로퍼티, 클래스/정적 메서드
- 실용적인 예제 - 데이터 파이프라인, 알림 시스템
- isinstance()와 ABC - 타입 검증 활용
2 추상화란 무엇인가
추상화(Abstraction) = 복잡한 현실을 단순화하여 핵심 특징만 추출하는 과정
OOP에서의 두 가지 의미:
| 의미 | 설명 | 예시 |
|---|---|---|
| 공통점 추출 | 여러 객체의 공통점을 상위 클래스로 정의 | 원/사각형/삼각형 → Shape.area() |
| 세부 숨김 | 구현 세부사항을 숨기고 인터페이스만 노출 | 운전자는 엔진 원리 몰라도 액셀만 알면 됨 |
효과: 시스템의 복잡도 감소 + 직관성 향상
3 추상 클래스와 abc 모듈
3.1 추상 클래스란 무엇인가
추상 클래스(Abstract Class) = 직접 인스턴스화 불가, 반드시 상속받아 사용해야 하는 설계도
- 추상 베이스 클래스는 하나 이상의 추상 메서드를 포함한다.
- 추상 메서드는 메서드의 시그니처만 정의하고 구현은 하지 않으며, 이를 상속받는 하위 클래스에서 반드시 구현해야 한다.
추상 클래스가 필요한 이유:
Shape.area()를 정의하고 싶지만,Shape자체는 구체적 도형이 아님area()없이 정의만 하면 자식 클래스에서 구현 강제 불가- 개발자가 실수로 빠뜨려도 일반 클래스는 오류가 미발생
해결책: @abstractmethod로 선언 → 자식 클래스에서 반드시 구현, 미구현 시 인스턴스 생성 시점에 TypeError 발생
3.2 abc 모듈 사용법
Python에서는 abc 모듈을 통해 추상 베이스 클래스를 정의한다.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""도형의 면적을 계산한다"""
pass
@abstractmethod
def perimeter(self):
"""도형의 둘레를 계산한다"""
pass
# 추상 클래스도 구체 메서드를 포함할 수 있다
def describe(self):
return f"이 도형의 면적은 {self.area()}이고 둘레는 {self.perimeter()}입니다."
# 추상 클래스는 직접 인스턴스화할 수 없다
# shape = Shape() # TypeError: Can't instantiate abstract class Shape
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self): # 추상 메서드 구현
return self.width * self.height
def perimeter(self): # 추상 메서드 구현
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # 추상 메서드 구현
return 3.14159 * self.radius ** 2
def perimeter(self): # 추상 메서드 구현
return 2 * 3.14159 * self.radius
rect = Rectangle(5, 3)
circle = Circle(4)
print(rect.describe()) # 이 도형의 면적은 15이고 둘레는 16입니다.
print(circle.describe()) # 이 도형의 면적은 50.26544이고 둘레는 25.13272입니다.Shape를 상속받는 클래스가 area()나 perimeter()를 구현하지 않으면
인스턴스를 생성하는 순간 TypeError가 발생한다.
이 강제성이 추상 클래스의 핵심 가치다.
대규모 팀에서 여러 개발자가 Shape를 상속받아 새로운 도형을 추가할 때,
메서드를 빠뜨리는 실수를 런타임 오류가 아닌 인스턴스 생성 시점에 즉시 잡아낼 수 있다.
3.3 설계 계약(Design Contract)
추상 클래스를 상속받는다는 것은 일종의 설계 계약 을 맺는 것이다.
“나는 Shape를 상속받는다. 그러므로 area()와 perimeter()를 반드시 구현하겠다”는
약속을 코드 수준에서 강제하는 것이다.
이 계약 구조는 대규모 시스템에서 매우 중요한 역할을 한다.
Scikit-learn의 BaseEstimator와 TransformerMixin이 대표적인 예다.
fit(), transform(), predict() 같은 메서드를 인터페이스로 정의함으로써
어떤 모델이든 동일한 인터페이스를 가지도록 강제한다.
덕분에 Pipeline이나 GridSearchCV 같은 도구가
내부 모델의 구체적인 구현을 알지 못해도
fit(), transform()을 일관되게 호출할 수 있다.
from sklearn.base import BaseEstimator, TransformerMixin
class CustomScaler(BaseEstimator, TransformerMixin):
def fit(self, X, y=None):
self.mean_ = X.mean(axis=0)
self.std_ = X.std(axis=0)
return self
def transform(self, X):
return (X - self.mean_) / self.std_BaseEstimator와 TransformerMixin을 상속받음으로써
CustomScaler는 Scikit-learn의 모든 파이프라인과 자동으로 호환된다.
이것이 추상화와 상속이 만들어내는 실질적인 생산성이다.
4 @abstractmethod 데코레이터
4.1 기본 추상 메서드
추상 메서드는 @abstractmethod 데코레이터를 사용하여 정의하며,
하위 클래스에서 반드시 구현해야 하는 메서드를 지정한다.
4.2 추상 프로퍼티 (Abstract Properties)
- 프로퍼티(property)는 메서드를 마치 속성(attribute)처럼 접근할 수 있게 만드는 기능이다.
@property와@abstractmethod를 함께 사용하면, 속성처럼 접근 가능하면서도 반드시 구현해야 하는 요소를 정의할 수 있다.
# @property 없이
class WithoutProperty:
def __init__(self):
self._speed = 100
def get_max_speed(self):
return self._speed
car1 = WithoutProperty()
print(car1.get_max_speed()) # 메서드 호출 — 괄호 필요
# @property 사용
class WithProperty:
def __init__(self):
self._speed = 100
@property
def max_speed(self):
return self._speed
car2 = WithProperty()
print(car2.max_speed) # 속성처럼 접근 — 괄호 불필요@property + @abstractmethod 조합으로 추상 프로퍼티 정의:
from abc import ABC, abstractmethod
class Vehicle(ABC):
@property
@abstractmethod
def max_speed(self):
pass
@property
@abstractmethod
def fuel_type(self):
pass
@abstractmethod
def start_engine(self):
pass
class Car(Vehicle):
def __init__(self, brand, model):
self.brand = brand
self.model = model
self._max_speed = 200
self._fuel_type = "gasoline"
@property
def max_speed(self):
return self._max_speed
@property
def fuel_type(self):
return self._fuel_type
def start_engine(self):
return f"{self.brand} {self.model}의 엔진이 시작되었습니다."
class ElectricCar(Vehicle):
def __init__(self, brand, model, battery_capacity):
self.brand = brand
self.model = model
self.battery_capacity = battery_capacity
self._max_speed = 180
self._fuel_type = "electric"
@property
def max_speed(self):
return self._max_speed
@property
def fuel_type(self):
return self._fuel_type
def start_engine(self):
return f"{self.brand} {self.model}의 전기 모터가 시작되었습니다."
car = Car("현대", "소나타")
electric_car = ElectricCar("테슬라", "모델 3", 75)
print(f"연료 타입: {car.fuel_type}, 최고 속도: {car.max_speed}km/h")
# 연료 타입: gasoline, 최고 속도: 200km/h
print(car.start_engine())
# 현대 소나타의 엔진이 시작되었습니다.
print(f"연료 타입: {electric_car.fuel_type}, 최고 속도: {electric_car.max_speed}km/h")
# 연료 타입: electric, 최고 속도: 180km/h
print(electric_car.start_engine())
# 테슬라 모델 3의 전기 모터가 시작되었습니다.4.3 추상 클래스 메서드와 정적 메서드
@classmethod, @staticmethod도 추상으로 선언할 수 있다.
현재 Python 3.3 이후에서는 @abstractclassmethod/@abstractstaticmethod 대신
@classmethod + @abstractmethod 조합을 사용하는 것이 권장된다.
from abc import ABC, abstractmethod
class DatabaseConnection(ABC):
@abstractmethod
def connect(self):
pass
@abstractmethod
def disconnect(self):
pass
@classmethod
@abstractmethod
def get_driver_name(cls):
pass
@staticmethod
@abstractmethod
def validate_connection_string(connection_string):
pass
class MySQLConnection(DatabaseConnection):
def __init__(self, host, database):
self.host = host
self.database = database
def connect(self):
return f"MySQL 데이터베이스 {self.database}에 연결되었습니다."
def disconnect(self):
return f"MySQL 데이터베이스 {self.database}와의 연결이 종료되었습니다."
@classmethod
def get_driver_name(cls):
return "MySQL Driver"
@staticmethod
def validate_connection_string(connection_string):
return "mysql://" in connection_string
mysql_conn = MySQLConnection("localhost", "my_database")
print(mysql_conn.connect())
# MySQL 데이터베이스 my_database에 연결되었습니다.
print(mysql_conn.disconnect())
# MySQL 데이터베이스 my_database와의 연결이 종료되었습니다.
print(mysql_conn.get_driver_name())
# MySQL Driver
print(mysql_conn.validate_connection_string("mysql://localhost/my_database"))
# True5 실용적인 ABC 예제
5.1 데이터 처리 파이프라인
추상 클래스와 템플릿 메서드 패턴(Template Method Pattern) 을 결합한 예다.
공통 실행 흐름(run_pipeline)은 추상 클래스에 정의하고,
각 단계의 구체적 구현은 하위 클래스에 위임한다.
from abc import ABC, abstractmethod
from typing import Any, List
class DataProcessor(ABC):
@abstractmethod
def load_data(self, source: str) -> Any:
"""데이터를 로드한다"""
pass
@abstractmethod
def process_data(self, data: Any) -> Any:
"""데이터를 처리한다"""
pass
@abstractmethod
def save_data(self, data: Any, destination: str) -> bool:
"""처리된 데이터를 저장한다"""
pass
# 템플릿 메서드: 공통 실행 흐름 정의
def run_pipeline(self, source: str, destination: str):
print("데이터 파이프라인 시작")
data = self.load_data(source)
processed_data = self.process_data(data)
success = self.save_data(processed_data, destination)
print(f"데이터 파이프라인 완료: {'성공' if success else '실패'}")
return success
class CSVProcessor(DataProcessor):
def load_data(self, source: str) -> List[dict]:
print(f"CSV 파일 {source}를 로드합니다")
return [{"name": "홍길동", "age": 30}, {"name": "김철수", "age": 25}]
def process_data(self, data: List[dict]) -> List[dict]:
print("CSV 데이터를 처리합니다")
for record in data:
record["age"] += 1
return data
def save_data(self, data: List[dict], destination: str) -> bool:
print(f"처리된 데이터를 {destination}에 저장합니다")
print(f"저장된 데이터: {data}")
return True
class JSONProcessor(DataProcessor):
def load_data(self, source: str) -> dict:
print(f"JSON 파일 {source}를 로드합니다")
return {"users": [{"name": "이영희", "score": 85}]}
def process_data(self, data: dict) -> dict:
print("JSON 데이터를 처리합니다")
for user in data["users"]:
user["score"] += 10
return data
def save_data(self, data: dict, destination: str) -> bool:
print(f"처리된 데이터를 {destination}에 저장합니다")
print(f"저장된 데이터: {data}")
return True
csv_processor = CSVProcessor()
json_processor = JSONProcessor()
csv_processor.run_pipeline("input.csv", "output.csv")
print("=" * 50)
json_processor.run_pipeline("input.json", "output.json")
# 데이터 파이프라인 시작
# CSV 파일 input.csv를 로드합니다
# CSV 데이터를 처리합니다
# 처리된 데이터를 output.csv에 저장합니다
# 데이터 파이프라인 완료: 성공
# ==================================================
# JSON 파일 input.json를 로드합니다
# JSON 데이터를 처리합니다
# 처리된 데이터를 output.json에 저장합니다
# 데이터 파이프라인 완료: 성공5.2 알림 시스템
다양한 알림 채널(이메일, SMS, 푸시)을 동일한 인터페이스로 다루는 예다.
from abc import ABC, abstractmethod
from typing import Dict
class NotificationSender(ABC):
@abstractmethod
def send_notification(self, recipient: str, message: str, **kwargs) -> bool:
"""알림을 발송한다"""
pass
@abstractmethod
def validate_recipient(self, recipient: str) -> bool:
"""수신자 정보가 유효한지 검증한다"""
pass
# 구체 메서드: 검증 후 발송 공통 로직
def send_with_validation(self, recipient: str, message: str, **kwargs) -> bool:
if not self.validate_recipient(recipient):
print(f"유효하지 않은 수신자: {recipient}")
return False
return self.send_notification(recipient, message, **kwargs)
class EmailSender(NotificationSender):
def send_notification(self, recipient: str, message: str, **kwargs) -> bool:
subject = kwargs.get("subject", "알림")
print(f"이메일 발송: {recipient}")
print(f"제목: {subject}")
print(f"내용: {message}")
return True
def validate_recipient(self, recipient: str) -> bool:
return "@" in recipient and "." in recipient
class SMSSender(NotificationSender):
def send_notification(self, recipient: str, message: str, **kwargs) -> bool:
print(f"SMS 발송: {recipient}")
print(f"내용: {message}")
return True
def validate_recipient(self, recipient: str) -> bool:
return recipient.isdigit() and len(recipient) >= 10
class PushNotificationSender(NotificationSender):
def send_notification(self, recipient: str, message: str, **kwargs) -> bool:
title = kwargs.get("title", "푸시 알림")
print(f"푸시 알림 발송: {recipient}")
print(f"제목: {title}")
print(f"내용: {message}")
return True
def validate_recipient(self, recipient: str) -> bool:
return len(recipient) > 10 # 디바이스 토큰 길이 검증
class NotificationManager:
def __init__(self):
self.senders: Dict[str, NotificationSender] = {}
def register_sender(self, name: str, sender: NotificationSender):
self.senders[name] = sender
def send_notification(self, sender_type: str, recipient: str, message: str, **kwargs):
if sender_type not in self.senders:
print(f"알 수 없는 발송자 타입: {sender_type}")
return False
sender = self.senders[sender_type]
return sender.send_with_validation(recipient, message, **kwargs)
manager = NotificationManager()
manager.register_sender("email", EmailSender())
manager.register_sender("sms", SMSSender())
manager.register_sender("push", PushNotificationSender())
manager.send_notification("email", "user@example.com", "환영합니다!", subject="회원가입 완료")
manager.send_notification("sms", "01012345678", "인증 코드: 123456")
manager.send_notification("push", "device_token_12345", "새로운 메시지가 있습니다", title="메시지 알림")
# 이메일 발송: user@example.com
# 제목: 회원가입 완료
# 내용: 환영합니다!
# SMS 발송: 01012345678
# 내용: 인증 코드: 123456
# 푸시 알림 발송: device_token_12345
# 제목: 메시지 알림
# 내용: 새로운 메시지가 있습니다6 isinstance()와 ABC
ABC는 isinstance() 및 issubclass() 함수와 함께 사용하여 타입 검증에 활용한다.
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self):
pass
class Circle(Drawable):
def draw(self):
return "원을 그립니다"
class Square(Drawable):
def draw(self):
return "사각형을 그립니다"
def render_shape(shape):
if isinstance(shape, Drawable):
return shape.draw()
else:
raise TypeError("Drawable 객체가 아닙니다")
circle = Circle()
square = Square()
print(render_shape(circle)) # 원을 그립니다
print(render_shape(square)) # 사각형을 그립니다
print(isinstance(circle, Drawable)) # True
print(issubclass(Circle, Drawable)) # True7 ABC의 장단점과 사용 지침
7.1 장점
| 항목 | 설명 |
|---|---|
| 인터페이스 강제 | 하위 클래스에서 반드시 구현해야 할 메서드를 명확히 정의 |
| 코드 문서화 | 클래스의 의도와 계약을 코드 자체로 표현 |
| 조기 오류 감지 | 인터페이스 미구현 시 인스턴스 생성 시점에 즉시 TypeError 발생 |
| 다형성 지원 | 동일한 인터페이스를 구현하는 다양한 클래스들을 일관되게 처리 |
| 설계 개선 | 객체지향 설계 원칙을 강제하여 더 나은 코드 구조 유도 |
7.2 사용 시 고려사항
| 항목 | 설명 |
|---|---|
| 복잡성 | 간단한 프로젝트에서는 과도한 추상화가 될 수 있음 |
| 성능 | 추상화 레이어 추가로 인한 미미한 성능 오버헤드 |
| 학습 곡선 | 초보자에게는 개념 이해가 어려울 수 있음 |
8 정리
추상 클래스는 “이 인터페이스를 반드시 구현하라”는 설계 계약을 코드 수준에서 강제한다.
단순히 상속의 편의를 위한 도구가 아니라,
대규모 팀 개발에서 일관성 있는 인터페이스를 보장하고
구현 누락이라는 실수를 원천적으로 차단하는 설계 도구다.
| 개념 | 역할 |
|---|---|
| 추상화 | 공통점 추출 + 구현 세부사항 숨김 |
| 추상 클래스 | 직접 인스턴스화 불가, 상속을 통해서만 사용 |
| @abstractmethod | 하위 클래스에서 반드시 구현해야 할 메서드 지정 |
| @property + @abstractmethod | 속성처럼 접근 가능한 추상 인터페이스 |
| 설계 계약 | 상속 관계에서 인터페이스 구현을 코드로 보장 |