문서 읽는 데 51분 · D1

D-1: 클래스 — 데이터와 행동을 하나로 묶기

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

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

안녕하세요, 홍순구 튜터입니다. 지난 시간엔 코드를 여러 파일(모듈)로 나누고 import 로 가져다 쓰는 정리정돈의 기술을 배웠어요. 카테고리 C 를 멋지게 마무리하고, 오늘부터는 새 카테고리 D, 객체지향의 문을 엽니다.

지난 시간 마지막을 떠올려 볼까요? 우리는 게시물을 딕셔너리 {"title": ..., "likes": ...} 로 표현하고, 그걸 다루는 함수(top_by_likes·total_likes)를 따로 만들었어요. 데이터는 데이터대로, 함수는 함수대로 떨어져 있었죠.

그런데 잘 생각해 보면, "게시물" 이라는 하나의 대상에 "제목·좋아요" 라는 데이터와 "좋아요를 누른다" 라는 행동이 같이 붙어 다니면 더 자연스럽지 않을까요? 오늘은 이 둘을 하나로 묶는 클래스(class) 를 배웁니다. 붕어빵 틀로 붕어빵을 찍어내듯, 같은 모양의 데이터를 찍어내는 설계도예요.

텍스트
   지난 시간 (C-3)              오늘 배울 것 (D-1)
   ──────────────              ──────────────────
   코드를 파일로 나눴다   ──>   데이터 + 행동을 클래스로 묶기
                                │
                                ├─ class — 설계도(틀)
                                ├─ __init__ / self
                                ├─ 속성(데이터) / 메서드(행동)
                                ├─ 인스턴스 변수 vs 클래스 변수
                                └─ __str__ / __repr__

__init__·self 같은 낯선 식구도 만나게 돼요. 처음엔 기호가 좀 어색해도 괜찮습니다. 하나씩 직접 실행하며 "아, 이렇게 묶이는구나" 를 눈으로 확인하며 갈게요. 카테고리 D, 함께 출발해 봅시다!

💡 오늘 수업의 핵심 — "데이터(속성)와 행동(메서드)을 클래스 하나로 묶는다"

🎯 학습 목표

  • 클래스(class)로 데이터(속성)와 행동(메서드)을 하나의 설계도에 묶을 수 있어요
  • __init__self 로 인스턴스를 만들고, 인스턴스 변수와 클래스 변수를 구분해요
  • __str____repr__ 으로 객체를 사람이 읽기 좋게 보여줄 수 있어요

Step 1: "데이터와 행동이 따로 노는 불편함"

본격적으로 새 문법을 배우기 전에, "왜 클래스가 필요한가" 부터 짚고 갈게요. 새 도구는 항상 "기존 방식이 불편해서" 나오거든요.

지난 시간까지 우리가 게시물을 다루던 방식을 떠올려 봅시다. 게시물 데이터는 딕셔너리에 담고, 그 데이터를 다루는 동작은 함수로 따로 만들었어요.

Python
# src/instagram/d1_before.py
post = {"title": "한강 야경", "likes": 120}


def like(p):
    p["likes"] = p["likes"] + 1


def describe(p):
    title = p["title"]
    likes = p["likes"]
    return f'"{title}" — 좋아요 {likes}개'


like(post)
print(describe(post))
# 출력: "한강 야경" — 좋아요 121개

잘 동작하긴 해요. 그런데 자세히 보면 좀 불편한 구석이 있어요. likedescribe 는 분명히 "게시물" 을 다루는 함수인데, 게시물 데이터(post)와는 따로 떨어져 있어요. 함수를 부를 때마다 게시물을 like(post) 처럼 매번 넘겨줘야 하죠.

게시물 종류가 늘고 다루는 동작도 많아지면, "이 함수가 어떤 데이터를 위한 거였더라?" 하고 헷갈리기 시작해요. 데이터와 그 데이터를 다루는 행동이 따로 노는 거예요.

텍스트
   [ 지금까지 ] 데이터와 행동이 따로

      post = {"title": "한강 야경", "likes": 120}    데이터(dict)
      def like(post): ...                           행동(함수)
      def describe(post): ...                        행동(함수)

   [ 오늘 ] 하나로 묶기 — 설계도(클래스) 하나에

      Post
        ├─ 데이터(속성):  title, likes
        └─ 행동(메서드):  like(), describe()

"제목·좋아요 같은 데이터" 와 "좋아요를 누르고 설명을 만드는 행동" 을 한 덩어리로 묶을 수 있다면 훨씬 깔끔하겠죠? 그게 바로 오늘 배울 클래스(class) 예요. 데이터와 행동을 하나로 묶는 설계도입니다.

💡 한 줄 정리

데이터(dict)와 그걸 다루는 행동(함수)이 따로 떨어져 있으면 관리가 번거롭다 — 이 둘을 하나로 묶는 도구가 클래스다.

🙋 학생 질문 — "튜터님, dict 랑 함수로도 잘 되는데 왜 굳이 클래스를 또 배워요?"

맞아요, 지금처럼 게시물 하나에 함수 두어 개면 dict 와 함수로도 충분해요. 클래스가 빛나는 건 다루는 대상이 많아지고 복잡해질 때예요.

회원·게시물·댓글… 각각이 자기 데이터와 행동을 잔뜩 가지게 되면, "이 데이터는 누구 거고, 이 함수는 무엇을 위한 거지?" 가 점점 헷갈려요. 클래스로 묶어두면 "회원에 관한 건 다 Member 안에 있어" 처럼 한곳에 모여서 찾기도, 고치기도 쉬워져요. 오늘은 작게 시작하지만, 카테고리 D 를 지나며 그 가치가 점점 또렷해질 거예요.


Step 2: "첫 클래스 — 설계도와 실체"

이제 진짜 클래스를 만들어 봅시다. 가장 단순한 형태부터 시작할게요. 회원을 표현하는 Member 라는 클래스를 만들어 보겠습니다.

Python
# src/instagram/d1_first_class.py
class Member:
    pass


