문서 읽는 데 48분 · D3

D-3: 매직 메서드와 프로퍼티

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

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

안녕하세요, 홍순구입니다. 지난 시간엔 Post 를 부모로 두고 PhotoPost·VideoPost 가 물려받는 상속, 같은 describe() 한 마디에 종류마다 다르게 반응하는 다형성까지 만들어 봤어요. 그러면서 __str__·__repr__ 처럼 밑줄 두 개로 감싼 특별한 메서드도 슬쩍 썼죠.

그런데 오늘 만든 게시물들엔 아쉬운 점이 있어요. 같은 내용의 게시물 두 개를 == 로 비교하면 엉뚱하게 "다르다" 고 나오고, 좋아요순으로 정렬하는 것도 아직 안 돼요. 게시물 길이를 len() 으로 재는 것도 안 되고요.

오늘은 이 밑줄 두 개짜리 특별한 메서드들 — 매직 메서드(magic method)를 더해서 우리가 만든 객체에게 파이썬의 기본 문법(==·정렬·len())을 가르칠 거예요. 거기에 메서드를 속성처럼 쓰게 해주는 @property 까지 만나봅니다.

텍스트
 [지난 시간 D-2]  Post  PhotoPost / VideoPost  (상속·다형성)
                 __str__·__repr__ 으로 객체를 보기 좋게 표현

 [오늘 D-3]      그 Post 에게 파이썬 문법을 가르친다
                 ├─ __eq__     두 게시물이 같은지     ==
                 ├─ __lt__     좋아요순으로 정렬       sorted()
                 ├─ __len__    게시물 길이             len()
                 └─ @property  메서드를 속성처럼       post.is_popular

💡 오늘 수업의 핵심 — "객체에게 파이썬 문법을 가르친다"

==·정렬·len() 같은 파이썬 기본 문법은, 사실 객체 안의 약속된 메서드(__eq__·__lt__·__len__)를 부르는 거예요. 이 매직 메서드를 우리가 정의하면, 내가 만든 객체도 == 로 비교하고 sorted() 로 정렬할 수 있어요. 그리고 @property 로 메서드를 괄호 없이 속성처럼 쓰는 법까지 익혀, 지난 시간 만든 게시물 피드를 한층 똑똑하게 다듬습니다.

🎯 학습 목표

  • __eq__·__lt__·__len__ 같은 매직 메서드 로 내가 만든 객체에 ==·정렬·len() 을 가르친다.
  • 매직 메서드로 연산자의 동작을 정의하는 게 연산자 오버로딩 임을 이해한다.
  • @property 로 메서드를 속성처럼 쓰고, 읽기 전용 속성과 setter 가공을 구분해 쓴다.

Step 1: "이미 매직 메서드를 쓰고 있었다" — 던더의 정체

사실 우리는 지난 시간부터 매직 메서드를 쓰고 있었어요. __init__ 으로 객체를 초기화하고, __str__ 으로 print() 결과를 예쁘게 만들었죠. 이렇게 이름이 밑줄 두 개(__)로 감싸인 메서드를 매직 메서드라고 불러요.

매직 메서드의 정체는 이거예요. 파이썬이 특정 순간에 자동으로 불러주는 약속된 이름. 객체를 만들 때 __init__ 이, print(obj) 할 때 __str__ 이 자동으로 불려요. 우리가 직접 obj.__str__() 이라고 부르지 않아도요.

Python
# src/instagram/d3_magic_intro.py
class Greeting:
    def __init__(self, name):   # 객체를 만들 때 파이썬이 자동 호출
        self.name = name

    def __str__(self):          # print(obj) 할 때 파이썬이 자동 호출
        return f"안녕하세요, {self.name}님"


g = Greeting("재훈")
print(g)               # __str__ 자동 호출 → 안녕하세요, 재훈님
print(len("인스타"))     # 문자열의 __len__ 자동 호출 → 3
print(3 + 4)           # int 의 __add__ 자동 호출 → 7

실행하면 이렇게 나와요.

텍스트
안녕하세요, 재훈님
3
7

여기서 놀라운 게 마지막 두 줄이에요. 늘 쓰던 len("인스타") 도 사실은 문자열의 __len__ 을, 3 + 4 도 정수의 __add__ 를 부르는 거였어요. 파이썬의 기본 문법 거의 전부가 이렇게 매직 메서드로 만들어져 있어요. 즉 우리가 __len__ 이나 __add__ 를 직접 정의하면, 내가 만든 객체도 len() 이나 + 를 쓸 수 있다는 뜻이죠.

매직 메서드를 파이썬과 미리 맺어둔 "신호 → 행동" 약속표라고 생각하면 편해요. == 신호가 오면 __eq__ 를 부르고, < 신호가 오면 __lt__ 를 부르는 식이죠. 오늘은 이 약속표를 우리 게시물에 하나씩 채워 넣을 거예요.

💡 한 줄 정리

매직 메서드는 파이썬이 특정 순간(print·len·== 등)에 자동으로 불러주는, 밑줄 두 개로 감싼 약속된 이름이에요. 우리가 정의하면 내 객체도 그 문법을 쓸 수 있어요.

🙋 학생 질문 — "왜 하필 이름을 밑줄 두 개로 감쌌어요?"

밑줄 두 개로 감싼 이름을 영어로 더블 언더스코어(double underscore), 줄여서 던더(dunder)라고 불러요. __init__ 은 "던더 이닛" 이라고 읽죠. 파이썬이 이렇게 특이한 모양을 쓰는 이유는, 우리가 평소에 만드는 일반 이름(name·add_like)과 확실히 구분하기 위해서예요. "이건 내가 직접 부르는 메서드가 아니라, 파이썬이 알아서 부르는 특별한 이름이다" 라는 표시인 거죠. 그래서 매직 메서드는 보통 우리가 obj.__eq__() 처럼 직접 부르지 않고, == 같은 문법을 통해 간접적으로 불러요.


