7편까지로 스크립트 자동화를 경험했다면, 이제는 코드를 구조화해 재사용성과 유지보수성을 높일 차례입니다. 이번 글에서는 Python의 클래스 문법으로 상태와 동작을 묶고, dataclasses 모듈로 데이터 중심 객체를 빠르게 만드는 패턴을 정리합니다.
이번 글에서 새로 나오는 용어
- 인스턴스: 클래스로부터 만들어진 실제 객체 한 개를 의미하며
self가 가리키는 대상 - dataclass: 데이터 담는 클래스를 자동으로 생성자·비교 메서드까지 만들어 주는 표준 라이브러리 데코레이터
- slots: 클래스가 가질 수 있는 속성 이름을 미리 고정해 메모리와 오타를 관리하는 Python 기능
- Protocol: "이 메서드를 제공한다"라는 규격만 선언해 여러 구현체가 같은 행동을 하도록 강제하는 타입 힌트 도구
핵심 개념
학습 메모
- 소요 시간: 60분
- 준비물: 함수·모듈 분리 경험, requests·파일 IO 감각
- 학습 목표: 상태·행동을 묶는 클래스와
dataclass모델을 정의해 변환 메서드 작성하기
클래스는 상태와 행동을 결합하는 틀이고, dataclass는 데이터를 담기 위한 클래스를 손쉽게 만드는 도구입니다.
코드로 따라하기
가장 기본적인 클래스
class MealFetcher:
def __init__(self, school_code: str):
self.school_code = school_code
def build_url(self, date: str) -> str:
return f"https://school.example/api/meals/{self.school_code}?date={date}"
def parse(self, raw: dict) -> list[str]:
return raw["menu"]
클래스는 상태(self.school_code)와 행동(build_url, parse)을 묶어, 이후 CLI나 배치 스크립트에서도 동일한 로직을 재사용할 수 있게 해 줍니다.
인스턴스 생성과 메서드 호출
fetcher = MealFetcher("demo-high")
today_menu = fetcher.parse({"menu": ["돈까스", "샐러드"]})
self는 메서드를 호출한 인스턴스를 가리키므로 명시적으로 넘기지 않습니다. IDE 자동완성, 타입 힌트 등과 잘 작동하기 위해 typing 모듈과 함께 사용하면 좋습니다.
클래스 메서드와 팩토리 패턴
from datetime import date
class MealFetcher:
@classmethod
def for_today(cls, school_code: str) -> "MealFetcher":
instance = cls(school_code)
instance.query_date = date.today().isoformat()
return instance
@classmethod는 클래스를 첫 번째 인자로 받아 대체 생성자(factory)를 만들 때 유용합니다. API 토큰이나 공통 설정을 묶는 Config 객체에도 자주 쓰입니다.
dataclasses로 데이터 모델 정의
from dataclasses import dataclass
@dataclass(slots=True)
class Meal:
menu: list[str]
calories: int
origin: list[str]
@dataclass는 반복적인 __init__, __repr__, 비교 메서드를 자동 생성합니다. slots=True를 쓰면 메모리 사용이 감소하고 오타로 인한 속성 추가를 막을 수 있습니다.
slots는 허용된 속성 이름을 고정하는 Python 문법입니다. 목록에 없는 속성을 만들 수 없게 막아 실수와 메모리 낭비를 줄입니다.
🧩 dataclass 한 줄 정의 데이터 중심 클래스를 빠르게 정의할 수 있도록 생성자와 표현 메서드를 자동으로 만들어 주는 표준 라이브러리 데코레이터입니다.
기본 값과 변환 메서드
from dataclasses import dataclass, field
@dataclass
class Meal:
menu: list[str]
calories: int = 0
origin: list[str] = field(default_factory=list)
@classmethod
def from_json(cls, payload: str) -> "Meal":
data = json.loads(payload)
return cls(menu=data["menu"], calories=data.get("calories", 0))
def to_markdown(self) -> str:
return "\n".join(f"- {item}" for item in self.menu)
field(default_factory=list)는 리스트 같은 가변 객체를 안전하게 초기화할 때 필수입니다. 변환 메서드(from_json, to_markdown)를 붙이면 추후 API 응답과 템플릿 렌더링 사이 브릿지 역할을 수행합니다.
상속과 프로토콜
from typing import Protocol
class Notifier(Protocol):
def notify(self, message: str) -> None: ...
class SlackNotifier:
def __init__(self, webhook: str):
self.webhook = webhook
def notify(self, message: str) -> None:
print("send to slack", message)
추상 클래스 대신 Protocol을 사용하면 duck typing과 정적 타입 체크 모두를 만족할 수 있습니다. Protocol은 "이 메서드를 제공한다"라는 규격을 정의하기 때문에, 알림 채널을 바꿔 끼우거나 테스트 더블을 만들 때도 타입 안정성이 유지됩니다.
📌 Protocol이란? 클래스가 어떤 속성과 메서드를 제공해야 하는지 시그니처만 선언하는 인터페이스 규격입니다. 구현체는 다르지만 같은 행동을 보장하고 싶을 때 사용합니다.
이처럼 클래스 간 책임을 도식화하면 어떤 객체가 데이터를 만들고, 누가 알림을 보내며, 어느 지점이 Protocol로 추상화되는지 금방 파악할 수 있습니다.
왜 중요한가
클래스로 책임을 나누면 API 호출, 데이터 파싱, 알림 전송 같은 기능을 독립적으로 발전시킬 수 있습니다. dataclass 변환 메서드를 더하면 JSON과 Markdown 사이를 손쉽게 오가며 자동화 결과를 사람이 읽을 수 있는 형태로 바꿀 수 있습니다.
실습
- 따라 하기:
MealFetcher와Meal클래스를 그대로 작성해to_markdown()출력이 나오는지 확인합니다. - 확장하기:
Notifier프로토콜을 구현한ConsoleNotifier,FileNotifier를 추가하고MealFetcher가 알림을 보내도록 의존성을 주입합니다. - 디버깅:
dataclass에서list기본값을 직접 넣어mutable default경고를 확인한 뒤field(default_factory=list)로 고칩니다. - 완료 기준: 최소 두 개의 클래스를 협력시키고
dataclass변환 메서드가 JSON 문자열과 객체를 왕복할 수 있을 때입니다.
마무리
클래스와 dataclasses를 활용해 데이터를 구조화하면 API 호출, 캐싱, 알림 로직이 명확한 모듈로 나뉩니다. 다음 편에서는 이 구조를 안정적으로 유지하기 위한 예외 처리와 로깅 전략에 집중해, 운영 환경에서 신뢰할 수 있는 코드로 다듬어 보겠습니다.
💬 댓글
이 글에 대한 의견을 남겨주세요