a = Member()   # 설계도(Member)로 실체(인스턴스) 하나를 찍어냈어요
b = Member()   # 또 하나 찍어냈어요

print(a)
print(type(a))
print(a is b)  # 같은 붕어빵일까요?

class Member: 가 클래스를 정의하는 문법이에요. 클래스 이름은 첫 글자를 대문자로 쓰는 게 약속이에요(Member, Post 처럼요). 아직 안에 내용이 없어서 pass 라고만 적었어요. "여긴 비어 있다" 는 표시예요.

중요한 건 그다음이에요. Member() 처럼 클래스 이름 뒤에 괄호를 붙여 부르면, 그 설계도로 실체 하나를 찍어내요. 이렇게 찍어낸 실체를 인스턴스(instance) 라고 불러요. 위에서 ab, 두 개의 인스턴스를 만들었죠.

붕어빵으로 비유하면 이렇게 나뉘어요.

텍스트
        class Member             붕어빵 틀 (설계도)
            │
            │   Member()         틀로 하나씩 찍어내기
            
     ┌──────┬──────┬──────┐
     │  a   │  b   │  c   │      찍어낸 붕어빵 (인스턴스)
     └──────┴──────┴──────┘
     서로 다른 객체  (a is b 는 False)

실행하면 이렇게 나와요.

텍스트
<__main__.Member object at 0x104f8e3c0>
<class '__main__.Member'>
False

print(a) 는 아직 좀 못생겼죠? <__main__.Member object at 0x...> 라고 나오는데, "Member 인스턴스가 메모리 어딘가(0x...)에 있다" 는 뜻이에요. 뒤의 주소(0x...)는 실행할 때마다 달라지니 숫자가 다르게 나와도 정상이에요. 이 표시를 예쁘게 바꾸는 법은 Step 7 에서 배웁니다.

마지막 a is bFalse 인 게 핵심이에요. 같은 틀(Member)로 찍었지만 ab서로 다른 붕어빵, 즉 별개의 인스턴스예요.

💡 한 줄 정리

class 이름: 으로 설계도를 만들고, 이름() 으로 부르면 인스턴스(실체)가 하나씩 찍혀 나온다.

🙋 학생 질문 — "튜터님, class Member 안에 적은 pass 는 뭐예요?"

pass 는 "여기엔 아무것도 안 할게요" 라는 빈자리 표시예요. 파이썬은 class Member: 라고 콜론을 찍으면 그 아래에 들여쓰기된 내용이 반드시 와야 해요(함수 def 와 똑같죠). 그런데 아직 넣을 내용이 없으니, "비었지만 문법은 맞춰둔다" 는 의미로 pass 를 적어요.

다음 Step 부터 이 pass 자리에 __init__ 과 메서드가 들어가면, pass 는 자연스럽게 빠집니다. 지금은 "텅 빈 설계도" 라는 뜻으로만 봐두세요.


Step 3: "__init__self — 태어날 때 채우기"

빈 설계도는 만들었지만, 회원이라면 이름 정도는 있어야겠죠? 인스턴스를 만들 때 데이터를 채워 넣는 특별한 함수가 있어요. 바로 __init__ 이에요.

Python
# src/instagram/d1_init.py
class Member:
    def __init__(self, username):
        self.username = username   # self = 지금 막 태어난 그 인스턴스


a = Member("jaehoon")   # Member(...) 라고 부르는 순간 __init__ 이 자동 실행돼요
b = Member("minji")

print(a.username)
print(b.username)

__init__ 은 인스턴스가 태어날 때 자동으로 한 번 불리는 특별한 메서드예요. 이름 앞뒤의 밑줄 두 개(__)는 "파이썬이 특별하게 다루는 이름" 이라는 표시예요(지난 시간 if __name__ == "__main__" 에서 봤던 그 밑줄과 같아요).

Member("jaehoon") 처럼 괄호 안에 값을 넣어 부르면, 그 값이 __init__ 으로 전달돼요. 그럼 self.username = username 한 줄이 실행되면서, 그 인스턴스에 username 이라는 데이터가 담겨요.

여기서 가장 중요한 단어가 self 예요. self 는 "지금 막 태어나는 바로 그 인스턴스 자신" 을 가리켜요.

텍스트
   m = Member("jaehoon")
        │
        │    Member(...) 부르는 순간 __init__ 자동 실행
        
   def __init__(self, username):
       self.username = username
         │        │
         │        └─ "jaehoon" 이 username 으로 들어옴
         └─ self = 지금 태어나는 그 인스턴스 (= m)

실행 결과는 이래요.

텍스트
jaehoon
minji

a = Member("jaehoon") 으로 만든 ausername"jaehoon", busername"minji". 같은 설계도에서 나왔지만 각자 자기 이름을 가지게 됐죠. self 덕분에 "누구의 username 인지" 가 인스턴스마다 따로 관리돼요.

💡 한 줄 정리

__init__ 은 인스턴스가 태어날 때 자동 실행되는 함수이고, self 는 그 인스턴스 자신을 가리킨다 — self.속성 = 값 으로 데이터를 채운다.

🙋 학생 질문 — "튜터님, Member("jaehoon") 은 값을 하나만 넣었는데 왜 init 은 (self, username) 둘이에요?"

아주 좋은 관찰이에요! self 는 우리가 직접 넣는 값이 아니에요. Member("jaehoon") 이라고 부르면, 파이썬이 "방금 만든 인스턴스" 를 self 자리에 자동으로 끼워 넣어줘요. 우리가 적은 "jaehoon" 은 그다음 자리인 username 으로 들어가고요.

그래서 우리가 괄호에 넣는 값은 self 다음 인자들이에요. self 는 "파이썬이 알아서 채워주는 첫 번째 자리" 라고 기억해 두면 편해요. 메서드를 만들 때는 항상 첫 인자로 self 를 적되, 부를 땐 신경 쓰지 않아도 돼요.


Step 4: "속성 — 인스턴스마다 다른 데이터"

