Skip to content

Python Dataclasses: @dataclass 데코레이터 완전 가이드

Updated on

Python 클래스를 작성하다 보면 반복적인 보일러플레이트 코드가 자주 생깁니다. 속성을 초기화하기 위해 __init__를 정의하고, 보기 좋은 출력용 __repr__, 비교를 위한 __eq__, 그리고 경우에 따라 해시 가능성을 위한 __hash__까지 구현하게 됩니다. 이런 수동 구현은 설정 객체, API 응답, 데이터베이스 레코드처럼 “데이터를 담는” 클래스에서는 특히 번거롭습니다.

Python 3.7에서는 PEP 557을 통해 dataclasses가 도입되어, 일반 클래스의 유연성은 유지하면서도 이 보일러플레이트를 자동화할 수 있게 되었습니다. @dataclass 데코레이터는 타입 어노테이션을 기반으로 특수 메서드를 자동 생성해 수십 줄의 코드를 몇 줄로 줄여줍니다. 이 가이드는 dataclasses를 활용해 더 깔끔하고 유지보수하기 쉬운 Python 코드를 작성하는 방법을 보여줍니다.

📚

Dataclasses가 존재하는 이유: 보일러플레이트 문제 해결

전통적인 Python 클래스는 흔히 사용하는 작업을 위해 메서드를 명시적으로 정의해야 합니다. 예를 들어 사용자 데이터를 저장하는 표준 클래스를 보면 다음과 같습니다.

class User:
    def __init__(self, name, email, age):
        self.name = name
        self.email = email
        self.age = age
 
    def __repr__(self):
        return f"User(name={self.name!r}, email={self.email!r}, age={self.age!r})"
 
    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return (self.name, self.email, self.age) == (other.name, other.email, other.age)

dataclasses를 사용하면 다음처럼 줄어듭니다.

from dataclasses import dataclass
 
@dataclass
class User:
    name: str
    email: str
    age: int

데코레이터는 타입 어노테이션을 바탕으로 __init__, __repr__, __eq__를 자동 생성합니다. 기능은 동일하게 유지하면서 15줄 이상의 보일러플레이트가 사라집니다.

기본 @dataclass 문법

가장 단순한 dataclass는 필드에 타입 어노테이션만 있으면 됩니다.

from dataclasses import dataclass
 
@dataclass
class Product:
    name: str
    price: float
    quantity: int
 
product = Product("Laptop", 999.99, 5)
print(product)  # Product(name='Laptop', price=999.99, quantity=5)
 
product2 = Product("Laptop", 999.99, 5)
print(product == product2)  # True

데코레이터는 동작을 커스터마이즈하기 위한 파라미터도 받습니다.

@dataclass(
    init=True,       # __init__ 생성 (기본값: True)
    repr=True,       # __repr__ 생성 (기본값: True)
    eq=True,         # __eq__ 생성 (기본값: True)
    order=False,     # 비교 메서드 생성 (기본값: False)
    frozen=False,    # 인스턴스를 불변(immutable)으로 만들기 (기본값: False)
    unsafe_hash=False  # __hash__ 생성 (기본값: False)
)
class Config:
    host: str
    port: int

필드 타입과 기본값

dataclasses는 필드 기본값을 지원합니다. 기본값이 없는 필드는 기본값이 있는 필드보다 앞에 와야 합니다.

from dataclasses import dataclass
 
@dataclass
class Server:
    host: str
    port: int = 8080
    protocol: str = "http"
 
server1 = Server("localhost")
print(server1)  # Server(host='localhost', port=8080, protocol='http')
 
server2 = Server("api.example.com", 443, "https")
print(server2)  # Server(host='api.example.com', port=443, protocol='https')

리스트/딕셔너리 같은 변경 가능한(mutable) 기본값은 공유 참조 문제를 피하기 위해 default_factory를 사용해야 합니다.

from dataclasses import dataclass, field
 
# WRONG - 모든 인스턴스가 같은 리스트를 공유
@dataclass
class WrongConfig:
    tags: list = []  # Raises error in Python 3.10+
 
# CORRECT - 각 인스턴스가 새로운 리스트를 가짐
@dataclass
class CorrectConfig:
    tags: list = field(default_factory=list)
    metadata: dict = field(default_factory=dict)
 
config1 = CorrectConfig()
config2 = CorrectConfig()
 
config1.tags.append("production")
print(config1.tags)  # ['production']
print(config2.tags)  # [] - separate list

field() 함수: 고급 필드 설정

field() 함수는 개별 필드를 세밀하게 제어할 수 있게 해줍니다.