Step 2: "두 게시물이 같다는 건?" — __eq__ 로 내용 비교

같은 내용의 게시물 두 개가 같은지 == 로 비교해 볼게요. 그런데 매직 메서드를 정의하지 않은 상태에선 결과가 좀 이상해요.

Python
# src/instagram/d3_eq.py
class PostNoEq:
    """__eq__ 가 없는 게시물 — 파이썬 기본 == 는 주소(정체)를 비교한다."""

    def __init__(self, author, caption):
        self.author = author
        self.caption = caption


x = PostNoEq("jaehoon", "오늘의 노을")
y = PostNoEq("jaehoon", "오늘의 노을")
print(x == y)   # False

작성자도 캡션도 똑같은데 False 가 나와요. 왜냐하면 __eq__ 가 없을 때 파이썬의 기본 ==내용이 아니라 주소(메모리 위치) 를 비교하거든요. xy 는 똑같이 생겼어도 따로따로 만든 별개의 객체라, 주소가 다르니 "다르다" 고 하는 거예요.

이때 __eq__ 를 정의하면, == 의 판단 기준을 우리가 정할 수 있어요.

Python
class Post:
    """__eq__ 를 정의한 게시물 — 작성자 + 캡션이 같으면 같은 게시물로 본다."""

    def __init__(self, author, caption):
        self.author = author
        self.caption = caption

    def __eq__(self, other):
        return self.author == other.author and self.caption == other.caption


a = Post("jaehoon", "오늘의 노을")
b = Post("jaehoon", "오늘의 노을")
print(a == b)   # True
print(a is b)   # False — "같다(==)" 와 "같은 객체다(is)" 는 다르다
텍스트
True
False

__eq__(self, other) 는 "나(self)와 상대(other)가 같은가?" 를 판단해요. 여기선 작성자와 캡션이 둘 다 같으면 True 로 봤죠. 이제 내용이 같은 두 게시물은 ==True 예요.

마지막 줄의 a is bFalse 인 게 핵심이에요. 지난 자료구조 시간에 b = a 로 같은 리스트를 가리키게 했던 것 기억하시죠? 그때 is 는 "둘이 진짜 같은 객체(같은 주소)냐" 를 묻는 거였어요. ab 는 따로 만든 두 객체라 isFalse 지만, 내용으로 보면 같으니 ==True 인 거죠. 쌍둥이가 똑같이 생겼어도(==) 엄연히 다른 사람(is)인 것과 같아요.

💡 한 줄 정리

기본 == 는 주소를 비교해서 내용이 같아도 False 예요. __eq__ 를 정의하면 "무엇이 같으면 같은 것인지" 의 기준을 내가 정할 수 있어요.

🙋 학생 질문 — "eq 를 정의했더니 set 에 넣을 때 에러가 나요. 왜죠?"

날카로운 발견이에요. 파이썬에는 숨은 규칙이 하나 있어요. __eq__ 를 직접 정의하면, 그 객체를 set 에 넣거나 딕셔너리의 key 로 쓰는 기능이 자동으로 꺼져요. "같음" 의 기준이 바뀌었으니 파이썬이 안전을 위해 잠시 막아두는 거예요. set 이나 dict key 로도 쓰고 싶다면 __hash__ 라는 또 다른 매직 메서드를 같이 정의해야 하는데, 이건 지금 단계에선 몰라도 괜찮아요. 다음 시간에 배울 @dataclass 를 쓰면 이 부분도 한 번에 해결되거든요. 지금은 "__eq__ 를 손으로 정의하면 set 에는 바로 못 넣는다" 정도만 기억해 두세요.


Step 3: "좋아요순으로 줄 세우기" — __lt__sorted()

이번엔 게시물을 좋아요순으로 정렬해 볼게요. 함수형 시간에 sorted(posts, key=lambda p: p["likes"]) 처럼 key= 로 정렬 기준을 넘겼던 것 기억하시죠? 그건 게시물이 딕셔너리일 때였어요. 객체라면 더 깔끔한 방법이 있어요.

<(작다) 비교를 담당하는 매직 메서드가 __lt__(less than)예요. 이걸 정의하면 sorted()·max()·min() 이 알아서 그 기준으로 동작해요.

Python
# src/instagram/d3_lt.py
class Post:
    def __init__(self, author, caption, likes):
        self.author = author
        self.caption = caption
        self.likes = likes

    def __lt__(self, other):
        return self.likes < other.likes   # 좋아요 수로 크기를 비교

    def __repr__(self):
        return f"Post('{self.author}', 좋아요={self.likes})"


feed = [
    Post("jaehoon", "노을", 320),
    Post("minji", "브이로그", 8500),
    Post("seungwoo", "강아지", 1240),
]

# __lt__ 덕분에 sorted() 가 좋아요 기준으로 정렬 — key= 가 필요 없다
print(sorted(feed))                 # 좋아요 적은 순
print(sorted(feed, reverse=True))   # 좋아요 많은 순 (인기 피드)
print(max(feed))                    # 가장 인기 있는 게시물
print(min(feed))                    # 가장 인기 없는 게시물
텍스트
[Post('jaehoon', 좋아요=320), Post('seungwoo', 좋아요=1240), Post('minji', 좋아요=8500)]
[Post('minji', 좋아요=8500), Post('seungwoo', 좋아요=1240), Post('jaehoon', 좋아요=320)]
Post('minji', 좋아요=8500)
Post('jaehoon', 좋아요=320)

key= 를 한 번도 안 썼는데 좋아요순으로 정렬됐죠? sorted() 는 원소들을 정렬하려고 내부에서 계속 < 비교를 해요. 그때마다 우리가 정의한 __lt__ 를 불러서 "좋아요로 비교" 하는 거예요. reverse=True 면 많은 순, max·min 도 같은 기준으로 동작하고요. 정렬 기준을 객체 안에 한 번 정해두니, 쓸 때마다 key= 를 적을 필요가 없어졌어요.