__init__ 에서 채운 self.username 같은 데이터를 속성(attribute) 이라고 불러요. 이번엔 속성을 여러 개 두고, 인스턴스마다 값이 어떻게 따로 노는지 확인해 봅시다.

Python
# src/instagram/d1_attributes.py
class Member:
    def __init__(self, username):
        self.username = username
        self.followers = 0   # 처음엔 팔로워 0
        self.posts = 0       # 처음엔 게시물 0


a = Member("jaehoon")
b = Member("minji")

# 만든 뒤에 속성 값을 바꿀 수도 있어요
a.followers = 1240
b.followers = 8500

print(a.username, a.followers, a.posts)
print(b.username, b.followers, b.posts)

__init__ 에서 속성을 세 개 채웠어요. username 은 받은 값으로, followersposts 는 일단 0 으로 시작하게 했죠. 새 회원이라면 팔로워도 게시물도 0 부터 시작하는 게 자연스러우니까요.

속성을 꺼내 볼 땐 a.followers 처럼 인스턴스.속성이름 으로 점을 찍어 접근해요. 지난 시간 모듈에서 모듈.함수() 라고 점을 찍었던 것과 같아요. 또 a.followers = 1240 처럼 만든 뒤에 값을 바꿀 수도 있어요.

텍스트
   a = Member("jaehoon")            b = Member("minji")

   a ─┐                             b ─┐
      ├ username  = "jaehoon"          ├ username  = "minji"
      ├ followers = 1240               ├ followers = 8500
      └ posts     = 0                  └ posts     = 0

   같은 틀에서 나왔지만, 각자 자기 데이터를 따로 가진다

실행하면 이래요.

텍스트
jaehoon 1240 0
minji 8500 0

a.followers 를 1240 으로 바꿔도 b.followers 는 8500 그대로예요. 인스턴스가 각자 자기만의 속성 값을 들고 있다는 게 핵심이에요. a 를 고쳐도 b 는 멀쩡하죠.

💡 한 줄 정리

속성은 self.이름 = 값 으로 만들고 인스턴스.이름 으로 꺼낸다 — 인스턴스마다 자기만의 값을 따로 가진다.

🙋 학생 질문 — "튜터님, 속성을 init 에 안 적고 a.followers = 1240 처럼 나중에 추가해도 되나요?"

문법적으로는 가능해요. 파이썬은 a.nickname = "재훈" 처럼 나중에 없던 속성을 슬쩍 붙이는 것도 허용하거든요. 하지만 권하지는 않아요.

왜냐하면 어떤 인스턴스엔 nickname 이 있고 어떤 인스턴스엔 없으면, 나중에 b.nickname 을 꺼낼 때 "없는 속성" 이라 에러가 나거든요. 그래서 그 클래스가 가질 데이터는 __init__ 에서 한자리에 모아 정해두는 게 안전해요. "이 회원은 username·followers·posts 를 가진다" 가 __init__ 만 보면 한눈에 들어오니까요.


Step 5: "메서드 — 클래스 안의 함수"

데이터(속성)는 묶었으니, 이제 행동을 묶을 차례예요. 클래스 안에 정의한 함수를 메서드(method) 라고 불러요. 메서드는 self 를 통해 자기 속성을 읽고 고칠 수 있어요.

Python
# src/instagram/d1_method.py
class Member:
    def __init__(self, username):
        self.username = username
        self.followers = 0
        self.posts = 0

    def add_follower(self):
        self.followers = self.followers + 1   # 자기 팔로워를 1 늘려요

    def write_post(self):
        self.posts = self.posts + 1           # 자기 게시물을 1 늘려요


m = Member("jaehoon")
m.add_follower()
m.add_follower()
m.write_post()

print(m.username, "팔로워", m.followers, "게시물", m.posts)

add_followerwrite_post 가 메서드예요. 일반 함수와 똑같이 def 로 만들지만, 클래스 안에 들여쓰기되어 있고 첫 인자가 self 라는 점이 달라요. 이 self 덕분에 메서드 안에서 self.followers 처럼 자기 속성에 접근할 수 있어요.

메서드를 부를 땐 m.add_follower() 처럼 인스턴스.메서드() 로 불러요. 이렇게 부르면 self 자리에 m 이 자동으로 들어가서, self.followers 가 곧 m.followers 가 돼요. 그래서 m.add_follower() 를 두 번 부르면 m 의 팔로워가 2 가 되죠.

지난 시간엔 게시물 dict 를 함수에 like(post) 처럼 넘겨줬지만, 이제는 데이터(followers)와 행동(add_follower)이 Member 안에 한 몸으로 들어있어요. 바로 이게 "데이터와 행동을 묶는다" 의 정체예요.

실행 결과는 이래요.

텍스트
jaehoon 팔로워 2 게시물 1

add_follower() 두 번, write_post() 한 번을 불렀더니 팔로워 2, 게시물 1 이 됐어요. 메서드가 자기 속성을 정확히 고쳐준 거죠.

💡 한 줄 정리

메서드는 클래스 안에 정의한 함수로, 첫 인자 self 를 통해 자기 속성을 읽고 고친다 — 인스턴스.메서드() 로 부른다.

🙋 학생 질문 — "튜터님, 메서드도 그냥 함수인데 왜 굳이 self 를 또 적어요?"

메서드는 "어떤 인스턴스의" 동작인지가 중요하기 때문이에요. add_follower() 를 부를 때, 파이썬은 "누구의 팔로워를 올릴까?" 를 알아야 하잖아요. 그 "누구" 가 바로 self 예요.

m.add_follower() 라고 부르면 selfm 이 들어가서 m 의 팔로워가 올라가고, other.add_follower() 라고 부르면 selfother 가 들어가서 other 의 팔로워가 올라가요. 같은 메서드인데도 부른 인스턴스마다 다른 대상을 고치는 거죠. self 가 없으면 "누구 걸 고칠지" 를 알 수가 없어요.


Step 6: "인스턴스 변수 vs 클래스 변수"

