문서 읽는 데 58분 · D4

D-4: dataclass와 설계

목차 29
전체 22강 중 14강 · 파이썬 기초
난이도 · 입문

ℹ️프로그래밍 경험이 없어도 시작할 수 있는 파이썬 입문 — ‘개발 입문’을 먼저 들으면 더 수월해요. AI·데이터 분석으로 이어지는 출발점이에요.

안녕하세요, 홍순구입니다. 지난 시간엔 우리가 만든 게시물에 파이썬의 기본 문법을 가르쳤어요. __eq__== 비교를, __lt__ 로 정렬을, __len__ 으로 len() 을 연결하고, @property 로 메서드를 속성처럼 쓰는 법까지 익혔죠.

그런데 카테고리 D 를 거치며 클래스를 만들 때마다 했던 일을 떠올려 보세요. __init__ 으로 속성을 일일이 담고, __repr__ 로 출력 모양을 직접 쓰고, 지난 시간엔 __eq__ 까지 손으로 적었어요. 회원·게시물·댓글처럼 "데이터를 담는" 클래스를 만들 때마다 이 비슷한 코드를 매번 반복하는 건 번거롭죠.

오늘은 이 반복을 @dataclass 한 줄로 자동화해요. 클래스 위에 데코레이터 하나만 붙이면 __init__·__eq__·__repr__ 을 파이썬이 알아서 만들어 줘요. 거기에 값을 잠그는 frozen, 정해진 값만 다루는 Enum 까지 더해 인스타그램 도메인을 한층 단단하게 설계합니다.

텍스트
 [지난 시간 D-3]  손으로 쓴 __init__·__eq__·__repr__ + @property
                 매직 메서드로 객체에 파이썬 문법을 가르쳤다

 [오늘 D-4]      그 반복을 @dataclass 한 줄로 자동화한다
                 ├─ @dataclass    __init__·__eq__·__repr__ 자동 생성
                 ├─ field          기본값 · 가변 기본값(default_factory)
                 ├─ frozen / slots 못 바꾸게 잠그고, 오타를 막는다
                 ├─ __post_init__  객체가 태어난 직후 값 다듬기
                 └─ Enum           정해진 값(공개·비공개·삭제됨)만

💡 오늘 수업의 핵심 — "반복되는 클래스 코드를 한 줄로 자동화한다"

데이터를 담는 클래스는 __init__·__eq__·__repr__ 가 거의 똑같이 반복돼요. @dataclass 는 이걸 자동으로 만들어 주고, 가변 기본값 함정 같은 흔한 실수도 막아줘요. 여기에 값을 잠그는 frozen, 정해진 값을 묶는 Enum 을 더하면, 짧고 안전한 코드로 도메인을 설계할 수 있어요. 마지막엔 "언제 dataclass 를 쓰고 언제 일반 클래스가 나은지" 판단 기준까지 잡습니다.

🎯 학습 목표

  • @dataclass__init__·__eq__·__repr__ 을 자동 생성하고, field(default_factory) 로 가변 기본값을 안전하게 다룬다.
  • frozen(불변·해시 가능)·slots(오타 방지)·__post_init__(값 다듬기)로 dataclass 를 더 단단하게 만든다.
  • Enum/StrEnum 으로 정해진 값을 묶고, dataclass 와 일반 클래스를 언제 골라 쓸지 기준을 세운다.

Step 1: "한 줄로 끝나는 클래스" — @dataclass 의 등장

지난 시간까지 우리는 클래스를 만들 때마다 __init__ 으로 속성을 담고, 출력을 위해 __repr__ 을, 비교를 위해 __eq__ 를 손으로 적었어요. 회원을 표현하는 Member 를 그 방식으로 만들면 이렇게 돼요.

Python
# src/instagram/d4_dataclass_intro.py
from dataclasses import dataclass


# (1) 손으로 다 쓴 버전 — D-1~D-3 에서 하던 방식
class ManualMember:
    def __init__(self, username, followers):
        self.username = username
        self.followers = followers

    def __repr__(self):
        return f"ManualMember(username='{self.username}', followers={self.followers})"

    def __eq__(self, other):
        return self.username == other.username and self.followers == other.followers

속성이 단 두 개인데도 같은 이름(username·followers)을 자그마치 여섯 번이나 반복해서 적었어요. 속성이 다섯 개, 열 개로 늘어나면 이 반복은 더 길어지고, 하나라도 빠뜨리면 버그가 돼요.

이걸 @dataclass 로 바꾸면 이렇게 줄어들어요.

Python
# (2) @dataclass 버전 — 한 줄로 위 셋을 자동 생성
@dataclass
class Member:
    username: str
    followers: int

class 위에 @dataclass 한 줄을 붙이고, 담을 값의 이름과 종류만 적었어요. 그랬더니 파이썬이 __init__·__repr__·__eq__ 를 알아서 다 만들어 줘요. 실행해서 확인해 볼게요.

Python
m = Member("minji", 8500)
print(m)                                   # 자동 생성된 __repr__
print(f"minji == 같은값? {m == Member('minji', 8500)}")   # 자동 생성된 __eq__
print(f"minji == 다른값? {m == Member('jaehoon', 320)}")
텍스트
Member(username='minji', followers=8500)
minji == 같은값? True
minji == 다른값? False

__repr__ 을 직접 안 썼는데도 Member(username='minji', followers=8500) 처럼 속성이 다 보이게 출력되죠. == 도 마찬가지예요. 지난 시간 손으로 적었던 "작성자와 캡션이 같으면 같다" 같은 __eq__ 를, @dataclass 는 "모든 필드가 같으면 같다" 로 자동으로 만들어 줘요. 우리가 손으로 하던 일을 데코레이터 하나가 대신 해준 거예요.

여기서 새로 등장한 게 하나 있어요. 필드 이름 옆의 : str, : int 예요.

💡 한 줄 정리

@dataclass 를 클래스 위에 붙이면, 필드 이름과 종류만 적어도 __init__·__repr__·__eq__ 를 파이썬이 자동으로 만들어 줘요. 손으로 반복하던 코드가 사라져요.

🙋 학생 질문 — "필드 옆에 붙은 : str, : int 는 뭐예요?"

이건 타입 어노테이션(type annotation)이라고 해요. "이 필드엔 문자열(str)이 들어가요", "이건 정수(int)예요" 하고 종류를 적어두는 표시예요. @dataclass 는 이 표시를 보고 어떤 필드가 있는지 파악해서 __init__ 을 만들기 때문에, dataclass 에선 타입 어노테이션이 문법의 일부예요(빠뜨리면 필드로 인식하지 않아요).

