dataclass와 namedtuple, 값 객체를 만들 때 뭐가 다를까
문제 발생
좌표를 표현하는 작은 값 객체가 필요했는데, namedtuple로 만들지 dataclass로 만들지 결정하지 못해 팀 안에서 코드 스타일이 뒤섞였습니다.
원인 분석
둘 다 "필드 몇 개를 가진 작은 값 객체"를 __init__/__repr__/__eq__ 같은 보일러플레이트 없이 만들 수 있게 해주지만, 설계 목적이 다릅니다.
namedtuple(collections.namedtuple 또는 typing.NamedTuple)은 이름 그대로 튜플입니다.
- 튜플이므로 기본적으로 불변(immutable)입니다 — 필드를 나중에 바꿀 수 없습니다.
- 튜플처럼 인덱스로도 접근 가능하고(
point[0]), 언패킹도 됩니다(x, y = point). - 튜플이 필요한 모든 곳(딕셔너리 키, 셋의 원소 등)에 그대로 쓸 수 있습니다 — 해시 가능합니다.
dataclass(dataclasses.dataclass)는 일반 클래스입니다.
- 기본적으로 가변(mutable)입니다 —
frozen=True옵션을 줘야 불변이 됩니다. - 메서드를 자유롭게 추가할 수 있고, 상속 구조를 만들기 쉽습니다.
- 필드에 기본값,
field(default_factory=...)같은 고급 기능을 쓸 수 있습니다. - 기본적으로 해시 가능하지 않습니다(가변 객체는 보통 해시 불가능해야 안전합니다) — 필요하면
frozen=True와 함께eq=True를 조합해 해시 가능하게 만들 수 있습니다.
해결 방안
- 정말 단순한 불변 값 튜플이 필요하고, 튜플처럼 인덱스/언패킹으로도 쓰고 싶다면
NamedTuple을 씁니다.
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int- 메서드가 필요하거나, 필드가 나중에 바뀌어야 하거나, 상속 구조가 필요하다면
dataclass를 씁니다.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def distance_from_origin(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5- 값 객체가 절대 바뀌면 안 된다는 걸 명확히 하고 싶다면
dataclass(frozen=True)로dataclass도 불변으로 만들 수 있습니다 — "불변이 필요하다"는 이유만으로NamedTuple을 선택할 필요는 없습니다. - 튜플 언패킹/인덱스 접근이 필요 없다면
dataclass가 더 명시적이고 확장하기 쉬운 선택인 경우가 많습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.