지금까지 본 self.username, self.followers 는 인스턴스마다 따로인 값이었어요. 이런 걸 인스턴스 변수 라고 불러요. 그런데 가끔은 "모든 인스턴스가 똑같이 공유하는 값" 이 필요할 때가 있어요. 그게 클래스 변수 예요.

Python
# src/instagram/d1_class_var.py
class Member:
    platform = "instagram"   # 클래스 변수 — 모든 회원이 같은 값을 공유
    count = 0                # 지금까지 만든 회원 수

    def __init__(self, username):
        self.username = username           # 인스턴스 변수 — 각자 다름
        Member.count = Member.count + 1    # 회원이 하나 생길 때마다 +1


a = Member("jaehoon")
b = Member("minji")

print(a.username, a.platform)
print(b.username, b.platform)
print("총 회원 수:", Member.count)
print("플랫폼:", Member.platform)

차이를 보세요. platformcount__init__ 바깥, 클래스 바로 아래에 적었어요. 이렇게 적으면 모든 인스턴스가 공유하는 클래스 변수가 돼요. 반면 self.username__init__ 안에서 self. 을 붙여 만든 인스턴스 변수라 각자 다르고요.

platform 은 "이 서비스의 회원은 모두 instagram 소속" 이라는, 모두에게 똑같은 값이에요. count 는 회원이 하나 태어날 때마다(__init__ 이 불릴 때마다) Member.count = Member.count + 1 로 하나씩 올라가요. 그래서 지금까지 만든 회원 수를 셀 수 있죠.

텍스트
   Member  (클래스)
     │
     ├─ platform = "instagram"     클래스 변수: 모든 인스턴스가 공유
     ├─ count    = 2               클래스 변수: 만든 인스턴스 수
     │
     ├─ a: username = "jaehoon"    인스턴스 변수: a 만의 값
     └─ b: username = "minji"      인스턴스 변수: b 만의 값

실행하면 이래요.

텍스트
jaehoon instagram
minji instagram
총 회원 수: 2
플랫폼: instagram

a.platformb.platform 도 똑같이 instagram 이에요(공유하니까요). 회원을 둘 만들었더니 Member.count 가 2 가 됐고요. 공유하는 값은 클래스 변수, 각자 다른 값은 인스턴스 변수 — 이 구분이 핵심이에요.

💡 한 줄 정리

클래스 바로 아래에 적은 변수는 모든 인스턴스가 공유하는 클래스 변수, __init__ 안에서 self. 로 만든 변수는 인스턴스마다 따로인 인스턴스 변수다.

🙋 학생 질문 — "튜터님, count 를 self.count = self.count + 1 로 올리면 안 되나요?"

그렇게 하면 우리가 원하는 대로 안 돌아가요. self.count = ... 라고 쓰는 순간, 그 인스턴스만의 새 count 가 생겨버려요(인스턴스 변수가 클래스 변수를 가려요). 그래서 전체 회원 수를 세는 공유 카운터는 안 올라가고, 인스턴스마다 1 만 들고 있게 돼요.

전체가 공유하는 값을 고칠 땐 Member.count 처럼 클래스 이름을 콕 집어 올려야 해요. 그래야 "모두가 보는 그 하나의 count" 가 제대로 증가하거든요. "공유 값을 바꿀 땐 클래스 이름으로" 라고 기억해 두세요.


Step 7: "__str____repr__ — 객체를 사람이 읽게"

Step 2 에서 print(a)<__main__.Member object at 0x...> 처럼 못생기게 나왔던 거 기억나죠? 이제 그걸 예쁘게 바꿀 차례예요. __str____repr__, 두 가지 특별한 메서드를 쓰면 돼요.

Python
# src/instagram/d1_str_repr.py
class Member:
    def __init__(self, username, followers):
        self.username = username
        self.followers = followers

    def __str__(self):
        return f"@{self.username} (팔로워 {self.followers}명)"

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


m = Member("jaehoon", 1240)

print(m)         # print 는 __str__ 을 써요
print(repr(m))   # repr 은 __repr__ 을 써요
print([m])       # 리스트 안에서는 __repr__ 이 쓰여요

__str____repr____init__ 처럼 밑줄 두 개로 감싼 특별한 메서드예요. 둘 다 문자열을 return 하는데, 쓰이는 곳이 달라요.

  • __str__사람이 읽기 좋은 표현이에요. print(m) 이나 str(m) 을 하면 이게 쓰여요.
  • __repr__개발자를 위한 표현이에요. repr(m) 을 하거나, 리스트 안에 담긴 채로 출력될 때 이게 쓰여요. 보통 "이 객체를 어떻게 만들었는지" 가 드러나게 적어요.
텍스트
   print(m)   ─────>  __str__   ─────>  @jaehoon (팔로워 1240명)
   str(m)     ─────>  __str__            (사람이 읽기 좋은 표현)

   repr(m)    ─────>  __repr__  ─────>  Member(username='jaehoon', ...)
   [m]        ─────>  __repr__           (개발자가 보는 표현)

실행하면 이렇게 나와요.

텍스트
@jaehoon (팔로워 1240명)
Member(username='jaehoon', followers=1240)
[Member(username='jaehoon', followers=1240)]

print(m)__str__ 이 만든 깔끔한 문장으로, repr(m)__repr__ 이 만든 개발자용 표현으로 나와요. 재미있는 건 print([m]) 처럼 리스트에 담아 출력하면 __str__ 이 아니라 __repr__ 이 쓰인다는 점이에요. 리스트 안 요소는 "정확히 뭔지" 를 보여주는 게 더 유용하니까요.

__str__·__repr__ 처럼 밑줄로 감싼 특별한 메서드들을 한데 묶어 부르는 이름이 있는데, 그건 다음 시간에 만나봅시다.

💡 한 줄 정리

__str__print·str 에 쓰이는 사람용 표현, __repr__repr·리스트 출력에 쓰이는 개발자용 표현이다.

🙋 학생 질문 — "튜터님, str 하나만 있으면 되지 repr 은 왜 또 있어요?"