타입 어노테이션 자체를 본격적으로 다루는 건 한참 뒤(F-1, 타입 힌트)예요. 거기선 list[int] 같은 더 정교한 표기와, 틀린 타입에 빨간 줄을 그어주는 도구까지 배워요. 지금은 "dataclass 가 필드를 알아보게 해주는 이름표" 정도로만 생각하면 충분해요. 참고로 이 표기는 그저 안내하는 표시일 뿐, followers 에 문자열을 넣어도 파이썬이 실행을 막지는 않아요(그 얘기는 Step 7 에서 다시 해요).


Step 2: "기본값을 주고 싶어요" — field 와 default_factory

회원마다 자기소개(bio)는 비어 있을 수도 있고, 관심사(interests)는 여러 개일 수도 있어요. 매번 다 채워서 만들기 번거로우니, 안 주면 알아서 쓰일 기본값을 정해 둘게요. 숫자나 문자열 같은 단순한 값은 그냥 = 0, = "" 처럼 적으면 돼요.

Python
# src/instagram/d4_defaults.py
from dataclasses import dataclass, field


@dataclass
class Member:
    username: str
    followers: int = 0
    bio: str = ""
    interests: list[str] = field(default_factory=list)

followers = 0, bio = "" 는 단순하죠. 그런데 마지막 interests 가 좀 달라요. 빈 리스트를 기본값으로 주고 싶은데, interests: list[str] = [] 라고 직접 쓰지 않고 field(default_factory=list) 라는 낯선 걸 썼어요. 왜 그럴까요?

C-1(함수) 에서 봤던 가변 기본값 함정을 떠올려 보세요. def add_tag(tag, tags=[]) 처럼 빈 리스트를 기본값으로 주면, 그 리스트가 호출마다 새로 생기는 게 아니라 하나를 모두가 공유해서 값이 누적되는 함정이었죠. dataclass 도 똑같은 위험이 있어요. 그래서 파이썬은 아예 사고가 나기 전에 막아줘요. 리스트를 기본값으로 직접 쓰면 클래스를 정의하는 순간 에러가 나요.

Python
# src/instagram/d4_mutable_default_crash.py
@dataclass
class Member:
    username: str
    interests: list[str] = []   # ← 가변 기본값을 직접! 정의하는 순간 ValueError

실행하면 마지막 줄에 이런 에러가 나요.

텍스트
ValueError: mutable default <class 'list'> for field interests is not allowed: use default_factory

"리스트 같은 가변 기본값은 안 되니 default_factory 를 써라" 고 친절하게 알려주죠. field(default_factory=list) 는 "기본값이 필요할 때마다 list 를 새로 불러서 빈 리스트를 만들어 줘" 라는 뜻이에요. 그래서 인스턴스마다 자기만의 새 리스트를 갖게 돼요. 정말 그런지 확인해 볼게요.

Python
a = Member("minji")                 # 기본값만으로 생성
b = Member("jaehoon")
a.interests.append("여행")          # a 에만 추가했는데…
print(f"a.interests = {a.interests}")   # ['여행']
print(f"b.interests = {b.interests}")   # [] — 서로 섞이지 않는다!
print(f"기본 bio = {a.bio!r}, 기본 followers = {a.followers}")
텍스트
a.interests = ['여행']
b.interests = []
기본 bio = '', 기본 followers = 0

a 에만 "여행" 을 넣었는데 b 의 관심사는 여전히 비어 있죠. C-1 에서 def f(x=None) 으로 피했던 그 함정을, dataclass 에선 field(default_factory=list) 가 정석으로 해결해 줘요.

💡 한 줄 정리

단순한 기본값(= 0, = "")은 그냥 적으면 되지만, 리스트·딕셔너리 같은 가변 기본값은 field(default_factory=list) 로 줘요. 그래야 인스턴스마다 새 값이 생겨 C-1 의 가변 기본값 함정을 피해요.

🙋 학생 질문 — "왜 리스트만 default_factory 를 쓰고, 숫자·문자열은 그냥 = 0 으로 줘요?"

핵심은 그 값이 바뀔 수 있는가(가변) 없는가(불변) 예요. 숫자 0 이나 문자열 "" 는 한번 만들어지면 내용이 바뀌지 않는 불변 값이에요. 여러 인스턴스가 같은 0 을 가리켜도, 누가 그 0 자체를 바꿀 수 없으니 문제가 없죠.

반면 리스트·딕셔너리·집합은 append 처럼 내용을 바꿀 수 있는 가변 값이에요. 하나를 공유하면 한쪽에서 바꾼 게 다른 쪽에도 보여서 누적 사고가 나요. 그래서 가변 값만 default_factory 로 "매번 새로 만들어" 라고 해주는 거예요. C-1 에서 tags=None 으로 두고 안에서 [] 를 새로 만든 것과 똑같은 이유예요.


Step 3: "한번 정하면 못 바꾸게" — frozen=True

게시물의 좋아요 수처럼 자주 바뀌는 값도 있지만, 한번 정해지면 절대 바뀌면 안 되는 값도 있어요. 예를 들어 해시태그의 이름이나 사진을 찍은 좌표 같은 건, 실수로라도 바뀌면 곤란하죠. 이렇게 못 바꾸게 잠그고 싶을 땐 @dataclass(frozen=True) 를 써요.

Python
# src/instagram/d4_frozen.py
from dataclasses import dataclass


@dataclass(frozen=True)
class Tag:
    name: str

frozen=True 로 만든 객체는 만든 뒤에 값을 바꾸려 하면 막혀요. 정말 그런지 일부러 바꿔 볼게요.

Python
# src/instagram/d4_frozen_crash.py
t = Tag("python")
t.name = "java"   # ← frozen 이라 변경 불가 → FrozenInstanceError
텍스트
dataclasses.FrozenInstanceError: cannot assign to field 'name'

"name 필드에는 값을 다시 넣을 수 없다" 며 막아주죠. 덕분에 한번 만든 태그는 어디서도 몰래 바뀌지 않는다고 믿고 쓸 수 있어요.

그런데 frozen 에는 보너스가 하나 더 있어요. 지난 시간을 떠올려 보세요. __eq__ 를 직접 정의했더니 그 객체를 set 에 넣지 못했죠(해시가 불가능해졌어요). 그런데 frozen=True 로 잠그면 값이 안 바뀌는 게 보장되니, 파이썬이 해시를 자동으로 만들어 줘요. 그래서 set 으로 중복을 제거하거나 dict 의 키로 쓸 수 있어요.

Python
tags = {Tag("python"), Tag("python"), Tag("java")}   # frozen → 해시 가능 → set 가능
print(f"중복 제거된 태그 수: {len(tags)}")            # 2 (python 중복 제거)
print(f"#python 이 있나? {Tag('python') in tags}")     # True
텍스트
중복 제거된 태그 수: 2
#python 이 있나? True