from dataclasses import dataclass, field
from typing import List
 
@dataclass
class Employee:
    name: str
    employee_id: int
    salary: float = field(repr=False)  # repr에서 salary 숨김
    skills: List[str] = field(default_factory=list)
    _internal_id: str = field(init=False, repr=False)  # __init__에 포함되지 않음
    performance_score: float = field(default=0.0, compare=False)  # 비교에서 제외
 
    def __post_init__(self):
        self._internal_id = f"EMP_{self.employee_id:06d}"
 
emp = Employee("Alice", 12345, 85000.0, ["Python", "SQL"])
print(emp)  # Employee(name='Alice', employee_id=12345, skills=['Python', 'SQL'], performance_score=0.0)
print(emp._internal_id)  # EMP_012345

주요 field() 파라미터:

ParameterTypeDescription
defaultAny필드 기본값
default_factoryCallable기본값을 반환하는 인자 없는 함수
initbool__init__에 포함 (기본값: True)
reprbool__repr__에 포함 (기본값: True)
comparebool비교 메서드에 포함 (기본값: True)
hashbool__hash__에 포함 (기본값: None)
metadatadict임의 메타데이터(dataclasses 모듈이 사용하지는 않음)
kw_onlybool필드를 keyword-only로 강제 (Python 3.10+)

metadata 파라미터는 fields()를 통해 접근 가능한 임의 정보를 저장합니다.

from dataclasses import dataclass, field, fields
 
@dataclass
class APIRequest:
    endpoint: str = field(metadata={"description": "API endpoint path"})
    method: str = field(default="GET", metadata={"choices": ["GET", "POST", "PUT", "DELETE"]})
 
for f in fields(APIRequest):
    print(f"{f.name}: {f.metadata}")
# endpoint: {'description': 'API endpoint path'}
# method: {'choices': ['GET', 'POST', 'PUT', 'DELETE']}

Dataclasses에서의 타입 어노테이션

dataclasses는 타입 어노테이션을 활용하지만, 런타임에 타입을 강제하진 않습니다. 복잡한 타입은 typing 모듈을 사용하세요.

from dataclasses import dataclass
from typing import List, Dict, Optional, Union, Tuple
from datetime import datetime
 
@dataclass
class DataAnalysisJob:
    job_id: str
    dataset_path: str
    columns: List[str]
    filters: Dict[str, Union[str, int, float]]
    output_format: str = "csv"
    created_at: datetime = field(default_factory=datetime.now)
    completed_at: Optional[datetime] = None
    error_message: Optional[str] = None
    results: Optional[Dict[str, Tuple[float, float]]] = None
 
job = DataAnalysisJob(
    job_id="job_001",
    dataset_path="/data/sales.csv",
    columns=["date", "revenue", "region"],
    filters={"year": 2026, "region": "US"}
)

런타임 타입 체크가 필요하다면 pydantic 같은 라이브러리를 통합하거나 __post_init__에서 검증 로직을 추가하세요.

frozen=True: 불변(immutable) Dataclass 만들기

frozen=True를 설정하면 생성 이후 인스턴스를 수정할 수 없게 되어 named tuple과 비슷한 성질을 갖습니다.

from dataclasses import dataclass
 
@dataclass(frozen=True)
class Point:
    x: float
    y: float
 
    def distance_from_origin(self):
        return (self.x**2 + self.y**2) ** 0.5
 
point = Point(3.0, 4.0)
print(point.distance_from_origin())  # 5.0
 
# Attempting to modify raises FrozenInstanceError
try:
    point.x = 5.0
except AttributeError as e:
    print(f"Error: {e}")  # Error: cannot assign to field 'x'

Frozen dataclass는 모든 필드가 hashable이면 기본적으로 hashable이 되어, set이나 dict 키로도 사용할 수 있습니다.

@dataclass(frozen=True)
class Coordinate:
    latitude: float
    longitude: float
 
locations = {
    Coordinate(40.7128, -74.0060): "New York",
    Coordinate(51.5074, -0.1278): "London"
}
 
print(locations[Coordinate(40.7128, -74.0060)])  # New York

post_init 메서드: 검증과 계산 필드

__post_init____init__ 이후에 실행되며, 검증이나 계산된 필드 초기화에 사용할 수 있습니다.

from dataclasses import dataclass, field
from datetime import datetime
 
@dataclass
class BankAccount:
    account_number: str
    balance: float
    created_at: datetime = field(default_factory=datetime.now)
    account_type: str = field(init=False)
 
    def __post_init__(self):
        if self.balance < 0:
            raise ValueError("Initial balance cannot be negative")
 
        # Compute account_type based on balance
        if self.balance >= 100000:
            self.account_type = "Premium"
        elif self.balance >= 10000:
            self.account_type = "Gold"
        else:
            self.account_type = "Standard"
 