용도가 다르기 때문이에요. __str__ 은 사용자에게 예쁘게 보여주는 게 목적이라, @jaehoon (팔로워 1240명) 처럼 읽기 좋게 꾸며요. 반면 __repr__ 은 개발자가 디버깅할 때 "이 객체가 정확히 어떤 값으로 이뤄졌나" 를 보려는 거라, Member(username='jaehoon', followers=1240) 처럼 속을 그대로 드러내요.

그래서 리스트 안에 객체 100 개가 있을 때 출력하면 __repr__ 이 쓰여요. 각 객체의 정체를 정확히 알아야 디버깅에 도움이 되니까요. 둘 중 하나만 만든다면 __repr__ 을 먼저 만드는 걸 권하는데, __str__ 이 없으면 print__repr__ 을 대신 쓰거든요. 지금은 "사람용 __str__, 개발자용 __repr__" 만 기억해 두면 충분해요.


Step 8: "종합 — Member 클래스 완성"

오늘 배운 걸 전부 모아서, 제대로 된 Member 클래스를 완성해 봅시다. 속성·메서드·클래스 변수·__str__·__repr__ 을 한 클래스에 담아볼게요. 이건 앞으로 카테고리 D 내내 함께할 우리 도메인의 첫 주인공이에요.

Python
# src/instagram/member.py
"""인스타그램 회원(Member)을 표현하는 클래스."""


class Member:
    """한 명의 회원.

    username·followers·posts 를 가지고, 팔로워 추가·게시물 작성을 할 수 있어요.
    """

    platform = "instagram"   # 클래스 변수 — 모든 회원이 공유
    count = 0                # 지금까지 가입한 회원 수

    def __init__(self, username):
        self.username = username
        self.followers = 0
        self.posts = 0
        Member.count = Member.count + 1

    def add_follower(self):
        self.followers = self.followers + 1

    def write_post(self):
        self.posts = self.posts + 1

    def __str__(self):
        return f"@{self.username} · 팔로워 {self.followers} · 게시물 {self.posts}"

    def __repr__(self):
        return f"Member(username='{self.username}')"


if __name__ == "__main__":
    jaehoon = Member("jaehoon")
    jaehoon.add_follower()
    jaehoon.add_follower()
    jaehoon.write_post()

    minji = Member("minji")
    minji.add_follower()

    print(jaehoon)
    print(minji)
    print(f"플랫폼: {Member.platform}, 총 회원 수: {Member.count}")
    print(repr(jaehoon))

차근차근 뜯어보면 전부 오늘 배운 거예요. 맨 위 platform·count 는 클래스 변수, __init__ 에서 채우는 username·followers·posts 는 인스턴스 변수예요. add_follower·write_post 는 자기 속성을 고치는 메서드고, __str__·__repr__ 으로 보기 좋은 표현까지 갖췄죠.

맨 아래 if __name__ == "__main__": 는 지난 시간에 배운 가드예요. 이 파일을 직접 실행할 때만 아래 시연 코드가 돌아가고, 다른 파일이 from instagram.member import MemberMember 만 가져다 쓸 땐 시연 코드가 돌지 않게 막아줘요.

터미널에서 직접 실행해 봅시다.

Bash
uv run python src/instagram/member.py
텍스트
@jaehoon · 팔로워 2 · 게시물 1
@minji · 팔로워 1 · 게시물 0
플랫폼: instagram, 총 회원 수: 2
Member(username='jaehoon')

재훈이는 팔로워 2·게시물 1, 민지는 팔로워 1·게시물 0 으로 각자 자기 데이터를 정확히 들고 있어요. 회원을 둘 만들었으니 Member.count 는 2 고요. 데이터와 행동이 Member 하나로 깔끔하게 묶인 모습이에요. 지난 시간 dict 와 함수가 따로 놀던 것과 비교하면 훨씬 한 덩어리죠?

참고로 이 Member 클래스가 의도대로 동작하는지는 코드베이스의 tests/test_d1_class.py 가 여러 경우로 검증해 두었어요(팔로워가 잘 올라가는지, 인스턴스끼리 데이터가 섞이지 않는지, count 가 정확한지 등). 우리는 그 위에서 안심하고 다음으로 나아가면 됩니다.

💡 한 줄 정리

하나의 클래스에 클래스 변수·인스턴스 변수·메서드·__str__·__repr__ 을 모두 담아, 데이터와 행동이 한 몸인 완성된 설계도를 만들 수 있다.

🙋 학생 질문 — "튜터님, followers 를 init 에서 0 으로 고정했는데, 처음부터 값을 받게 하면 안 되나요?"

당연히 가능해요! def __init__(self, username, followers): 처럼 매개변수를 늘리고 self.followers = followers 라고 받으면, 만들 때 팔로워 수를 정해줄 수 있어요. Step 7 의 d1_str_repr.py 가 바로 그렇게 했었죠.

여기서 0 으로 고정한 건 "갓 가입한 새 회원" 을 표현하고 싶어서예요. 새 회원이라면 팔로워·게시물이 0 부터 시작하는 게 자연스러우니까요. 정답이 있는 건 아니에요. "어떤 값을 만들 때 받고, 어떤 값을 기본으로 둘지" 는 그 클래스가 표현하려는 대상에 맞춰 설계자가 정하는 거예요.


마무리

오늘은 데이터와 행동을 하나로 묶는 클래스(class) 를 배웠어요. 빈 설계도에서 시작해 __init__·self 로 속성을 채우고, 메서드로 행동을 붙이고, 클래스 변수로 모두가 공유하는 값을 두고, __str__·__repr__ 으로 보기 좋게 표현하는 것까지 — 카테고리 D 의 문을 멋지게 열었습니다.

오늘 배운 핵심 세 가지

💡 하나: 클래스는 데이터(속성)와 행동(메서드)을 묶는 설계도예요. class 이름: 으로 정의하고 이름() 으로 부르면 인스턴스(실체)가 찍혀 나와요. __init__ 은 태어날 때 자동 실행되어 속성을 채우고, self 는 그 인스턴스 자신을 가리켜요.