키 순서로 학생들을 줄 세운다고 해봐요. "누가 더 작은지" 판단 기준(__lt__)만 정해주면, 실제로 줄 세우는 일은 sorted 가 알아서 해줘요. 우리는 비교 기준만 알려주면 되는 거죠.

그런데 잠깐, __eq__(==)와 __lt__(<)를 보면 공통점이 보이죠? 둘 다 연산자(==·<)의 동작을 우리가 직접 정의한 거예요. 이렇게 매직 메서드로 연산자의 동작을 내가 정하는 걸 연산자 오버로딩(operator overloading) 이라고 불러요. 이름은 거창하지만, 우리가 방금 한 게 바로 그거예요.

💡 한 줄 정리

__lt__ 로 "무엇으로 크기를 비교할지" 만 정하면, sorted()·max()·min() 이 그 기준으로 동작해요. 객체에 기준을 정해두면 key= 없이도 정렬돼요.

🙋 학생 질문 — "그럼 >, <=, >= 도 다 일일이 정의해야 하나요?"

원칙적으로는 __gt__(>), __le__(<=), __ge__(>=)도 각각 따로 있어요. 하지만 비교할 때마다 이걸 다 정의하는 건 번거롭죠. 그래서 함수형 시간에 잠깐 본 functools 에 편리한 도구가 있어요. from functools import total_ordering 으로 가져와 클래스 위에 @total_ordering 을 붙이면, __eq____lt__ 두 개만 정의해도 나머지 비교 연산을 파이썬이 자동으로 만들어 줘요. 지금은 "__eq____lt__ 만 있으면 나머지는 자동으로 채울 방법이 있다" 정도만 알아두면 충분해요.


Step 4: "len() 도 내가 정한다" — __len__

게시물의 길이를 len(post) 로 재고 싶다면 어떻게 할까요? 이미 답을 아실 거예요. len() 이 부르는 약속, __len__ 을 정의하면 돼요.

Python
# src/instagram/d3_len.py
class Post:
    def __init__(self, author, caption):
        self.author = author
        self.caption = caption

    def __len__(self):
        return len(self.caption)   # 게시물의 "길이" 를 캡션 글자 수로 정한다


p = Post("jaehoon", "오늘의 노을 사진")
print(len(p))   # 9 — len(post) 가 __len__ 을 자동 호출
텍스트
9

len(p) 라고 쓰면 파이썬이 p.__len__() 을 자동으로 불러요. 그 안에서 우리는 캡션의 글자 수를 돌려줬으니, "오늘의 노을 사진"(공백 포함 9글자)이 나온 거죠. 한 가지 규칙이 있어요. __len__ 은 반드시 0 이상의 정수 를 돌려줘야 해요. 길이라는 게 음수나 소수일 순 없으니까요.

💡 한 줄 정리

__len__ 을 정의하면 len(객체) 가 동작해요. 무엇을 그 객체의 "길이" 로 볼지는 우리가 정하는 거예요.

🙋 학생 질문 — "게시물 하나의 길이를 잰다는 게 좀 어색해요. 보통 len 은 목록에 쓰지 않나요?"

정확한 감각이에요. len() 은 원래 리스트·문자열·딕셔너리처럼 "여러 개를 담은 것" 의 개수를 셀 때 가장 자연스러워요. 게시물 하나에 len() 을 붙인 건, __len__ 이 어떻게 동작하는지 보여주려고 캡션 글자 수를 길이로 정해본 예시예요. 실무에서는 예를 들어 "게시물 여러 개를 담은 피드(Feed) 객체" 를 만들고 거기에 __len__ 을 정의해서 len(feed) 로 게시물 개수를 세는 식으로 더 자주 써요. 무엇을 길이로 볼지는 만드는 사람이 정하되, 읽는 사람이 헷갈리지 않게 자연스러운 기준을 고르는 게 좋아요.


Step 5: "메서드를 속성처럼" — @property (계산된 속성)

게시물이 인기 게시물인지 post.is_popular 로 확인하고 싶다고 해봐요. 메서드로 만들면 post.is_popular() 처럼 괄호를 붙여야 하죠. 그런데 좋아요 수처럼 "그냥 속성" 같은 느낌이라, 괄호 없이 post.is_popular 로 쓰고 싶을 때가 있어요. 이때 쓰는 게 @property 예요.

Python
# src/instagram/d3_property.py
class Post:
    def __init__(self, author, caption, likes):
        self.author = author
        self.caption = caption
        self.likes = likes

    @property
    def is_popular(self):
        return self.likes >= 1000          # 좋아요 1000 이상이면 인기 게시물

    @property
    def summary(self):
        if len(self.caption) <= 10:
            return self.caption
        return self.caption[:10] + "…"     # 너무 길면 앞 10글자만


p = Post("minji", "제주도 여행 브이로그 첫째 날 기록", 8500)
print(p.is_popular)   # True  — 괄호 없이 속성처럼! (실제로는 메서드)
print(p.summary)      # 앞 10글자 + …

q = Post("jaehoon", "노을", 320)
print(q.is_popular)   # False
print(q.summary)      # 노을 (10글자 이하라 그대로)
텍스트
True
제주도 여행 브이로…
False
노을

메서드 위에 @property 한 줄을 붙였더니, p.is_popular() 가 아니라 p.is_popular 로 괄호 없이 접근할 수 있게 됐어요. 겉보기엔 평범한 속성 같지만, 사실은 접근할 때마다 메서드가 실행되는 계산된 속성 이에요. 그래서 is_popular 는 좋아요 비교(>= 1000)를, summary 는 캡션 슬라이싱([:10])을 그 자리에서 계산해 돌려줬죠.

계산된 속성을 자판기 버튼이라고 생각하면 편해요. 버튼을 누를 때마다(속성에 접근할 때마다) 그 순간의 값을 새로 계산해서 내줘요. 좋아요가 999개에서 1000개로 늘면, 다시 p.is_popular 를 확인했을 때 자동으로 True 로 바뀌어 있는 거죠.