account = BankAccount("ACC123456", 50000.0)
print(account.account_type)  # Gold

다른 필드에 의존하는 init=False 필드는 __post_init__에서 설정하세요.

from dataclasses import dataclass, field
 
@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)
    perimeter: float = field(init=False)
 
    def __post_init__(self):
        self.area = self.width * self.height
        self.perimeter = 2 * (self.width + self.height)
 
rect = Rectangle(5.0, 3.0)
print(f"Area: {rect.area}, Perimeter: {rect.perimeter}")  # Area: 15.0, Perimeter: 16.0

Dataclasses에서의 상속

dataclasses는 필드를 자동으로 병합하는 방식으로 상속을 지원합니다.

from dataclasses import dataclass
 
@dataclass
class Animal:
    name: str
    age: int
 
@dataclass
class Dog(Animal):
    breed: str
    is_good_boy: bool = True
 
dog = Dog("Buddy", 5, "Golden Retriever")
print(dog)  # Dog(name='Buddy', age=5, breed='Golden Retriever', is_good_boy=True)

서브클래스는 부모 필드를 상속받고 새 필드를 추가할 수 있습니다. 단, 상속 관계 전체에서 “기본값 없는 필드가 기본값 있는 필드 뒤에 올 수 없다”는 규칙은 동일하게 적용됩니다.

from dataclasses import dataclass
 
@dataclass
class BaseConfig:
    environment: str = "production"
 
# ERROR: Non-default field 'api_key' cannot follow default field 'environment'
# @dataclass
# class APIConfig(BaseConfig):
#     api_key: str
 
# CORRECT: 기본값을 주거나 필드 순서를 조정
@dataclass
class APIConfig(BaseConfig):
    api_key: str = ""  # 기본값 제공
    timeout: int = 30

Python 3.10+에서는 kw_only로 이 문제를 해결할 수 있습니다.

from dataclasses import dataclass
 
@dataclass
class BaseConfig:
    environment: str = "production"
 
@dataclass(kw_only=True)
class APIConfig(BaseConfig):
    api_key: str  # keyword argument로만 전달 가능
    timeout: int = 30
 
config = APIConfig(api_key="secret_key_123")  # OK
# config = APIConfig("secret_key_123")  # TypeError

slots=True: 메모리 효율 (Python 3.10+)

Python 3.10에서 slots=True가 추가되어 __slots__를 정의할 수 있고, 메모리 오버헤드를 줄일 수 있습니다.

from dataclasses import dataclass
import sys
 
@dataclass
class RegularUser:
    username: str
    email: str
    age: int
 
@dataclass(slots=True)
class SlottedUser:
    username: str
    email: str
    age: int
 
regular = RegularUser("john", "john@example.com", 30)
slotted = SlottedUser("jane", "jane@example.com", 28)
 
print(f"Regular: {sys.getsizeof(regular.__dict__)} bytes")  # ~104 bytes
print(f"Slotted: {sys.getsizeof(slotted)} bytes")          # ~64 bytes

slots를 사용하는 dataclass는 30~40% 메모리 절감과 더 빠른 속성 접근을 제공하지만, 동적 속성 추가는 포기해야 합니다.

regular.new_attribute = "allowed"  # OK
# slotted.new_attribute = "error"  # AttributeError

kw_only=True: Keyword-only 필드 (Python 3.10+)

모든 필드를 keyword-only로 강제하면 인스턴스 생성이 더 명확해집니다.

from dataclasses import dataclass
 
@dataclass(kw_only=True)
class DatabaseConnection:
    host: str
    port: int
    username: str
    password: str
    database: str = "default"
 
# keyword arguments를 사용해야 함
conn = DatabaseConnection(
    host="localhost",
    port=5432,
    username="admin",
    password="secret"
)
 
# 위치 인자는 TypeError 발생
# conn = DatabaseConnection("localhost", 5432, "admin", "secret")

kw_only를 필드 단위로 섞어 쓸 수도 있습니다.

from dataclasses import dataclass, field
 
@dataclass
class MixedArgs:
    required_positional: str
    optional_positional: int = 0
    required_keyword: str = field(kw_only=True)
    optional_keyword: bool = field(default=False, kw_only=True)
 
obj = MixedArgs("value", 10, required_keyword="kw_value")

비교: dataclass vs 대안들