💡 : 속성과 메서드는 self 를 통해 인스턴스마다 따로 관리돼요. self.followers 같은 인스턴스 변수는 각자 다르고, 클래스 바로 아래 적은 platform·count 같은 클래스 변수는 모든 인스턴스가 공유해요.

💡 : __str__print·str 에 쓰이는 사람용 표현, __repr__repr·리스트 출력에 쓰이는 개발자용 표현이에요. 객체를 사람이 읽기 좋게 보여주는 두 가지 창구죠.

다음 시간 예고

오늘 우리는 Member 라는 클래스 하나를 만들었어요. 그런데 인스타그램엔 게시물도 여러 종류가 있죠? 사진 게시물, 동영상 게시물처럼요. 이들은 "게시물" 이라는 공통점이 있으면서도 저마다 조금씩 다르게 동작해요.

다음 시간(D-2)엔 이렇게 "공통된 부분은 물려받고, 다른 부분만 새로 쓰는" 상속(inheritance) 을 배웁니다. Post 라는 부모 설계도를 만들고, 그걸 물려받은 PhotoPost·VideoPost 를 만들어 볼 거예요. 부모의 기능을 가져오는 super(), 똑같은 메서드 이름이 인스턴스 종류에 따라 다르게 동작하는 다형성도 만나고요.

그리고 오늘 마지막에 본 __str__·__repr__ 처럼 밑줄로 감싼 특별한 메서드들 — 이들을 매직 메서드 라고 부르는데, 그 다음 시간(D-3)엔 두 객체를 == 로 비교하거나 크기순으로 정렬하게 해주는 매직 메서드들, 그리고 메서드를 속성처럼 쓰게 해주는 @property 까지 다뤄봅니다. 오늘 만든 Member 와 다음 시간의 Post 가 한층 더 똑똑해질 거예요. 기대해 주세요!


과제

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

[기초] 나만의 Member 클래스 만들기

회원을 표현하는 클래스를 직접 설계해 보세요.

  • Member 클래스를 만들고, __init__ 에서 usernamebio(자기소개) 두 가지를 받습니다
  • introduce() 메서드를 만들어 @username — bio 형태의 소개 문장을 돌려주게 합니다
  • 인스턴스를 두 개 만들어 각각 introduce() 결과를 출력해 봅니다
  • (생각해보기) 두 인스턴스의 bio 가 서로 다른 값으로 잘 유지되나요? 왜 그럴까요?

[응용] 게시물을 클래스로 묶기

지난 시간 dict 로 다루던 게시물을, 이번엔 클래스로 묶어 보세요.

  • Post 클래스를 만들고, __init__ 에서 title 을 받습니다. likes 는 처음에 0 으로 시작하게 합니다
  • add_like() 메서드로 좋아요를 1 늘립니다
  • __str__ 을 만들어 제목 (좋아요 N개) 형태로 보이게 합니다
  • 게시물 하나를 만들어 좋아요를 몇 번 누른 뒤 print 로 출력해 봅니다
  • (생각해보기) 데이터(title·likes)와 행동(add_like)이 한 클래스에 묶이니, 지난 시간 dict+함수 방식과 비교해 무엇이 편해졌나요?

[심화] 클래스 변수로 게시물 개수 세기

응용 과제의 Post 에 "지금까지 만든 게시물 수" 를 세는 기능을 더해 보세요.

  • Post 클래스에 클래스 변수 count 를 0 으로 두고, __init__ 에서 게시물이 하나 생길 때마다 Post.count 를 1 늘립니다
  • __repr__ 도 만들어 Post(title='제목') 형태로 개발자용 표현을 갖춥니다
  • 게시물을 세 개 만든 뒤, 하나는 print(...) 로(사람용), 하나는 print(repr(...)) 로(개발자용) 출력하고, 마지막에 Post.count 를 출력해 봅니다
  • (도전) Post.count 를 올릴 때 self.count 로 쓰면 왜 전체 개수가 제대로 안 세지는지 직접 바꿔 실행해 확인해 보세요

생각해볼 주제

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

1. 언제 클래스를 쓰고, 언제 그냥 함수와 dict 로 충분할까?

오늘 우리는 dict+함수로 다루던 게시물을 클래스로 묶었어요. 클래스는 데이터와 행동을 한곳에 모아줘서 편하죠.

하지만 모든 걸 클래스로 만들어야 하는 건 아니에요. 값 몇 개를 잠깐 묶어 넘기는 정도라면 dict 가 더 가볍고 간단할 때도 많아요. 어떤 상황에서 "이건 클래스로 묶는 게 낫겠다" 싶고, 반대로 어떤 상황에서 "이건 dict 로 충분하다" 싶은지, 그 경계를 여러분의 기준으로 정리해 보세요.

2. self 는 정확히 무엇일까?

오늘 메서드의 첫 인자로 늘 self 를 적었고, 부를 땐 신경 쓰지 않아도 된다고 했어요. m.add_follower() 라고 부르면 selfm 이 자동으로 들어간다고도 했죠.

그렇다면 self 는 왜 꼭 필요할까요? 만약 메서드에 self 가 없다면, 같은 add_follower 메서드가 어떤 인스턴스의 팔로워를 올려야 하는지 어떻게 알 수 있을까요? "메서드 하나를 모든 인스턴스가 공유하는데, 그 동작이 인스턴스마다 다른 대상에 적용되는" 구조를 self 가 어떻게 가능하게 하는지 정리해 보세요.

3. __str____repr__, 왜 둘 다 둘까?

오늘 우리는 사람용 표현(__str__)과 개발자용 표현(__repr__)을 따로 만들었어요. 둘이 같은 내용을 돌려줘도 동작은 해요.

그런데도 파이썬이 굳이 이 둘을 나눠 둔 이유는 뭘까요? 어떤 객체를 화면에 사용자에게 보여줄 때와, 개발자가 디버깅하며 리스트에 잔뜩 담긴 객체들을 살펴볼 때 — 각각 어떤 정보가 더 유용한지 떠올려 보면 힌트가 보일 거예요. 두 표현을 나누면 무엇이 좋은지 여러분의 생각을 정리해 보세요.