Tag("python") 을 두 번 넣었는데 set 이 같은 것으로 보고 하나로 합쳐서 2개가 됐어요. 지난 시간 "__eq__ 를 정의하면 set 에 못 넣는다" 던 그 불편을, frozen 이 깔끔하게 풀어준 거예요.

💡 한 줄 정리

@dataclass(frozen=True) 로 만들면 값을 못 바꾸게 잠겨요(바꾸려 하면 FrozenInstanceError). 대신 해시가 생겨서 set·dict 키로 쓸 수 있어요.

🙋 학생 질문 — "frozen 으로 잠그면 좋아요 수처럼 바뀌는 값은 어떻게 다뤄요?"

좋은 질문이에요. frozen 은 모든 클래스에 어울리는 게 아니에요. 좌표·태그·설정값처럼 한번 정하면 안 바뀌는 데이터에 어울려요.

좋아요 수처럼 계속 바뀌어야 하는 값이 있는 객체라면 frozen 을 쓰지 않고 일반 dataclass 로 두면 돼요. 또는 "값을 바꾼다" 대신 "바뀐 값을 가진 새 객체를 만든다" 는 방식으로 설계하기도 하는데, 그건 더 나중에 자연스럽게 만나게 돼요. 지금은 "안 바뀔 데이터엔 frozen, 바뀔 데이터엔 일반 dataclass" 정도로 구분하면 충분해요.


Step 4: "오타를 막고 메모리도 아끼고" — slots=True

이번엔 작지만 유용한 옵션 하나를 볼게요. 일반 클래스나 dataclass 는 만든 뒤에 아무 이름이나 속성을 새로 붙일 수 있어요. 편할 것 같지만, 사실은 흔한 버그의 원인이에요. 속성 이름을 오타로 잘못 쓰면, 에러가 나는 대신 엉뚱한 새 속성이 조용히 생겨버리거든요. @dataclass(slots=True) 를 쓰면 정해진 필드 외엔 새 속성을 못 만들게 막아줘요.

Python
# src/instagram/d4_slots.py
from dataclasses import dataclass


@dataclass(slots=True)
class Account:
    username: str
    followers: int = 0

정해진 필드(username·followers)는 당연히 잘 쓰여요.

Python
a = Account("minji", 8500)
print(a)
a.followers = 9000          # 정해진 필드는 당연히 바꿀 수 있다
print(f"팔로워: {a.followers}")
텍스트
Account(username='minji', followers=8500)
팔로워: 9000

문제가 되는 건 오타예요. followersfolowers 로 잘못 적어 값을 넣어볼게요.

Python
# src/instagram/d4_slots_crash.py
a = Account("minji", 8500)
a.folowers = 9000   # ← 'followers' 오타! slots 가 없는 속성 생성을 막아 AttributeError
텍스트
AttributeError: 'Account' object has no attribute 'folowers' and no __dict__ for setting new attributes. Did you mean: 'followers'?

slots 가 없었다면 folowers 라는 새 속성이 조용히 생기고, 진짜 followers 는 그대로 남아서 "값을 바꿨는데 왜 안 바뀌지?" 하고 한참 헤맸을 거예요. slots=True 가 그 오타를 즉시 잡아줬어요. 게다가 파이썬 3.14 는 "혹시 followers 를 쓰려던 거 아니에요?" 하고 정답까지 알려주죠. 덤으로 slots 는 객체가 쓰는 메모리도 줄여줘서, 같은 객체를 아주 많이 만들 때 도움이 돼요. slots=True 옵션은 파이썬 3.10 부터 쓸 수 있어요.

💡 한 줄 정리

@dataclass(slots=True) 는 정해진 필드 외에 새 속성을 못 만들게 막아 오타를 즉시 잡아주고, 메모리도 아껴줘요. 파이썬 3.10 이상에서 쓸 수 있어요.

🙋 학생 질문 — "그럼 slots 를 항상 켜두면 되는 거 아니에요?"

대부분의 경우엔 켜두면 좋아요. 오타를 막아주고 메모리도 아껴주니까요. 다만 만든 뒤에 자유롭게 속성을 추가해야 하는 특별한 상황에선 slots 가 오히려 방해가 될 수 있어요. 그런 유연함이 필요한 경우엔 끄고 쓰죠.

입문 단계에서는 "오타와 메모리에 도움이 되는 옵션이 있다" 는 걸 알아두는 정도면 충분해요. 익숙해지면 상황에 맞게 켜고 끄게 될 거예요.


Step 5: "객체가 태어난 직후에" — post_init

@dataclass__init__ 을 자동으로 만들어 준다고 했죠. 그런데 객체를 만든 직후에 값을 조금 다듬고 싶을 때가 있어요. 예를 들어 댓글 내용 앞뒤의 불필요한 공백을 정리하거나, 내용으로부터 글자 수를 계산해 두고 싶을 때요. 이럴 때 쓰는 게 __post_init__ 이에요. 이름 그대로 __init__ 이 끝난 직후(post)에 파이썬이 자동으로 불러줘요.

Python
# src/instagram/comment.py
from dataclasses import dataclass, field


@dataclass
class Comment:
    author: str
    text: str
    char_count: int = field(init=False)   # 생성자 인자가 아니라 __post_init__ 에서 계산

    def __post_init__(self):
        self.text = self.text.strip()         # 정규화: 앞뒤 공백 제거
        self.char_count = len(self.text)      # 파생 필드 계산

__init__@dataclass 가 자동으로 만들어 주고, 그 직후 __post_init__ 이 불려서 두 가지 일을 해요. 첫째, text 의 앞뒤 공백을 strip() 으로 정리해요(이렇게 값을 깔끔하게 다듬는 걸 정규화라고 해요). 둘째, 정리된 text 의 글자 수를 세서 char_count 에 담아요. 이렇게 다른 값으로부터 계산해서 채우는 필드를 파생 필드라고 불러요. 실행해 볼게요.

Python
c = Comment("jaehoon", "  멋진 사진이에요!  ")
print(c)                       # text 는 공백이 정리되고 char_count 가 채워져 있다
print(f"내용: {c.text!r}")
print(f"글자 수: {c.char_count}")
텍스트
Comment(author='jaehoon', text='멋진 사진이에요!', char_count=9)
내용: '멋진 사진이에요!'
글자 수: 9

앞뒤 공백이 사라지고, 글자 수 9 가 자동으로 채워져 있죠. 우리가 char_count 를 직접 넣지 않았는데도요.

여기서 한 가지 더 욕심이 날 수 있어요. "빈 댓글은 아예 거부하고 싶은데요?" 처럼, 잘못된 값이 들어오면 받아들이지 않고 막고 싶을 때가 있죠. 그런 거부는 raise 라는 도구로 하는데, 그건 다음 카테고리(예외 처리)에서 배워요. 오늘 __post_init__ 은 값을 다듬고 계산하는 정규화·파생 필드까지만 다룰게요.

