Python 데이터클래스 + 검증

Python dataclass로 만드는 도메인 모델. __post_init__ 검증, to_dict/from_dict 직렬화 포함.

Gist
from __future__ import annotations
from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import Optional, List
import re


@dataclass
class Address:
    street: str
    city: str
    country: str
    postal_code: str

    def __post_init__(self) -> None:
        self.postal_code = self.postal_code.strip()
        if not self.postal_code:
            raise ValueError("postal_code cannot be empty")
        self.country = self.country.upper()


@dataclass
class User:
    name: str
    email: str
    age: int
    address: Address
    roles: List[str] = field(default_factory=list)
    bio: Optional[str] = None
    created_at: datetime = field(default_factory=datetime.utcnow)
    is_active: bool = True

    _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

    def __post_init__(self) -> None:
        self.email = self.email.lower().strip()
        if not self._EMAIL_RE.match(self.email):
            raise ValueError(f"Invalid email: {self.email!r}")
        if not (0 <= self.age <= 150):
            raise ValueError(f"Age must be 0–150, got {self.age}")
        if not self.name.strip():
            raise ValueError("name cannot be blank")

    def to_dict(self) -> dict:
        d = asdict(self)
        d["created_at"] = self.created_at.isoformat()
        return d

    @classmethod
    def from_dict(cls, data: dict) -> User:
        data = data.copy()
        data["address"] = Address(**data["address"])
        if "created_at" in data:
            data["created_at"] = datetime.fromisoformat(data["created_at"])
        return cls(**data)

    def add_role(self, role: str) -> None:
        role = role.lower().strip()
        if role and role not in self.roles:
            self.roles.append(role)

    def __repr__(self) -> str:
        return f"User(name={self.name!r}, email={self.email!r}, age={self.age})"