✅ 예시 답안정답 보기

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


🎯 [과제 1 예시답안] 나만의 Member 클래스 만들기

채점 포인트

항목 확인 핵심
클래스 정의 class Member: 첫 글자 대문자
__init__ def __init__(self, username, bio): 태어날 때 두 속성을 받음
속성 채우기 self.username = username / self.bio = bio self. 로 인스턴스에 박기
메서드 def introduce(self): → f-string 반환 self 로 자기 속성 읽기
인스턴스 독립 두 인스턴스의 bio 가 따로 유지 각자 자기 데이터

풀이 예시

Python
# src/instagram/d1_member_intro_solution.py
"""나만의 Member 클래스 (D-1 기초 과제)."""


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

    def introduce(self):
        return f"@{self.username} — {self.bio}"


a = Member("jaehoon", "여행과 사진을 좋아해요")
b = Member("minji", "맛집 탐방러")

print(a.introduce())
print(b.introduce())

실행 결과예요.

텍스트
@jaehoon — 여행과 사진을 좋아해요
@minji — 맛집 탐방러

💡 튜터의 한마디: __init__ 에서 usernamebio 두 가지를 받아 self. 로 담아둔 게 핵심이에요. introduce() 메서드는 self.username·self.bio 로 자기 속성을 읽어 문장을 만들고요. abbio 가 서로 다른 값으로 잘 유지되는 건, 인스턴스마다 자기만의 속성을 따로 가지기 때문이에요. a 를 만들 때 selfa 를, b 를 만들 때 selfb 를 가리켰으니 서로 섞이지 않죠.

🎯 [과제 2 예시답안] 게시물을 클래스로 묶기

채점 포인트

항목 확인 핵심
클래스 정의 class Post: 게시물 설계도
기본값 속성 self.likes = 0 새 게시물은 좋아요 0 부터
메서드 def add_like(self):self.likes + 1 자기 속성을 고침
__str__ 제목 (좋아요 N개) 반환 print 용 사람 표현
데이터+행동 title·likes 와 add_like 가 한 클래스에 한 몸으로 묶기

풀이 예시

Python
# src/instagram/d1_post_solution.py
"""게시물을 클래스로 묶기 (D-1 응용 과제)."""


class Post:
    def __init__(self, title):
        self.title = title
        self.likes = 0

    def add_like(self):
        self.likes = self.likes + 1

    def __str__(self):
        return f"{self.title} (좋아요 {self.likes}개)"


post = Post("한강 야경")
post.add_like()
post.add_like()
post.add_like()

print(post)

실행 결과예요.

텍스트
한강 야경 (좋아요 3개)

💡 튜터의 한마디: 지난 시간엔 게시물을 dict 로 두고 like(post) 처럼 함수에 매번 넘겨줬죠. 이번엔 title·likes 데이터와 add_like 행동이 Post 안에 한 몸으로 들어있어요. post.add_like() 라고만 하면 "이 게시물의 좋아요" 가 올라가니, 어떤 데이터를 다루는 건지 헷갈릴 일이 없어요. print(post) 가 깔끔하게 나오는 건 __str__ 을 만들어 둔 덕분이고요. 데이터와 행동을 묶으면 코드가 "게시물 중심" 으로 읽혀서 한결 자연스러워집니다.

🎯 [과제 3 예시답안] 클래스 변수로 게시물 개수 세기

채점 포인트

항목 확인 핵심
클래스 변수 count = 0 (클래스 바로 아래) 모든 게시물이 공유
카운터 증가 Post.count = Post.count + 1 클래스 이름으로 콕 집어 올림
__repr__ Post(title='제목') 반환 개발자용 표현
두 표현 구분 print(p1) vs print(repr(p2)) 사람용 vs 개발자용
공유 확인 Post.count 가 3 인스턴스 수만큼 누적

풀이 예시

Python
# src/instagram/d1_post_count_solution.py
"""클래스 변수로 게시물 개수 세기 (D-1 심화 과제)."""


class Post:
    count = 0   # 지금까지 만든 게시물 수 (모든 게시물이 공유)

    def __init__(self, title):
        self.title = title
        self.likes = 0
        Post.count = Post.count + 1

    def __str__(self):
        return f"{self.title} (좋아요 {self.likes}개)"

    def __repr__(self):
        return f"Post(title='{self.title}')"


p1 = Post("한강 야경")
p2 = Post("제주 바다")
p3 = Post("벚꽃 산책")

print(p1)
print(repr(p2))
print("게시물 총 개수:", Post.count)

실행 결과예요.

텍스트
한강 야경 (좋아요 0개)
Post(title='제주 바다')
게시물 총 개수: 3

💡 튜터의 한마디: count 를 클래스 바로 아래 적어 클래스 변수로 두고, __init__ 에서 게시물이 하나 생길 때마다 Post.count 를 1 올렸어요. 그래서 게시물 셋을 만들었더니 Post.count 가 3 이 됐죠. 만약 도전 과제처럼 self.count = self.count + 1 로 바꿔 실행하면, 각 게시물마다 자기만의 count 가 1 로 생겨버려서 전체 개수가 제대로 안 세져요(공유 카운터가 안 올라가요). "모두가 공유하는 값을 바꿀 땐 클래스 이름(Post.count)으로" 라는 걸 직접 실험해 보면 확실히 와닿을 거예요. print(p1)__str__, print(repr(p2))__repr__ 이 쓰여 표현이 다르게 나오는 것도 확인해 두세요.


🤔 [생각해볼 주제 1] 언제 클래스를 쓰고, 언제 함수와 dict 로 충분할까?

문제 상황 요약

오늘 dict+함수로 다루던 게시물을 클래스로 묶었어요. 클래스는 데이터와 행동을 한곳에 모아주지만, 모든 걸 클래스로 만들 필요는 없어요. 어떤 때 클래스가 낫고, 어떤 때 dict 로 충분할까요?

튜터의 가이드 및 해설