💡 한 줄 정리

__post_init____init__ 직후에 자동으로 불려서, 값을 다듬거나(정규화) 다른 값에서 계산한 파생 필드를 채울 때 써요. 잘못된 값을 거부하는 건 다음 시간(예외)에서 배워요.

🙋 학생 질문 — "char_count 는 왜 field(init=False) 라고 적었어요?"

char_count 는 우리가 객체를 만들 때 직접 넣는 값이 아니에요. text 로부터 계산되는 값이죠. 그런데 그냥 필드로 두면 @dataclassComment("jaehoon", "안녕", 2) 처럼 char_count 까지 생성자 인자로 받으려고 해요. 우리가 글자 수를 손으로 세서 넣어야 하니 이상하죠.

field(init=False) 는 "이 필드는 생성자 인자로 받지 마" 라는 표시예요. 그래서 Comment("jaehoon", "안녕") 처럼 만들 수 있고, char_count__post_init__ 안에서 계산해 채워요. 출력(__repr__)에는 그대로 나타나서 결과 확인엔 편하고요.


Step 6: "정해진 값만 골라 쓰게" — Enum 과 StrEnum

게시물에는 상태가 있어요. 공개·비공개·삭제됨처럼요. 이걸 그냥 문자열 "공개" 로 여기저기 쓰면 어떻게 될까요? 어느 한 곳에서 "공게" 라고 오타를 내도 파이썬은 그냥 평범한 문자열로 받아들여서 아무도 못 잡아요. 버그가 한참 뒤에야 엉뚱한 곳에서 터지죠.

이럴 때 "쓸 수 있는 값은 이것뿐이야" 하고 정해진 값들을 하나로 묶어두면 안전해요. 이렇게 정해진 상수들의 묶음을 만드는 도구가 Enum(열거형, enumeration)이에요. 묶어두면 그 밖의 값은 못 쓰고, 어떤 값이 가능한지 한눈에 보이죠.

Python
# src/instagram/status.py
from enum import Enum, StrEnum


class PostStatus(Enum):
    PUBLIC = "공개"
    PRIVATE = "비공개"
    DELETED = "삭제됨"

PostStatus.PUBLIC 처럼 이름(PUBLIC)으로 꺼내 쓰고, 각 멤버는 값("공개")도 가져요. 어떻게 쓰는지 볼게요.

Python
# src/instagram/d4_enum.py
from instagram.status import PostStatus

print("=== 가능한 게시물 상태 ===")
for status in PostStatus:                  # 정해진 상태들을 한눈에 순회
    print(f"{status.name} → {status.value}")

current = PostStatus.PUBLIC
if current is PostStatus.PUBLIC:           # is 로 정확히 비교
    print("이 게시물은 공개 상태예요.")

print(f"'비공개' 에 해당하는 상태: {PostStatus('비공개')}")   # 값으로 거꾸로 찾기
텍스트
=== 가능한 게시물 상태 ===
PUBLIC  공개
PRIVATE  비공개
DELETED  삭제됨
이 게시물은 공개 상태예요.
'비공개' 에 해당하는 상태: PostStatus.PRIVATE

for 로 가능한 상태를 전부 훑을 수 있고, is 로 정확히 비교하고, 값에서 거꾸로 멤버를 찾을 수도 있어요. 만약 PostStatus.PUBLC 처럼 오타를 내면 그 즉시 에러가 나서 바로 알아챌 수 있죠. 문자열을 직접 쓸 때와 가장 큰 차이예요.

한 가지 알아둘 점은, Enum 멤버는 문자열 그 자체는 아니라는 거예요. 그래서 PostStatus.PUBLIC == "공개"False 예요. 그런데 가끔은 "정해진 값으로 묶고 싶으면서, 그냥 문자열처럼 비교하고 출력하고 싶을" 때가 있어요. 그럴 땐 StrEnum 을 써요. 파이썬 3.11 부터 들어온, 멤버가 문자열이기도 한 Enum 이에요.

Python
# 같은 status.py 안에 — 멤버가 문자열이기도 한 Enum
class Visibility(StrEnum):
    PUBLIC = "public"
    PRIVATE = "private"
    FOLLOWERS = "followers"
Python
s = PostStatus.PUBLIC
print(f"s == '공개'? {s == '공개'}")          # Enum 멤버는 문자열이 아니다 → False

v = Visibility.PUBLIC
print(f"v == 'public'? {v == 'public'}")     # StrEnum 멤버는 문자열이기도 하다 → True
print(f"대문자로: {v.upper()}")               # 문자열 메서드도 그대로 쓸 수 있다
텍스트
s == '공개'? False
v == 'public'? True
대문자로: PUBLIC

평범한 Enum 은 문자열과 다르지만, StrEnum 은 문자열이기도 해서 == "public" 비교도 되고 upper() 같은 문자열 메서드까지 쓸 수 있어요. 나중에 데이터를 파일로 저장하거나 다른 시스템과 주고받을 때 문자열처럼 다뤄지면 편한데, 그럴 때 StrEnum 이 잘 맞아요.

💡 한 줄 정리

Enum 은 정해진 값들을 하나로 묶어 오타를 막고 가능한 값을 한눈에 보여줘요. StrEnum(3.11+)은 멤버가 문자열이기도 해서 문자열처럼 비교·출력할 수 있어요.

🙋 학생 질문 — "그냥 문자열 '공개' 를 쓰면 안 돼요? Enum 이 좀 번거로운데요."

써도 동작은 해요. 다만 문자열은 오타가 나도 실행이 그대로 되기 때문에, 버그가 조용히 숨어 있다가 한참 뒤에 터져요. if status == "공게" 라고 잘못 적어도 파이썬은 "그냥 둘 다 문자열이네" 하고 넘어가서, 그 조건이 영영 참이 되지 않는 버그를 만들죠.

Enum 은 세 가지가 좋아요. 첫째, PostStatus.PUBLC 같은 오타는 즉시 에러가 나서 바로 잡혀요. 둘째, 에디터가 가능한 값을 자동완성으로 보여줘요. 셋째, "이 게시물이 가질 수 있는 상태가 정확히 무엇인지" 코드만 봐도 한눈에 보여요. 값의 종류가 처음부터 정해져 있을 땐(상태·등급·요일처럼) Enum 이 문자열보다 훨씬 안전해요.


Step 7: "그럼 모든 걸 dataclass 로?" — 일반 클래스와의 선택

여기까지 오면 이런 생각이 들 수 있어요. "이렇게 편한데 모든 클래스를 dataclass 로 만들면 되는 거 아니에요?" 꼭 그렇진 않아요. dataclass 가 빛나는 경우가 있고, 일반 클래스가 더 자연스러운 경우도 있어요. 기준을 잡아볼게요.