FeaturedataclassnamedtupleTypedDictPydanticattrs
Mutability기본 MutableImmutableN/A (dict subclass)Mutable설정 가능
Type validation어노테이션만없음어노테이션만런타임 검증런타임 검증
Default values가능가능불가가능가능
Methods완전한 class 지원제한적없음완전한 class 지원완전한 class 지원
Inheritance가능불가제한적가능가능
Memory overhead중간낮음낮음높음중간
Slots support가능 (3.10+)불가불가가능가능
Performance빠름가장 빠름빠름느림(검증)빠름
Built-in예 (3.7+)예 (3.8+)아니오아니오

dataclasses를 선택하면 좋은 경우:

  • 의존성 없는 일반 Python 프로젝트
  • 타입 힌트가 있는 단순 데이터 컨테이너
  • frozen/mutable 유연성이 필요할 때
  • 상속 계층이 있는 모델

Pydantic을 선택하면 좋은 경우:

  • API 요청/응답 검증
  • 엄격한 검증이 필요한 설정 관리
  • JSON schema 생성

namedtuple을 선택하면 좋은 경우:

  • 가벼운 불변 컨테이너
  • 최대 메모리 효율
  • Python < 3.7 호환성이 필요할 때

딕셔너리로/딕셔너리에서 변환

dataclasses는 직렬화를 위해 asdict()astuple()을 제공합니다.

from dataclasses import dataclass, asdict, astuple
 
@dataclass
class Config:
    host: str
    port: int
    ssl_enabled: bool = True
 
config = Config("api.example.com", 443)
 
# 딕셔너리로 변환
config_dict = asdict(config)
print(config_dict)  # {'host': 'api.example.com', 'port': 443, 'ssl_enabled': True}
 
# 튜플로 변환
config_tuple = astuple(config)
print(config_tuple)  # ('api.example.com', 443, True)

중첩 dataclass에도 동작합니다.

from dataclasses import dataclass, asdict
 
@dataclass
class Address:
    street: str
    city: str
    zipcode: str
 
@dataclass
class Person:
    name: str
    address: Address
 
person = Person("Alice", Address("123 Main St", "Springfield", "12345"))
person_dict = asdict(person)
print(person_dict)
# {'name': 'Alice', 'address': {'street': '123 Main St', 'city': 'Springfield', 'zipcode': '12345'}}

Dataclasses와 JSON 직렬화

dataclasses는 JSON 직렬화를 네이티브로 제공하진 않지만, 연동은 간단합니다.

import json
from dataclasses import dataclass, asdict
from datetime import datetime
 
@dataclass
class Event:
    name: str
    timestamp: datetime
    attendees: int
 
    def to_json(self):
        data = asdict(self)
        # datetime에 대한 커스텀 직렬화
        data['timestamp'] = self.timestamp.isoformat()
        return json.dumps(data)
 
    @classmethod
    def from_json(cls, json_str):
        data = json.loads(json_str)
        data['timestamp'] = datetime.fromisoformat(data['timestamp'])
        return cls(**data)
 
event = Event("Python Conference", datetime.now(), 500)
json_str = event.to_json()
print(json_str)
 
restored = Event.from_json(json_str)
print(restored)

더 복잡한 시나리오에서는 dataclasses-json 라이브러리나 Pydantic을 사용하세요.

실전 패턴

설정 객체(Configuration Objects)

from dataclasses import dataclass, field
from typing import List
 
@dataclass
class AppConfig:
    app_name: str
    version: str
    debug: bool = False
    allowed_hosts: List[str] = field(default_factory=lambda: ["localhost"])
    database_url: str = "sqlite:///app.db"
    cache_timeout: int = 300
 
    def __post_init__(self):
        if self.debug:
            print(f"Running {self.app_name} v{self.version} in DEBUG mode")
 
config = AppConfig("DataAnalyzer", "2.1.0", debug=True)

API 응답 모델

from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
 
@dataclass
class APIResponse:
    status: str
    data: Optional[List[dict]] = None
    error_message: Optional[str] = None
    timestamp: datetime = field(default_factory=datetime.now)
 
    @property
    def is_success(self):
        return self.status == "success"
 
response = APIResponse("success", data=[{"id": 1, "name": "Dataset A"}])
print(response.is_success)  # True

PyGWalker 통합을 활용한 데이터베이스 레코드

from dataclasses import dataclass, asdict
from typing import List
import pandas as pd
 
@dataclass
class SalesRecord:
    date: str
    product: str
    revenue: float
    region: str
    quantity: int
 