💡 한 줄 정리

@property 를 메서드에 붙이면 괄호 없이 속성처럼 접근해요. 접근할 때마다 그 자리에서 계산하니 항상 최신 값이 나와요.

🙋 학생 질문 — "그냥 self.is_popular = ... 로 일반 속성에 저장하면 안 되나요?"

저장해도 되긴 하지만 함정이 있어요. self.is_popular = self.likes >= 1000__init__ 에서 한 번 계산해 저장하면, 그 값은 게시물을 만든 순간에 고정돼 버려요. 그 뒤에 좋아요가 1000개를 넘어도 is_popular 는 여전히 처음 계산한 False 그대로죠. 직접 다시 계산해서 덮어쓰기 전까진 안 바뀌어요. 반면 @property 는 접근할 때마다 다시 계산하니, 좋아요가 바뀌면 자동으로 따라와요. "지금 이 순간의 상태로부터 계산되는 값" 은 @property 가 안전해요.


Step 6: "읽기 전용과 가공" — property setter

@property 로 만든 속성에는 재미있는 성질이 있어요. 기본적으로 읽기만 되고, 값을 새로 넣으려고 하면 막혀요.

Python
# src/instagram/d3_property_readonly.py
class Post:
    def __init__(self, caption):
        self._caption = caption

    @property
    def caption(self):     # setter 가 없으니 읽기만 된다
        return self._caption


p = Post("오늘의 노을")
print(p.caption)         # 읽기는 OK → 오늘의 노을
p.caption = "수정!"        # 쓰기 시도 → AttributeError 로 멈춘다

실행하면 읽기는 잘 되지만, 마지막 줄에서 이렇게 멈춰요.

텍스트
오늘의 노을
Traceback (most recent call last):
  File ".../d3_property_readonly.py", line 16, in <module>
    p.caption = "수정!"        # 쓰기 시도  AttributeError 로 멈춘다
    ^^^^^^^^^
AttributeError: property 'caption' of 'Post' object has no setter

"caption 이라는 property 에는 setter 가 없다" 는 에러예요. getter(읽기 담당)만 정의했으니 읽기 전용이 된 거죠. 의도치 않게 값을 바꾸는 실수를 막아주는 안전장치예요.

그럼 값을 넣는 것도 허용하되, 넣을 때 한 번 손봐서 저장하고 싶다면? @속성이름.setter 를 더하면 돼요.

Python
# src/instagram/d3_property_setter.py
class Post:
    def __init__(self, caption):
        self.caption = caption     # 이 대입도 아래 setter 를 거친다

    @property
    def caption(self):
        return self._caption

    @caption.setter
    def caption(self, value):
        self._caption = value.strip()   # 앞뒤 공백을 자동으로 정리


p = Post("   오늘의 노을   ")
print(f"[{p.caption}]")     # [오늘의 노을] — 만들 때부터 공백이 정리됨
p.caption = "  새 캡션  "
print(f"[{p.caption}]")     # [새 캡션] — 다시 넣어도 정리됨
텍스트
[오늘의 노을]
[새 캡션]

이제 caption 에 값을 넣을 때마다 setter 가 가로채서 .strip() 으로 앞뒤 공백을 정리해 저장해요. 흥미로운 건 __init__ 안의 self.caption = caption 도 setter 를 거친다는 거예요. 그래서 만들 때부터 공백이 정리되고, 나중에 다시 넣어도 정리되죠. 값이 들어올 때마다 문지기(setter)가 한 번 손봐서 들여보내는 셈이에요.

여기선 공백 정리 같은 가벼운 가공만 했어요. "빈 캡션은 거부한다" 같은 진짜 검증은 잘못된 값이 들어오면 오류를 일으켜야 하는데, 그 방법(예외)은 다음 카테고리에서 배워요. 지금은 setter 가 값을 한 번 가공해 준다는 것까지만 기억하면 충분해요.

💡 한 줄 정리

getter만 있는 @property 는 읽기 전용이라 값을 넣으면 AttributeError 가 나요. @속성.setter 를 더하면 값을 넣을 때 한 번 가공해서 저장할 수 있어요.

🙋 학생 질문 — "왜 저장은 caption 이 아니라 _caption 에 하나요? 밑줄은 뭐예요?"

좋은 관찰이에요. 만약 setter 안에서 self.caption = value 라고 쓰면, 그 대입이 또 setter 를 부르고, 그 안에서 또 setter 를 부르고… 끝없이 반복돼서 멈춰버려요. 그래서 실제 값은 caption 과 다른 이름인 _caption 에 저장하고, caption 은 그걸 읽고 쓰는 통로로만 쓰는 거예요. 앞에 붙은 밑줄 하나(_)는 파이썬 개발자들의 관습으로, "이건 내부에서만 쓰는 값이니 바깥에서 직접 건드리지 마세요" 라는 표시예요. 강제로 막는 건 아니지만, 다들 약속처럼 지켜요.


Step 7: 종합 — 매직 메서드와 프로퍼티를 박은 Post 피드

이제 지난 시간 만든 post.py 에 오늘 배운 걸 전부 합쳐볼게요. 부모 Post 에 매직 메서드와 프로퍼티를 더하면, 자식 PhotoPost·VideoPost 도 상속으로 그대로 물려받아요. 한 번 정의로 온 가족이 똑똑해지는 거죠.

먼저 Post 부모에 더한 매직 메서드부터 볼게요.

Python
# src/instagram/post.py
    # --- 비교 매직 메서드 ---
    def __eq__(self, other):
        return self.author == other.author and self.caption == other.caption

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

    # --- 길이 매직 메서드 ---
    def __len__(self):
        return len(self.caption)

이어서 프로퍼티 부분이에요. 계산된 속성 is_popular·summary 와, 공백을 정리하는 caption setter 를 함께 뒀어요.