판단의 핵심은 그 클래스의 주된 목적이에요. 데이터를 담는 게 주목적이면 dataclass 가 잘 맞아요. 오늘 만든 Member(아이디·팔로워·관심사)나 Comment(작성자·내용)처럼요. 이런 클래스는 __init__·__eq__·__repr__ 가 거의 똑같은 반복이라, dataclass 가 그 반복을 없애주죠.

반대로 행동이 풍부하고 상속 계층이 복잡한 클래스는 일반 클래스도 충분히 자연스러워요. 지난 시간 만든 Post 가 그런 예예요.

Python
# src/instagram/post.py  (행동이 풍부한 일반 클래스 — 일부 발췌)
class Post:
    def describe(self):
        return f"@{self.author}: {self.caption} (좋아요 {self.likes})"

    def __lt__(self, other):
        return self.likes < other.likes

    @property
    def is_popular(self):
        return self.likes >= 1000


class PhotoPost(Post):
    def describe(self):
        base = super().describe()
        return f"{base} · [사진] 필터 {self.filter_name}"

Postdescribe() 같은 행동, __lt__ 같은 좋아요순 정렬 기준, is_popular 같은 계산 속성, 그리고 PhotoPostsuper().describe() 로 부모 동작을 받아 확장하는 상속·다형성 계층까지 갖고 있어요. 이렇게 행동과 계층이 중심인 클래스는 dataclass 로 억지로 바꿀 필요가 없어요. 데이터를 담는 게 주목적인지, 행동이 주목적인지로 나눠 보면 선택이 쉬워져요.

텍스트
 [ 데이터가 중심이면 ]    @dataclass
    ├─ Member    (아이디 · 팔로워 · 관심사)
    └─ Comment   (작성자 · 내용)
        __init__·__eq__·__repr__ 반복을 자동화

 [ 행동·상속이 중심이면 ]    일반 class
    ├─ Post  PhotoPost / VideoPost   (상속 계층)
    └─ describe() · __lt__ · @property  (풍부한 행동)

한 가지 더 짚고 갈게요. dataclass 의 타입 어노테이션은 그저 안내하는 표시일 뿐, 실제로 값을 검사하지는 않아요. Memberfollowers: int 에 실수로 문자열을 넣어도 dataclass 는 막지 않아요. "입력값이 진짜 정수인지 실행 중에 검사해서 틀리면 거부" 하는 런타임 검증이 필요하면, 다음 과목에서 배울 전용 도구(입력값을 자동으로 검증해주는 모델)를 써요. 오늘 우리가 다루는 건 표준 라이브러리의 @dataclass 까지예요.

💡 한 줄 정리

데이터를 담는 게 주목적이면 @dataclass(Member·Comment), 행동과 상속이 풍부하면 일반 클래스(Post)가 자연스러워요. dataclass 의 타입 어노테이션은 그저 안내하는 표시일 뿐 값을 검사하진 않아요.

🙋 학생 질문 — "dataclass 랑 일반 클래스를 한 클래스 안에서 섞어 써도 돼요?"

사실 경계는 생각보다 부드러워요. 오늘 만든 Member 를 보세요. dataclass 인데도 handle·is_influencer 같은 @property 를 그대로 가졌죠. dataclass 도 메서드와 property 를 얼마든지 가질 수 있어요.

그래서 실무에선 이렇게 접근해요. 데이터가 중심인 클래스라면 dataclass 로 시작하고, 거기에 필요한 메서드를 더해 쓰는 거예요. 그러다 행동과 상속이 점점 복잡해지면 그때 일반 클래스 설계를 고민하면 돼요. "처음부터 완벽히 하나를 고른다" 기보다, 데이터가 중심이면 dataclass 로 출발하는 게 편한 기본값이에요.


Step 8: "도메인 한 조각을 완성하자" — 종합 캡스톤

오늘 배운 걸 한데 모아볼게요. 회원(Member)·댓글(Comment)·게시물 상태(PostStatus)를 묶어, 인스타그램 도메인의 한 장면을 만들어요. 각자 오늘 배운 도구가 들어 있어요. Memberdefault_factory@property 가, Comment__post_init__ 이, PostStatusEnum 이요.

Python
# src/instagram/d4_capstone.py
from instagram.member import Member
from instagram.comment import Comment
from instagram.status import PostStatus


minji = Member("minji", followers=12000, interests=["여행", "사진"])
status = PostStatus.PUBLIC
comments = [
    Comment("jaehoon", "  멋진 사진이에요!  "),
    Comment("seungwoo", "어디서 찍으셨어요?"),
]

print(f"작성자: {minji.handle} (인플루언서: {minji.is_influencer})")
print(f"관심사: {minji.interests}")
print(f"상태: {status.name} ({status.value})")
print("댓글:")
for c in comments:
    print(f"  {c.author}: {c.text!r} ({c.char_count}자)")
텍스트
작성자: @minji (인플루언서: True)
관심사: ['여행', '사진']
상태: PUBLIC (공개)
댓글:
  jaehoon: '멋진 사진이에요!' (9자)
  seungwoo: '어디서 찍으셨어요?' (10자)

Member@propertyhandle(@minji)과 is_influencer(True)를 계산해서 보여주고, 관심사는 default_factory 로 받은 리스트예요. PostStatus 는 이름과 값을 함께 가졌고, Comment__post_init__ 이 앞뒤 공백을 정리하고 글자 수까지 세 뒀죠("멋진 사진이에요!" 의 공백이 정리돼 9자).

카테고리 D 를 돌아보면, 클래스(D-1)에서 출발해 상속·다형성(D-2), 매직 메서드와 property(D-3)를 거쳐, 오늘 dataclass 로 이 모든 걸 짧고 단단하게 묶었어요. 데이터에 행동을 붙이는 객체지향의 한 바퀴를 완성한 거예요.

💡 한 줄 정리

@dataclass·default_factory·__post_init__·Enum 을 한데 모으면, 짧고 안전한 코드로 인스타그램 도메인 객체를 설계할 수 있어요. 카테고리 D 의 마무리예요.

🙋 학생 질문 — "이렇게 만든 객체를 프로그램이 꺼져도 남게 저장하려면요?"

지금 만든 회원·댓글·상태는 프로그램이 실행되는 동안만 메모리에 살아 있어요. 프로그램을 끄면 사라지죠. 이걸 파일로 저장해 뒀다가 다음에 다시 불러오려면 파일에 쓰고 읽는 법이 필요해요.

그건 다음 카테고리(예외와 파일)에서 배워요. 객체를 JSON 같은 형식으로 파일에 저장하고, 다시 읽어서 객체로 되살리는 과정을 다루게 돼요. 오늘 만든 단단한 dataclass 들이 그때 저장 대상이 될 거예요.


마무리