기준은 "데이터에 붙어 다니는 행동(동작)이 있느냐, 그리고 그 대상을 여러 개 만들고 오래 다루느냐" 예요.

클래스가 어울리는 경우

  • 데이터와 그걸 다루는 행동이 늘 함께 다닐 때 — 회원에게 "팔로우한다", 게시물에 "좋아요를 누른다" 처럼 동작이 데이터에 딱 붙어 있으면 묶는 게 자연스러워요.
  • 같은 모양의 대상을 여러 개 찍어낼 때 — 회원 100 명, 게시물 1000 개처럼 같은 구조를 반복해 만들면 설계도(클래스) 하나로 일관되게 찍어내는 게 안전해요.
  • 그 대상이 프로그램 안에서 오래 살아남아 여러 곳에서 다뤄질 때.

dict 로 충분한 경우

  • 값 몇 개를 잠깐 묶어서 넘기기만 할 때 — "이름과 나이를 한 번 전달" 같은 가벼운 묶음은 dict 가 더 간단해요.
  • 행동(메서드) 없이 데이터만 들고 있을 때.
  • 구조가 자주 바뀌거나 임시로 쓰고 버릴 데이터일 때.

한마디로, "데이터 + 그 데이터만의 행동" 이 한 덩어리로 반복되면 클래스, "값의 임시 묶음" 이면 dict 가 어울려요. 처음부터 완벽히 가를 필요는 없어요. dict 로 시작했다가 행동이 자꾸 붙기 시작하면 그때 클래스로 옮겨도 늦지 않아요.

💡 핵심을 한마디로

데이터에 행동이 딱 붙어 다니고 같은 모양을 여러 개 만든다면 클래스, 값 몇 개를 잠깐 묶어 넘기는 정도라면 dict — "행동이 있는가" 가 가장 큰 갈림길이에요.

🤔 [생각해볼 주제 2] self 는 정확히 무엇일까?

문제 상황 요약

메서드의 첫 인자로 늘 self 를 적었고, 부를 땐 신경 쓰지 않아도 된다고 했어요. m.add_follower() 라고 부르면 selfm 이 자동으로 들어가고요. 그렇다면 self 는 왜 꼭 필요할까요?

튜터의 가이드 및 해설

핵심은 "메서드는 모든 인스턴스가 공유하지만, 그 동작은 인스턴스마다 다른 대상에 적용된다" 는 데 있어요.

add_follower 메서드는 Member 클래스에 딱 하나만 정의돼요. 회원이 100 명이어도 메서드 코드는 한 벌이죠. 그런데 jaehoon.add_follower() 는 재훈이의 팔로워를, minji.add_follower() 는 민지의 팔로워를 올려야 해요. 같은 코드가 부를 때마다 다른 대상을 고쳐야 하는 거예요.

이걸 가능하게 하는 게 self 예요. jaehoon.add_follower() 라고 부르면 파이썬이 self 자리에 jaehoon 을 넣어줘요. 그럼 메서드 안의 self.followers 가 곧 jaehoon.followers 가 되죠. minji.add_follower()selfminji 라서 minji.followers 가 되고요.

텍스트
   jaehoon.add_follower()      self = jaehoon      jaehoon.followers + 1
   minji.add_follower()        self = minji        minji.followers + 1
        └ 같은 메서드 한 벌, 부른 인스턴스마다 다른 대상에 적용 ┘

만약 self 가 없다면, 메서드는 "누구의 팔로워를 올려야 할지" 를 알 방법이 없어요. 그래서 메서드의 첫 인자는 항상 self — "이 동작을 적용할 바로 그 인스턴스" 를 받는 거예요.

💡 핵심을 한마디로

메서드는 클래스에 한 벌만 있지만 self 가 "지금 이 메서드를 부른 인스턴스" 를 받아주기에, 같은 코드가 부른 대상마다 다르게 동작할 수 있어요.

🤔 [생각해볼 주제 3] strrepr, 왜 둘 다 둘까?

문제 상황 요약

사람용 표현(__str__)과 개발자용 표현(__repr__)을 따로 만들었어요. 둘이 같은 내용을 돌려줘도 동작은 하는데, 파이썬은 왜 굳이 이 둘을 나눠 뒀을까요?

튜터의 가이드 및 해설

목적이 다른 두 독자를 위한 거예요. 같은 객체라도 "사용자에게 보여줄 때" 와 "개발자가 들여다볼 때" 필요한 정보가 다르거든요.

__str__ — 사용자에게 보여주는 표현

사용자는 속사정을 알 필요가 없어요. @jaehoon (팔로워 1240명) 처럼 읽기 좋고 깔끔하면 충분하죠. 그래서 print 나 화면 출력에는 __str__ 이 쓰여요.

__repr__ — 개발자가 디버깅하는 표현

개발자는 "이 객체가 정확히 어떤 값으로 이뤄졌나" 를 알고 싶어요. 그래서 Member(username='jaehoon', followers=1240) 처럼 속을 그대로 드러내는 게 유용해요. 특히 리스트에 객체가 잔뜩 담겨 있을 때 출력하면 __repr__ 이 쓰이는데, 각 요소의 정체를 정확히 봐야 디버깅이 되니까요.

텍스트
   사용자 화면       __str__     "@jaehoon (팔로워 1240명)"     (예쁘게)
   개발자 디버깅     __repr__    "Member(username='jaehoon', ...)"  (정확하게)

둘을 나눠두면, 사용자에게는 깔끔한 표현을, 개발자에게는 정확한 표현을 동시에 줄 수 있어요. 만약 하나만 만든다면 __repr__ 을 권해요. __str__ 이 없을 때 print__repr__ 을 대신 쓰거든요. 즉 __repr__ 만 있어도 최소한의 표현은 보장돼요.

💡 핵심을 한마디로

__str__ 은 사용자에게 보여줄 깔끔한 표현, __repr__ 은 개발자가 디버깅할 때 객체의 정체를 정확히 드러내는 표현 — 독자가 다르기에 둘을 나눠 둬요.

전체 목록 파이썬 기초