Python
    # --- 프로퍼티 (계산된·읽기 전용 속성) ---
    @property
    def is_popular(self):
        return self.likes >= 1000

    @property
    def summary(self):
        if len(self.caption) <= 15:
            return self.caption
        return self.caption[:15] + "…"

    @property
    def caption(self):
        return self._caption

    @caption.setter
    def caption(self, value):
        self._caption = value.strip()

이제 사진·동영상 게시물을 섞은 피드를 좋아요순으로 정렬하고, 인기 여부와 캡션 길이까지 한 번에 출력해 볼게요.

Python
if __name__ == "__main__":
    feed = [
        PhotoPost("jaehoon", "오늘의 노을", "Clarendon"),
        VideoPost("minji", "제주 여행 브이로그", 60),
        PhotoPost("seungwoo", "강아지 산책", "Juno"),
    ]
    feed[0].likes = 320
    feed[1].likes = 8500
    feed[2].likes = 1240

    print("=== 인기순 피드 ===")
    for post in sorted(feed, reverse=True):     # __lt__ 덕분에 key= 없이 정렬
        mark = "[인기]" if post.is_popular else "[일반]"
        print(f"{mark} {post.summary} — 좋아요 {post.likes}, 캡션 {len(post)}글자")

    same = PhotoPost("jaehoon", "오늘의 노을", "Clarendon")
    print(f"feed[0] == same? {feed[0] == same}")   # __eq__ → True
    print(f"feed[0] is same? {feed[0] is same}")   # 같은 객체는 아님 → False
텍스트
=== 인기순 피드 ===
[인기] 제주 여행 브이로그 — 좋아요 8500, 캡션 10글자
[인기] 강아지 산책 — 좋아요 1240, 캡션 6글자
[일반] 오늘의 노을 — 좋아요 320, 캡션 6글자
feed[0] == same? True
feed[0] is same? False

sorted(feed, reverse=True) 한 줄로 좋아요 많은 순 정렬이 됐죠. PhotoPost·VideoPost__lt__ 를 직접 쓴 적이 없어요. 전부 부모 Post 에서 물려받았으니까요. is_popular·len(post) 도 마찬가지고요. 마지막 두 줄에선 같은 내용의 게시물이 == 로는 같고(True), is 로는 다른 객체(False)임을 다시 확인했어요. 지난 시간의 상속·다형성 위에 오늘의 매직 메서드·프로퍼티가 자연스럽게 얹힌 모습이에요.

💡 한 줄 정리

부모 Post 에 매직 메서드와 프로퍼티를 한 번 정의하면, 상속받은 자식들도 전부 그 기능을 물려받아요. 정렬·비교·계산 속성이 게시물 피드에 자연스럽게 녹아들었어요.

🙋 학생 질문 — "코드베이스의 동작은 어떻게 보장돼 있나요?"

post.py 의 동작(같은 내용이면 ==True, 좋아요순 정렬, len() 이 캡션 길이, is_popular·summary 계산, setter 의 공백 정리, 그리고 자식이 매직 메서드를 물려받는 것)은 코드베이스의 test_d3_magic.py 가 자동으로 검증해 두고 있어요. 그래서 교안에 적힌 출력은 실제로 그렇게 동작하는 게 확인된 결과예요. 여러분도 직접 uv run python src/instagram/post.py 로 실행해 같은 결과가 나오는지 확인해 보면 좋아요.


마무리

오늘은 우리가 만든 객체에게 파이썬의 기본 문법을 가르쳤어요. == 비교를 내용 기준으로 바꾸는 __eq__, 좋아요순 정렬을 가능하게 하는 __lt__, 길이를 재는 __len__ — 이 매직 메서드들로 게시물이 파이썬 문법과 자연스럽게 어울리게 됐죠. 그리고 @property 로 메서드를 속성처럼 쓰고, 읽기 전용과 setter 가공까지 다뤘어요.

오늘 배운 핵심 세 가지

💡 하나: 매직 메서드는 파이썬이 특정 순간 자동으로 부르는 밑줄 두 개짜리 약속이에요. __eq__== 의 기준을, __lt__ 로 정렬 기준을, __len__ 으로 len() 의 의미를 우리가 정의해요. 이렇게 연산자의 동작을 정하는 걸 연산자 오버로딩이라고 불러요.

💡 : __lt__ 만 정의하면 sorted()·max()·min() 이 그 기준으로 동작해요. 객체에 정렬 기준을 정해두니 key= 없이도 정렬되죠. 부모에 정의하면 상속받은 자식들도 전부 물려받고요.

💡 : @property 는 메서드를 괄호 없이 속성처럼 쓰게 해줘요. 접근할 때마다 계산하니 항상 최신 값이고, getter만 있으면 읽기 전용, @속성.setter 를 더하면 값을 넣을 때 가공할 수 있어요.

다음 시간 예고

지금까지 클래스를 만들 때마다 __init__ 으로 속성을 채우고, 오늘은 __eq__·__repr__ 까지 손으로 일일이 써왔어요. 그런데 회원·게시물·댓글처럼 "데이터를 담는" 클래스를 만들 때마다 이 비슷한 코드를 매번 반복하는 건 좀 번거롭죠.

다음 시간(D-4)엔 @dataclass 를 만나요. 클래스 위에 한 줄만 붙이면 __init__·__eq__·__repr__ 을 파이썬이 자동으로 만들어 줘요. 오늘 손으로 쓴 것들을 거의 공짜로 얻는 셈이죠. 거기에 값을 못 바꾸게 잠그는 frozen, 게시물 상태(공개·비공개·삭제됨) 같은 정해진 값들을 다루는 Enum 까지 배우면서, 인스타그램 도메인을 한층 단단하게 설계해 봅니다.


과제

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

[기초] 댓글 클래스에 매직 메서드 붙이기