오늘은 카테고리 D 의 마지막으로, 반복되던 클래스 코드를 @dataclass 한 줄로 자동화했어요. 손으로 쓰던 __init__·__eq__·__repr__ 를 데코레이터가 대신 만들어 주고, field·frozen·slots·__post_init__·Enum 으로 도메인을 더 단단하게 설계했죠. 마지막엔 dataclass 와 일반 클래스를 언제 골라 쓸지 기준까지 잡았어요.

오늘 배운 핵심 세 가지

💡 하나: @dataclass 한 줄이면 __init__·__eq__·__repr__ 가 자동으로 만들어져요. 기본값은 그냥 적되, 리스트 같은 가변 기본값은 field(default_factory=list) 로 줘서 C-1 의 가변 기본값 함정을 정석으로 피해요.

💡 : frozen=True 로 잠그면 값을 못 바꾸는 대신 해시가 생겨 set·dict 키로 쓸 수 있어요. slots=True 는 오타 속성을 막고 메모리를 아끼고, __post_init__ 은 객체가 태어난 직후 값을 다듬거나 파생 필드를 채워요.

💡 : 정해진 값 묶음은 Enum(또는 문자열처럼 쓰는 StrEnum)으로 오타를 막아요. 그리고 데이터가 중심이면 dataclass, 행동·상속이 풍부하면 일반 클래스 — 목적에 맞게 골라 써요.

다음 시간 예고

이걸로 카테고리 D(객체지향) 전체를 마쳤어요. 다음 시간(E-1)부터는 새로운 카테고리, 예외와 파일로 넘어가요. 다음 시간엔 잘못된 값이나 예상 못 한 상황이 생겼을 때 프로그램이 그냥 죽지 않고 우아하게 대처하는 예외 처리를 배워요. try·except 로 에러를 받아내고, raise 로 우리가 직접 에러를 일으키는 법도요.

오늘 __post_init__ 에서 "잘못된 값을 거부하는 건 다음 시간에" 하고 미뤄둔 그 raise 를 드디어 만나게 됩니다. 예를 들어 "빈 댓글은 받지 않는다" 같은 규칙을 우리 손으로 강제할 수 있게 돼요.


과제

오늘 배운 @dataclass 와 그 도구들을 직접 손으로 만들어 봅시다. 눈으로 읽는 것과 직접 @dataclass 를 쳐보는 건 완전히 달라요. 막히면 Step 으로 돌아가 코드를 다시 보세요.

[기초] 댓글을 dataclass 로 다시 만들기

지난 시간엔 댓글의 __init__·__eq__ 를 손으로 썼죠. 이번엔 @dataclass 로 훨씬 짧게 만들어 봅니다.

  • @dataclass 를 붙인 클래스 Comment 를 만들고, author(작성자)·text(내용) 필드와 기본값이 0likes(좋아요 수) 필드를 둡니다.
  • 작성자·내용이 같은 댓글 두 개와 다른 댓글 하나를 만들어, == 비교 결과를 출력해 봅니다(자동으로 만들어진 __eq__ 가 동작하는지 확인).
  • print(댓글) 으로 자동 생성된 __repr__ 모양도 확인합니다.
  • len(댓글) 으로 내용 글자 수를 재고 싶다면, @dataclass__len__ 을 만들어주지 않으니 직접 def __len__(self) 를 더해 봅니다.
  • (생각해보기) 손으로 __init__·__eq__·__repr__ 를 다 쓰던 지난 시간과 비교해, 코드가 얼마나 줄었는지 느껴 보세요.

[응용] 못 바꾸는 장소와 분류 Enum

게시물에 붙는 장소는 한번 정하면 바뀌지 않고, 장소의 분류는 정해진 값들 중 하나예요. frozenEnum 을 함께 써 봅니다.

  • Enum 으로 Category 를 만들고, FOOD(맛집)·TRAVEL(여행)·DAILY(일상) 멤버를 둡니다.
  • @dataclass(frozen=True)Location 을 만들고 name(이름)·category(분류, Category 타입) 필드를 둡니다.
  • 같은 장소를 일부러 두 번 포함한 리스트를 만들고 set(장소들) 으로 중복을 제거해, 고유한 장소 수를 출력합니다(frozen 이라 해시 가능).
  • 만든 장소의 name 을 바꾸려고 시도해, 정말 못 바꾸는지(에러가 나는지) 확인해 봅니다.
  • (생각해보기) 만약 Locationfrozen 없이 만들었다면 set 에 넣을 때 어떤 일이 생길까요?

[심화] 회원 dataclass 종합하기

오늘 배운 default_factory·__post_init__·@property 를 한 클래스에 모아 봅니다.

  • @dataclassMember 를 만들고 username(아이디)·기본값 0followers(팔로워)·field(default_factory=list)interests(관심사) 필드를 둡니다.
  • __post_init__ 에서 username 의 앞뒤 공백을 없애고 소문자로 바꿔 정규화합니다(예: " MinJi ""minji").
  • @propertyhandle 을 만들어 항상 @아이디 형태를 돌려주게 합니다.
  • 회원 두 명을 만들어 한 명에게만 관심사를 추가한 뒤, 두 회원의 관심사가 서로 섞이지 않는지 확인합니다(default_factory 의 효과).
  • (생각해보기) __post_init__ 의 정규화 덕분에, 회원이 아이디를 대소문자 섞어 입력해도 항상 같은 형태로 저장돼요. 이게 왜 편리할지 생각해 보세요.

생각해볼 주제

정답이 하나로 정해진 질문이 아니에요. 혼자 곰곰이 생각해 보거나, 함께 공부하는 동료와 이야기 나눠 보면 좋습니다.

1. 자동의 편리함과 직접의 통제, 어디서 균형을 잡을까?

@dataclass__init__·__eq__·__repr__ 를 알아서 만들어 줘서 정말 편해요. 그런데 가끔은 그 자동 동작이 우리가 원하는 것과 다를 수 있어요.

예를 들어 회원의 "같음" 을 모든 필드가 같을 때가 아니라 아이디만 같으면 같다고 보고 싶을 수도 있죠. 그러면 자동 __eq__ 대신 직접 정의해야 해요. 편리한 자동 생성에 얼마나 맡기고, 어디서부터 직접 통제하는 게 좋을까요? 자동에 전부 맡길 때의 장점과, 세밀하게 직접 쓸 때의 장점을 견줘 보세요.

2. 못 바꾸게 만드는 게 왜 더 안전할까?

frozen=True 는 값을 못 바꾸게 잠가요. 언뜻 보면 불편한 제약 같은데, 왜 이런 걸 일부러 만들까요?

값이 어디서도 몰래 바뀌지 않는다고 보장되면, 그 객체를 여러 곳에서 안심하고 공유할 수 있고 버그를 찾기도 쉬워져요. 반대로 모든 객체를 frozen 으로 만들면 어떤 불편이 생길까요? "안 바뀌어야 하는 데이터" 와 "바뀌어야 하는 데이터" 를 나누는 자신만의 기준을 세워 보세요.