# Create sample data
records = [
    SalesRecord("2026-01-01", "Laptop", 1299.99, "US", 5),
    SalesRecord("2026-01-02", "Mouse", 29.99, "EU", 50),
    SalesRecord("2026-01-03", "Keyboard", 89.99, "US", 20),
]
 
# Convert to DataFrame for visualization with PyGWalker
df = pd.DataFrame([asdict(r) for r in records])
 
# Use PyGWalker for interactive data exploration
# import pygwalker as pyg
# walker = pyg.walk(df)
# This creates a Tableau-like interface to visualize your dataclass-based data

dataclasses는 시각화 이전 단계에서 데이터를 구조화하는 데 매우 유용합니다. PyGWalker는 DataFrame을 인터랙티브한 시각 인터페이스로 변환해 주므로, dataclass 기반 데이터 분석 워크플로우를 매끄럽게 이어줍니다.

일반 클래스 대비 성능 벤치마크

import timeit
from dataclasses import dataclass
 
# Regular class
class RegularClass:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
 
    def __repr__(self):
        return f"RegularClass(x={self.x}, y={self.y}, z={self.z})"
 
    def __eq__(self, other):
        return (self.x, self.y, self.z) == (other.x, other.y, other.z)
 
@dataclass
class DataClass:
    x: int
    y: int
    z: int
 
# Benchmark instantiation
regular_time = timeit.timeit(lambda: RegularClass(1, 2, 3), number=1000000)
dataclass_time = timeit.timeit(lambda: DataClass(1, 2, 3), number=1000000)
 
print(f"Regular class: {regular_time:.4f}s")
print(f"Dataclass: {dataclass_time:.4f}s")
# Dataclasses are typically 5-10% slower due to decorator overhead
# but provide significantly cleaner code

slots=True(Python 3.10+)를 사용하면 dataclass는 일반 클래스의 성능에 근접하거나 앞서면서, 메모리 사용량도 30~40% 줄일 수 있습니다.

고급 패턴: 커스텀 필드 정렬

from dataclasses import dataclass, field
 
def sort_by_priority(items):
    return sorted(items, key=lambda x: x.priority, reverse=True)
 
@dataclass(order=True)
class Task:
    priority: int
    name: str = field(compare=False)
    description: str = field(compare=False)
 
tasks = [
    Task(3, "Review PR", "Code review for feature X"),
    Task(1, "Write docs", "Documentation update"),
    Task(5, "Fix bug", "Critical production issue"),
]
 
sorted_tasks = sorted(tasks)
for task in sorted_tasks:
    print(f"Priority {task.priority}: {task.name}")
# Priority 1: Write docs
# Priority 3: Review PR
# Priority 5: Fix bug

모범 사례와 주의사항

  1. mutable 기본값에는 항상 default_factory를 사용: [], {}를 직접 할당하지 말 것
  2. 타입 힌트는 필수: dataclass는 값이 아니라 어노테이션에 의존
  3. 필드 순서가 중요: 기본값 없는 필드를 앞에 배치
  4. 불변 데이터에는 frozen=True 사용: hashable 객체, 스레드 안전성에 유리
  5. __post_init__는 최소한으로: 로직이 과하면 dataclass의 단순함이 무너짐
  6. 대규모 데이터에는 slots=True 고려: Python 3.10+에서 큰 메모리 절감
  7. __post_init__에서 검증 수행: dataclass는 런타임 타입을 강제하지 않음

FAQ

결론

Python dataclasses는 클래스의 모든 장점을 유지하면서 보일러플레이트 코드를 제거합니다. @dataclass 데코레이터는 초기화, 표현, 비교 메서드를 자동으로 생성해 개발 시간과 유지보수 부담을 줄여줍니다. 설정 객체부터 API 모델, 데이터베이스 레코드에 이르기까지 dataclasses는 타입 어노테이션 기반으로 데이터를 담는 클래스를 깔끔하게 설계하는 방법을 제공합니다.

핵심 장점은 자동 메서드 생성, field()를 통한 필드 동작 커스터마이즈, frozen=True로 불변성 제공, __post_init__로 검증/계산 처리, slots=True로 메모리 효율 개선입니다. namedtuple이나 Pydantic 같은 대안도 특정 목적에 유리하지만, dataclasses는 대부분의 Python 프로젝트에서 단순성과 기능성 사이의 균형이 뛰어납니다.

데이터 분석 워크플로우에서는 dataclasses를 PyGWalker 같은 도구와 결합해, 구조화된 데이터 모델을 인터랙티브 시각화로 바로 연결하는 강력한 파이프라인을 만들 수 있습니다. 이를 통해 데이터 수집부터 인사이트 도출까지의 흐름을 크게 단순화할 수 있습니다.

📚