게시물 옆엔 댓글이 있죠. 댓글 두 개가 같은지 비교하고, 댓글 길이도 재고 싶어요.

  • 클래스 Comment 를 만들고 __init__ 에서 author(작성자)와 text(내용)를 받습니다.
  • __eq__ 를 정의해, 작성자와 내용이 둘 다 같으면 같은 댓글로 보게 합니다.
  • __len__ 을 정의해, len(댓글) 이 내용(text)의 글자 수를 돌려주게 합니다.
  • 작성자·내용이 같은 댓글 두 개와 다른 댓글 하나를 만들어, == 비교 결과와 len() 을 출력해 확인합니다.
  • (생각해보기) __eq__ 를 작성자만 비교하도록 바꾸면, 어떤 댓글들이 "같다" 고 판단될까요?

[응용] 회원의 계산된 속성 만들기

회원 정보를 매번 손으로 계산하지 말고, 속성처럼 꺼내 쓰고 싶어요.

  • 클래스 Member 를 만들고 __init__ 에서 username(아이디)과 followers(팔로워 수)를 받습니다.
  • @propertyhandle 을 만들어, 항상 @아이디 형태(예: @minji)를 돌려주게 합니다.
  • @propertyis_influencer 를 만들어, 팔로워가 1만(10000) 이상이면 True 를 돌려주게 합니다.
  • 팔로워가 많은 회원과 적은 회원을 각각 만들어, handleis_influencer 를 괄호 없이 출력해 봅니다.
  • (생각해보기) handle 을 일반 메서드(def handle(self))로 만들었다면, 쓸 때 무엇이 달라질까요?

[심화] 인기 해시태그 순위 매기기

해시태그를 사용 횟수순으로 줄 세우고, 인기 태그인지도 표시해 보세요.

  • 클래스 HashTag 를 만들고 __init__ 에서 name(이름)과 uses(사용 횟수)를 받습니다.
  • __lt__ 를 정의해 사용 횟수로 크기를 비교하게 하고, __repr__#이름(횟수) 형태로 보이게 합니다.
  • @propertyis_trending 을 만들어, 사용 횟수가 1000 이상이면 True 를 돌려주게 합니다.
  • 해시태그 세 개를 리스트에 담고 sorted(태그들, reverse=True) 로 사용 횟수 많은 순으로 출력합니다. 이때 key= 를 쓰지 않습니다.
  • (도전) __eq__ 를 이름만으로 비교하게 정의하고, 이름은 같지만 횟수가 다른 두 태그를 == 로 비교하면 어떤 결과가 나올지 예상하고 확인해 보세요.

생각해볼 주제

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

1. "같다" 의 기준은 누가 정할까?

오늘 우리는 __eq__ 에서 작성자와 캡션이 둘 다 같으면 같은 게시물로 봤어요. 그런데 이 기준은 우리가 정한 것일 뿐이에요.

만약 게시물마다 고유한 번호(id)가 있다면, "id 가 같아야 같은 게시물" 이라고 정의하는 게 더 맞을 수도 있어요. 반대로 작성자만 같으면 같다고 보면 이상한 일이 생기겠죠. __eq__ 를 어떤 기준으로 정의하느냐가 프로그램 전체의 "같음" 의 의미를 바꾼다는 점에서, 여러분이라면 게시물의 "같음" 을 무엇으로 판단할지 생각해 보세요.

2. @property 와 일반 메서드, 무엇으로 만들까?

오늘 is_popular@property 로 만들어 괄호 없이 썼어요. 그냥 메서드로 만들어 is_popular() 라고 괄호를 붙여 쓸 수도 있었죠.

겉보기엔 괄호 하나 차이지만, 읽는 사람에게 주는 느낌이 달라요. 속성처럼 보이는 건 "가벼운 값을 꺼낸다" 는 느낌을, 메서드는 "무언가 일을 시킨다" 는 느낌을 주죠. 만약 그 계산이 아주 오래 걸리는 무거운 작업이라면, @property 로 만들어 속성처럼 보이게 하는 게 좋을까요 나쁠까요? 어떤 경우에 @property 가 어울리고 어떤 경우에 메서드가 어울릴지, 여러분의 기준을 정리해 보세요.

3. 연산자 오버로딩, 어디까지 해도 될까?

파이썬은 __add__ 를 정의하면 + 까지 내 마음대로 만들 수 있어요. 예를 들어 두 게시물을 post1 + post2 로 더하게 만들 수도 있죠.

그런데 게시물을 더한다는 게 무슨 뜻일까요? 좋아요를 합치는 건지, 캡션을 잇는 건지, 읽는 사람마다 다르게 짐작할 거예요. ==< 처럼 의미가 분명한 연산은 오버로딩이 코드를 깔끔하게 만들지만, 의미가 애매한 연산까지 오버로딩하면 오히려 코드를 읽기 어렵게 만들 수 있어요. "편리함" 과 "헷갈림" 사이에서, 연산자 오버로딩을 어디까지 쓰는 게 좋을지 생각해 보세요.

✅ 예시 답안정답 보기

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


🎯 [과제 1 예시답안] 댓글 클래스에 매직 메서드 붙이기

채점 포인트

항목 확인 핵심
클래스 정의 class Comment: + __init__author·text 비교할 데이터를 담기
내용 비교 __eq__ 에서 authortext 둘 다 비교 == 의 기준을 내용으로
길이 정의 __len__len(self.text) 반환 len(댓글) 이 내용 글자 수
확인 같은 댓글 ==True, 다른 댓글 → False 직접 출력해 검증

풀이 예시

Python
# src/instagram/d3_comment_solution.py
# [기초] 댓글(Comment) 에 __eq__·__len__ 매직 메서드 붙이기


class Comment:
    def __init__(self, author, text):
        self.author = author
        self.text = text

    def __eq__(self, other):
        return self.author == other.author and self.text == other.text

    def __len__(self):
        return len(self.text)