3. 문자열 대신 Enum 을 쓰는 값은 어떤 것일까?

게시물 상태처럼 정해진 값이 분명할 땐 Enum 이 오타를 막아주고 안전해요. 그런데 모든 문자열을 Enum 으로 바꾸는 게 항상 좋은 건 아니에요.

예를 들어 댓글 내용이나 사용자가 입력하는 자유로운 글은 값이 무한하니 Enum 으로 묶을 수 없죠. 또 가능한 값이 자주 추가되거나 바뀌는 상황이라면 Enum 을 고치는 일이 잦아질 수도 있어요. 어떤 값이 Enum 에 어울리고 어떤 값은 그냥 문자열이 나은지, 구분하는 기준을 정리해 보세요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 먼저 스스로 충분히 @dataclass 를 직접 짜본 뒤에 펼쳐 보세요. 정답을 외우는 게 아니라, "내가 만든 코드와 어디가 다른지" 를 비교하는 게 진짜 공부예요.


🎯 [과제 1 예시답안] 댓글을 dataclass 로 다시 만들기

채점 포인트

항목 확인 핵심
데코레이터 class 위에 @dataclass 자동 생성의 출발점
필드 + 기본값 author·text + likes: int = 0 타입 어노테이션 = 필드 표시
자동 __eq__ 같은 댓글 ==True 손으로 안 써도 동작
직접 __len__ dataclass 가 안 만들어주는 건 직접 len(댓글) = 내용 글자 수

풀이 예시

Python
# src/instagram/d4_comment_dataclass_solution.py
from dataclasses import dataclass


@dataclass
class Comment:
    author: str
    text: str
    likes: int = 0          # 기본값 필드

    def __len__(self):       # dataclass 가 안 만들어주는 건 직접 정의
        return len(self.text)


if __name__ == "__main__":
    a = Comment("jaehoon", "좋아요!")
    b = Comment("jaehoon", "좋아요!")
    c = Comment("minji", "멋진 사진이에요")
    print(f"a == b? {a == b}")     # 자동 __eq__ → True
    print(f"a == c? {a == c}")     # False
    print(a)                        # 자동 __repr__
    print(f"len(c) = {len(c)}")    # 직접 만든 __len__ → 8

실행 결과예요.

텍스트
a == b? True
a == c? False
Comment(author='jaehoon', text='좋아요!', likes=0)
len(c) = 8

💡 튜터의 한마디: 지난 시간엔 __init__·__eq__·__repr__ 를 손으로 다 적었는데, 이번엔 @dataclass 한 줄과 필드 세 개로 끝났죠. a == bTrue 인 건 자동으로 만들어진 __eq__ 덕분이에요(모든 필드가 같으면 같다고 봐요). print(a) 의 깔끔한 출력도 자동 __repr__ 이고요. 다만 len(댓글)@dataclass 가 만들어주지 않으니 __len__ 만 직접 더했어요. "반복되는 건 자동에 맡기고, 내가 원하는 동작만 직접 더한다" 는 dataclass 의 방식을 느꼈다면 성공이에요.

🎯 [과제 2 예시답안] 못 바꾸는 장소와 분류 Enum

채점 포인트

항목 확인 핵심
분류 Enum class Category(Enum) + 3개 멤버 정해진 값만
frozen @dataclass(frozen=True)Location 못 바꾸게 잠그기
중복 제거 set(장소들) → 고유 수 2 frozen 이라 해시 가능
불변 확인 name 변경 시도 → 에러 정말 못 바꾸는지

풀이 예시

Python
# src/instagram/d4_location_frozen_solution.py
from dataclasses import dataclass
from enum import Enum


class Category(Enum):
    FOOD = "맛집"
    TRAVEL = "여행"
    DAILY = "일상"


@dataclass(frozen=True)
class Location:
    name: str
    category: Category


if __name__ == "__main__":
    spots = [
        Location("성수동 카페", Category.FOOD),
        Location("성수동 카페", Category.FOOD),
        Location("제주 바다", Category.TRAVEL),
    ]
    unique = set(spots)                            # frozen → 해시 가능 → 중복 제거
    print(f"방문한 고유 장소 수: {len(unique)}")   # 2
    for loc in sorted(unique, key=lambda x: x.name):
        print(f"- {loc.name} [{loc.category.value}]")

실행 결과예요.

텍스트
방문한 고유 장소 수: 2
- 성수동 카페 [맛집]
- 제주 바다 [여행]

💡 튜터의 한마디: 두 가지 도구가 만나는 과제였어요. CategoryEnum 으로 묶어 맛집·여행·일상만 쓰게 했고, Locationfrozen=True 로 잠가 한번 정한 장소가 바뀌지 않게 했죠. 핵심은 set(spots) 예요. "성수동 카페" 를 두 번 넣었는데 고유 장소가 2개로 나온 건, frozen 덕분에 해시가 생겨 set 이 같은 장소를 하나로 합칠 수 있었기 때문이에요. 만약 frozen 없이 만들었다면 set 에 넣는 순간 "해시할 수 없다" 는 에러가 났을 거예요. 정렬 출력에 쓴 sorted(..., key=lambda ...) 는 C-2 에서 배운 정렬이니 반가웠죠?

🎯 [과제 3 예시답안] 회원 dataclass 종합하기

채점 포인트

항목 확인 핵심
가변 기본값 interests: list[str] = field(default_factory=list) C-1 함정 회피
정규화 __post_init__ 에서 strip().lower() 태어난 직후 값 다듬기
계산 속성 @property def handle@아이디 괄호 없이 속성처럼
독립성 확인 두 회원의 관심사가 안 섞임 default_factory 효과

풀이 예시

Python
# src/instagram/d4_member_full_solution.py
from dataclasses import dataclass, field


@dataclass
class Member:
    username: str
    followers: int = 0
    interests: list[str] = field(default_factory=list)

    def __post_init__(self):
        self.username = self.username.strip().lower()   # 정규화

    @property
    def handle(self):
        return f"@{self.username}"


if __name__ == "__main__":
    a = Member("  MinJi  ", followers=12000, interests=["여행"])
    b = Member("Jaehoon")
    a.interests.append("사진")
    print(f"a.handle = {a.handle}")        # @minji (정규화됨)
    print(f"a.interests = {a.interests}")  # ['여행', '사진']
    print(f"b.interests = {b.interests}")  # [] — 서로 안 섞임

실행 결과예요.

텍스트
a.handle = @minji
a.interests = ['여행', '사진']
b.interests = []