if __name__ == "__main__":
    c1 = Comment("minji", "예쁘다!")
    c2 = Comment("minji", "예쁘다!")
    c3 = Comment("jaehoon", "멋져요")

    print(c1 == c2)   # True  — 작성자 + 내용이 같은 댓글
    print(c1 == c3)   # False — 내용이 다름
    print(len(c1))    # 4     — "예쁘다!" 글자 수

실행 결과예요.

텍스트
True
False
4

💡 튜터의 한마디: __eq__return self.author == other.author and self.text == other.text 가 이 과제의 핵심이에요. 작성자와 내용이 둘 다 같아야 같은 댓글로 봤죠. 그래서 c1c2True, 내용이 다른 c3False 가 나왔어요. __len__len(self.text) 를 돌려주게 해서, len(c1) 이 "예쁘다!"(4글자)를 세도록 했고요. 이렇게 매직 메서드 두 개를 더했을 뿐인데, 우리가 만든 Comment==len() 이라는 파이썬 기본 문법과 자연스럽게 어울리게 됐어요.

🎯 [과제 2 예시답안] 회원의 계산된 속성 만들기

채점 포인트

항목 확인 핵심
클래스 정의 __init__username·followers 계산의 재료를 담기
핸들 속성 @property def handle@아이디 괄호 없이 속성처럼
인플루언서 @property def is_influencerfollowers >= 10000 그 자리에서 계산
확인 m.handle·m.is_influencer 괄호 없이 접근 메서드인데 속성처럼

풀이 예시

Python
# src/instagram/d3_member_property_solution.py
# [응용] 회원(Member) 에 계산된 읽기 전용 속성 두 개를 @property 로


class Member:
    def __init__(self, username, followers):
        self.username = username
        self.followers = followers

    @property
    def handle(self):
        return f"@{self.username}"          # 항상 @ 가 붙은 핸들

    @property
    def is_influencer(self):
        return self.followers >= 10000      # 팔로워 1만 이상이면 인플루언서


if __name__ == "__main__":
    m = Member("minji", 85000)
    print(m.handle)          # @minji
    print(m.is_influencer)   # True

    n = Member("jaehoon", 320)
    print(n.handle)          # @jaehoon
    print(n.is_influencer)   # False

실행 결과예요.

텍스트
@minji
True
@jaehoon
False

💡 튜터의 한마디: handleis_influencer 둘 다 메서드 위에 @property 를 붙였어요. 그래서 m.handle() 이 아니라 m.handle 처럼 괄호 없이 속성처럼 꺼내 썼죠. handle 은 항상 @ 가 붙은 형태를 그 자리에서 만들어 주고, is_influencer 는 팔로워 수를 1만과 비교해 계산해요. 만약 minji 의 팔로워가 더 늘어도, 다시 m.is_influencer 를 확인하면 그 순간 다시 계산하니 항상 최신이에요. "저장된 값" 이 아니라 "필요할 때 계산되는 값" 이라는 점이 @property 의 매력이에요.

🎯 [과제 3 예시답안] 인기 해시태그 순위 매기기

채점 포인트

항목 확인 핵심
크기 비교 __lt__self.uses < other.uses 사용 횟수로 정렬 기준
표현 __repr__#이름(횟수) 리스트 출력이 읽기 좋게
계산 속성 @property def is_trendinguses >= 1000 인기 태그 여부
정렬 sorted(태그들, reverse=True), key= 없음 __lt__ 가 기준이 됨

풀이 예시

Python
# src/instagram/d3_hashtag_solution.py
# [심화] 해시태그(HashTag) — __eq__·__lt__ + @property 로 인기순 정렬


class HashTag:
    def __init__(self, name, uses):
        self.name = name
        self.uses = uses

    def __eq__(self, other):
        return self.name == other.name      # 이름이 같으면 같은 해시태그

    def __lt__(self, other):
        return self.uses < other.uses       # 사용 횟수로 크기 비교

    @property
    def is_trending(self):
        return self.uses >= 1000            # 1000번 이상 쓰였으면 인기 태그

    def __repr__(self):
        return f"#{self.name}({self.uses})"


if __name__ == "__main__":
    tags = [
        HashTag("일상", 320),
        HashTag("여행", 8500),
        HashTag("맛집", 1240),
    ]

    # __lt__ 덕분에 사용 횟수 많은 순으로 정렬 (key= 없이)
    for tag in sorted(tags, reverse=True):
        print(f"{tag}  인기태그={tag.is_trending}")

실행 결과예요.

텍스트
#여행(8500)  인기태그=True
#맛집(1240)  인기태그=True
#일상(320)  인기태그=False

💡 튜터의 한마디: __lt__ 에 "사용 횟수로 비교한다" 는 기준 하나만 박았더니, sorted(tags, reverse=True)key= 없이 인기순으로 줄을 세웠어요. 함수형 시간엔 딕셔너리를 정렬하려고 key=lambda 를 넘겼지만, 객체에 __lt__ 를 정의하면 그게 필요 없어지죠. is_trending 은 1000번 기준으로 인기 여부를 계산해 주고요. 도전 과제의 __eq__ 를 이름만 비교하게 두면, HashTag("여행", 8500)HashTag("여행", 100)== 로는 True 가 나와요. 횟수가 달라도 이름이 같으면 같은 태그로 본 거죠. 이렇게 __eq__ 의 기준을 무엇으로 잡느냐가 "같음" 의 의미를 정한다는 걸 직접 확인할 수 있어요.


🤔 [생각해볼 주제 1] "같다" 의 기준은 누가 정할까?

문제 상황 요약

__eq__ 에서 작성자와 캡션이 둘 다 같으면 같은 게시물로 봤어요. 그런데 이 기준은 우리가 정한 것일 뿐이에요. 게시물의 "같음" 을 무엇으로 판단하는 게 맞을까요?

튜터의 가이드 및 해설

핵심은 "무엇이 그 객체를 유일하게 구분하는가" 예요. __eq__ 의 기준을 어떻게 잡느냐에 따라 같은 데이터도 전혀 다르게 묶여요.

기준을 고유 번호(id)로 잡으면

  • 게시물마다 겹치지 않는 번호가 있다면, "id 가 같아야 같은 게시물" 이 가장 정확해요.
  • 작성자가 똑같은 캡션으로 게시물을 두 번 올려도, id 가 다르니 서로 다른 게시물로 구분되죠. 현실의 게시물과 잘 맞아요.

기준을 내용(작성자+캡션)으로 잡으면

  • 오늘처럼 작성자와 캡션이 같으면 같다고 봐요. "중복 게시물 찾기" 같은 상황엔 오히려 이게 유용해요.
  • 다만 같은 사람이 같은 캡션을 일부러 두 번 올린 경우, 둘을 같은 것으로 합쳐버리는 부작용이 있어요.

기준을 작성자만으로 잡으면

  • minji 가 올린 모든 게시물이 서로 == 로 같다고 나와요. 거의 항상 이상한 결과죠. "같음" 의 기준이 너무 느슨하면 의미가 사라져요.
텍스트
   id 로 비교         현실의 게시물과 가장 정확 (겹침 없음)
   내용으로 비교      중복 탐지엔 유용, 일부러 올린 중복은 합쳐짐
   작성자만 비교      너무 느슨함 — 같은 사람 글이 다 같다고 나옴

정답이 하나는 아니에요. "이 프로그램에서 두 게시물이 같다는 게 무슨 뜻이어야 하나" 를 먼저 정하고, 그 의미에 맞게 __eq__ 를 정의하는 게 순서예요.

💡 핵심을 한마디로

__eq__ 의 기준이 곧 그 프로그램에서 "같음" 의 정의예요. 보통은 객체를 유일하게 구분하는 값(고유 id)으로 잡는 게 안전하고, 기준이 느슨할수록 엉뚱한 것들이 같다고 묶여요.

🤔 [생각해볼 주제 2] @property 와 일반 메서드, 무엇으로 만들까?

문제 상황 요약

is_popular@property 로 만들어 괄호 없이 썼어요. 그냥 메서드로 만들어 is_popular() 라고 쓸 수도 있었죠. 둘 중 무엇으로 만드는 게 좋을까요?

튜터의 가이드 및 해설

겉보기엔 괄호 하나 차이지만, 읽는 사람에게 주는 신호가 달라요.

@property 가 어울리는 경우

  • 좋아요 수로 인기 여부를 판단하듯, 이미 가진 데이터로 가볍게 계산해 꺼내는 값일 때.
  • "이건 이 객체의 상태다" 라는 느낌이 자연스러울 때 — post.is_popular 는 마치 게시물의 한 속성처럼 읽혀요.

일반 메서드가 어울리는 경우

  • 무언가 동작을 시키는 느낌일 때 — post.add_like() 처럼 상태를 바꾸는 일은 괄호가 있어야 "일을 시킨다" 가 분명해요.
  • 계산이 아주 무겁거나(시간이 오래 걸리거나), 부를 때마다 결과가 달라질 수 있을 때. 속성처럼 보이면 "가벼운 값" 으로 오해하기 쉬워요.

여기서 중요한 함정이 하나 있어요. @property 는 "가벼운 값처럼 보이게" 하는 도구라, 그 안에 무거운 작업을 숨기면 읽는 사람이 깜빡 속아요. post.summary 한 줄이 사실은 몇 초씩 걸리는 작업이라면, 코드를 읽는 사람은 그걸 예상하지 못하죠. 그래서 무거운 일은 일부러 메서드(괄호)로 드러내는 게 친절해요.

💡 핵심을 한마디로

가볍게 계산되는 "상태 같은 값" 은 @property, 동작을 시키거나 무거운 작업은 메서드(괄호)로 만들어요. 속성처럼 보이는 곳에 무거운 작업을 숨기면 읽는 사람이 비용을 예상하지 못해요.

🤔 [생각해볼 주제 3] 연산자 오버로딩, 어디까지 해도 될까?

문제 상황 요약

파이썬은 __add__ 를 정의하면 + 까지 내 마음대로 만들 수 있어요. 두 게시물을 post1 + post2 로 더하게 만들 수도 있죠. 그런데 이렇게까지 해도 될까요?

튜터의 가이드 및 해설

기준은 "그 연산의 의미가 누가 봐도 분명한가" 예요.

오버로딩이 좋은 경우

  • ==(같다)·<(작다)처럼 의미가 명확한 연산. post1 < post2 가 "좋아요가 더 적다" 로 자연스럽게 읽히면, 오버로딩이 코드를 깔끔하게 만들어요.
  • 수학적으로 자연스러운 대상. 예를 들어 좌표나 금액을 다루는 객체에 + 를 정의하면, 누구나 "더한다" 의 뜻을 똑같이 짐작해요.

오버로딩을 피해야 하는 경우

  • 의미가 애매한 연산. post1 + post2 는 좋아요를 합치는 건지, 캡션을 잇는 건지, 읽는 사람마다 다르게 짐작해요. 이러면 오히려 코드가 더 어려워지죠.
  • "신기하니까" 정의하는 경우. 연산자 오버로딩은 멋있어 보이지만, 읽는 사람을 헷갈리게 하면 안 한 것만 못해요.

오버로딩의 목표는 "코드를 더 읽기 쉽게" 예요. ==·<·len() 처럼 누구나 같은 의미로 받아들이는 연산만 정의하고, 의미가 갈리는 연산은 차라리 이름 있는 메서드(예: merge_likes())로 만드는 게 친절해요. 이름이 있으면 무슨 일을 하는지 분명하니까요.

💡 핵심을 한마디로

의미가 누가 봐도 분명한 연산(==·<·len())만 오버로딩하고, 뜻이 갈리는 연산은 이름 있는 메서드로 만들어요. 오버로딩의 목적은 과시가 아니라 "코드를 더 읽기 쉽게" 예요.

전체 목록 파이썬 기초