💡 튜터의 한마디: 오늘 배운 도구 세 개가 한 클래스에 모인 종합 과제였어요. interestsfield(default_factory=list) 라 회원마다 새 리스트를 가져서, a 에만 "사진" 을 넣어도 b 의 관심사는 비어 있죠(C-1 가변 기본값 함정 회피). __post_init__" MinJi " 를 받자마자 strip().lower() 로 다듬어 "minji" 로 저장했고, 그래서 handle@minji 로 깔끔하게 나왔어요. 입력이 제멋대로 들어와도 객체가 태어나는 순간 정돈되니, 이후 코드가 한결 단순해져요.


🤔 [생각해볼 주제 1] 자동의 편리함과 직접의 통제, 어디서 균형을 잡을까?

문제 상황 요약

@dataclass__init__·__eq__·__repr__ 를 자동으로 만들어 줘서 편하지만, 가끔은 그 자동 동작이 원하는 것과 다를 수 있어요. 예를 들어 회원의 "같음" 을 모든 필드가 아니라 아이디만으로 판단하고 싶을 때처럼요. 어디까지 자동에 맡기고 어디서부터 직접 통제할지가 고민이에요.

튜터의 가이드 및 해설

기준은 "흔한 경우인가, 특별한 경우인가" 예요.

대부분의 데이터 클래스는 "모든 필드가 같으면 같다", "필드를 다 보여준다" 는 자동 동작으로 충분해요. 이런 평범한 경우엔 자동에 맡기는 게 코드도 짧고 실수도 줄어요. 직접 쓰면 오히려 빠뜨리거나 틀릴 위험만 생기죠.

특별한 의미가 필요할 때만 직접 통제하면 돼요. 회원을 아이디로만 비교하고 싶다면 그때 __eq__ 를 직접 정의하는 거예요. 중요한 건 dataclass 가 전부 아니면 전무가 아니라는 점이에요. 자동 생성을 기본으로 깔고, 그중 특별한 동작 하나만 골라 덮어쓸 수 있어요. 오늘 Member 가 자동 생성을 받으면서도 @property 를 따로 더한 것처럼요.

그래서 실무의 결은 이래요. "기본은 자동에 맡기고, 도메인에 특별한 의미가 있는 부분만 직접 손본다." 처음부터 모든 걸 직접 통제하려 들면 dataclass 를 쓰는 의미가 옅어져요.

💡 핵심을 한마디로

평범한 동작은 자동 생성에 맡기고, 도메인에 고유한 의미(예: 아이디만으로 같음 판단)가 있는 부분만 골라 직접 정의해요. dataclass 는 전부 아니면 전무가 아니라, 자동을 깔고 일부만 덮어쓰는 도구예요.

🤔 [생각해볼 주제 2] 못 바꾸게 만드는 게 왜 더 안전할까?

문제 상황 요약

frozen=True 는 값을 못 바꾸게 잠가요. 언뜻 불편한 제약 같은데, 왜 이런 걸 일부러 만들까요? 그리고 모든 객체를 frozen 으로 만들면 어떤 불편이 생길까요?

튜터의 가이드 및 해설

못 바꾸게 막으면 좋은 점이 셋이에요.

첫째, 안심하고 공유할 수 있어요. 한 객체를 여러 곳에서 같이 쓸 때, 어느 한 곳이 몰래 값을 바꾸면 다른 곳이 영문도 모르고 영향을 받아요. frozen 이면 "이 값은 절대 안 바뀐다" 가 보장되니, 마음 놓고 여기저기 넘겨 쓸 수 있죠.

둘째, 버그를 찾기 쉬워요. "이 값이 왜 바뀌었지?" 를 추적하는 건 정말 힘든 디버깅인데, 못 바꾸는 값은 애초에 이 고민이 없어요.

셋째, 해시가 가능해져요. 오늘 봤듯이 set 의 원소나 dict 의 키로 쓸 수 있죠.

그렇다고 모든 걸 frozen 으로 만들면 곤란해요. 좋아요 수, 팔로워 수, 장바구니처럼 본래 자주 바뀌는 데이터는 잠그면 쓸 수가 없어요(바꿀 때마다 새 객체를 만들어야 하니 번거롭고요). 그래서 기준은 "이 값이 본질적으로 바뀌는 데이터인가" 예요. 좌표·태그·설정처럼 한번 정하면 고정인 건 frozen, 상태가 계속 변하는 건 일반 dataclass 가 어울려요.

💡 핵심을 한마디로

못 바꾸게 막으면 안심하고 공유할 수 있고, 버그 추적이 쉬워지고, 해시가 가능해져요. 단 본래 자주 바뀌는 데이터(좋아요·팔로워 수)는 일반 dataclass 로 두고, 고정된 데이터(좌표·태그·설정)에만 frozen 을 써요.

🤔 [생각해볼 주제 3] 문자열 대신 Enum 을 쓰는 값은 어떤 것일까?

문제 상황 요약

게시물 상태처럼 정해진 값이 분명할 땐 Enum 이 오타를 막아 안전해요. 하지만 모든 문자열을 Enum 으로 바꾸는 게 항상 좋은 건 아니에요. 어떤 값이 Enum 에 어울리고 어떤 값은 그냥 문자열이 나을까요?

튜터의 가이드 및 해설

판단의 핵심은 "값의 종류가 미리 정해져 있고, 그 수가 적은가" 예요.

Enum 이 어울리는 값은 가능한 종류가 정해져 있어요. 게시물 상태(공개·비공개·삭제됨), 요일(월~일), 회원 등급(일반·인플루언서) 처럼요. 종류가 고정돼 있으니 묶어두면 오타를 막고, 가능한 값이 코드에 한눈에 보이고, 자동완성도 돼요.

반대로 Enum 이 안 맞는 값도 있어요. 댓글 내용이나 사용자가 입력하는 자유로운 글은 값이 무한해서 묶을 수가 없죠. 또 가능한 값이 자주 늘거나 바뀌는 경우(예: 수시로 추가되는 태그 종류)라면, Enum 을 계속 고쳐야 해서 오히려 번거로울 수 있어요. 이럴 땐 문자열이나 다른 방식이 나아요.

정리하면, 종류가 처음부터 정해져 있고 잘 바뀌지 않는 값(상태·등급·요일)은 Enum 이 안전하고, 값이 무한하거나 자주 바뀌는 값은 문자열이 자연스러워요. "이 값이 가질 수 있는 경우의 수를 내가 다 적을 수 있는가?" 를 스스로 물어보면 구분이 쉬워져요.

💡 핵심을 한마디로

가능한 종류가 미리 정해져 있고 잘 안 바뀌는 값(상태·등급·요일)은 Enum 으로 묶어 오타를 막고, 값이 무한하거나(댓글 내용) 자주 바뀌는 값은 문자열이 나아요. "경우의 수를 다 적을 수 있나?" 가 판단 기준이에요.

전체 목록 파이썬 기초