D-2: 상속과 다형성
목차 25
안녕하세요, 홍순구입니다. 지난 시간엔 Member 클래스를 만들면서 "데이터와 행동을 하나로 묶는" 클래스의 세계로 첫발을 디뎠어요. __init__ 으로 속성을 채우고, self 로 인스턴스 자신을 가리키고, __str__·__repr__ 으로 객체를 보기 좋게 표현하는 것까지 해봤죠.
그런데 인스타그램을 떠올려 볼까요? 게시물엔 사진도 있고 동영상도 있어요. 둘 다 "게시물" 이라는 공통점이 있지만, 사진은 필터가 있고 동영상은 재생 길이가 있죠. 이걸 클래스로 만들면, 공통된 부분(작성자·캡션·좋아요)을 사진용·동영상용에 똑같이 두 번 써야 할까요?
오늘 배울 상속(inheritance)이 바로 이 "중복" 문제를 풀어줘요. 공통은 부모가 한 번만 갖고, 자식은 다른 점만 새로 쓰는 거예요. 거기에 같은 명령 한 마디에 종류마다 다르게 반응하는 다형성(polymorphism)까지 만나봅니다.
[지난 시간] Member 클래스 하나 — __init__ · self · __str__/__repr__
[오늘 D-2] Post (부모: 공통을 한 번만)
│ 물려받기 = 상속
├── PhotoPost (자식: 사진 — 필터)
└── VideoPost (자식: 동영상 — 길이)
└ 같은 describe() 한 마디, 다른 결과 = 다형성
Advertisement (부모 없는 남남, 그래도 describe() 만 있으면 합류 = 덕 타이핑)
💡 오늘 수업의 핵심 — "공통은 부모가 한 번만, 다른 점만 자식이"
상속은 공통 코드를 부모 클래스에 한 번만 써두고 자식이 물려받는 거예요. 자식은 다른 점만 새로 쓰면 되죠. 그리고 같은 describe() 한 마디에 사진은 사진답게, 동영상은 동영상답게 반응하는 게 다형성이에요. 오늘은 이 두 가지를 인스타그램 게시물로 직접 만들어 봅니다.
🎯 학습 목표
- 공통 코드를 부모 클래스에 한 번만 쓰고 자식이 물려받는 상속을 이해하고 직접 만든다.
- 자식이 부모 메서드를 다시 쓰는 오버라이딩과, 부모 것을 불러오는
super()를 구분해 쓴다. - 같은 이름이 종류에 따라 다르게 동작하는 다형성과, 타입을 따지지 않는 덕 타이핑을 안다.
Step 1: "똑같은 코드를 또 쓴다고?" — 상속의 출발
먼저 상속을 모르는 상태로 사진 게시물과 동영상 게시물을 만들어 볼게요. 둘 다 작성자(author)·캡션(caption)·좋아요(likes)를 갖고, 좋아요를 누르는 add_like() 가 필요하죠.
# src/instagram/d2_before.py
class PhotoPost:
def __init__(self, author, caption, filter_name):
self.author = author
self.caption = caption
self.likes = 0
self.filter_name = filter_name
def add_like(self):
self.likes = self.likes + 1
class VideoPost:
def __init__(self, author, caption, duration):
self.author = author # 또 똑같이
self.caption = caption # 또 똑같이
self.likes = 0 # 또 똑같이
self.duration = duration
def add_like(self): # add_like 도 똑같이 한 번 더
self.likes = self.likes + 1
보세요. author·caption·likes 채우는 코드와 add_like 가 두 클래스에 똑같이 들어갔어요. 게시물 종류가 댓글·스토리까지 늘어나면, 좋아요 규칙 하나 바꿀 때마다 모든 클래스를 다 고쳐야 해요. 실수하기 딱 좋죠.
이럴 때 상속을 써요. 공통된 부분을 부모 클래스(parent class) 하나에 한 번만 쓰고, 사진·동영상은 그걸 물려받으면 돼요. 물려받는 쪽을 자식 클래스(child class) 라고 불러요.
# src/instagram/d2_inherit.py
class Post:
def __init__(self, author, caption):
self.author = author
self.caption = caption
self.likes = 0
def add_like(self):
self.likes = self.likes + 1
def describe(self):
return f"@{self.author}: {self.caption} (좋아요 {self.likes})"
class PhotoPost(Post): # 괄호 안에 부모 이름 = "Post 를 물려받는다"
pass # 아직 새로 쓸 게 없어요. 부모 것을 그대로 사용
핵심은 class PhotoPost(Post): 한 줄이에요. 클래스 이름 뒤 괄호 안에 부모 이름을 적으면 "나는 Post 를 물려받는다" 는 뜻이에요. 몸통엔 아직 쓸 게 없어서 pass(아무것도 안 함)만 뒀어요. 그런데도 PhotoPost 는 Post 의 모든 기능을 그대로 쓸 수 있어요.
photo = PhotoPost("jaehoon", "오늘의 노을")
photo.add_like() # 부모의 add_like 를 그대로 사용
print(photo.describe()) # 부모의 describe 를 그대로 사용
print(isinstance(photo, PhotoPost))
print(isinstance(photo, Post)) # 자식은 부모 타입이기도 해요
실행하면 이렇게 나와요.
@jaehoon: 오늘의 노을 (좋아요 1)
True
True
PhotoPost 에는 add_like 도 describe 도 안 적었는데 잘 동작하죠? 전부 부모 Post 에서 물려받았으니까요. 마지막 두 줄도 재미있어요. isinstance(photo, Post)(photo 가 Post 의 인스턴스냐?)가 True 예요. 자식은 부모 타입이기도 하다 — 사진 게시물은 동시에 "게시물" 이기도 한 거죠.
붕어빵으로 비유하면, Post 는 기본 붕어빵 틀이고 PhotoPost 는 그 틀을 그대로 가져와 모양만 살짝 바꾼 특별 틀이에요. 기본 틀의 기능은 다 들어 있고요.
💡 한 줄 정리
class 자식(부모): 으로 부모의 속성·메서드를 통째로 물려받아요. 공통은 부모에 한 번만 쓰면, 자식은 그걸 거저 쓸 수 있어요.
🙋 학생 질문 — "부모 클래스 Post 는 따로 실행하거나 등록 안 해도 되나요?"
네, 따로 할 게 없어요. class Post: 로 정의해 두고, class PhotoPost(Post): 처럼 괄호 안에 이름만 적으면 파이썬이 알아서 연결해 줘요. 단 한 가지, 부모 클래스가 자식보다 먼저 정의되어 있어야 해요. 파이썬은 위에서 아래로 코드를 읽으니까, 자식이 부모를 가리키는 순간 부모가 이미 만들어져 있어야 하거든요. 보통 같은 파일 위쪽에 부모를, 아래쪽에 자식을 두면 자연스럽게 해결돼요.
Step 2: "물려받은 메서드를 내 식대로" — 오버라이딩
물려받기만 하면 자식은 부모와 똑같이 행동해요. 하지만 사진 게시물은 "사진" 답게 자기소개를 하고 싶을 수 있죠. 이럴 때 자식이 부모와 같은 이름 의 메서드를 다시 정의하면, 자식 것이 부모 것을 덮어써요. 이걸 오버라이딩(overriding, 재정의) 이라고 해요.
# src/instagram/d2_override.py
class Post:
def __init__(self, author, caption):
self.author = author
self.caption = caption
def describe(self):
return f"@{self.author}: {self.caption}"
class PhotoPost(Post):
def describe(self): # 부모와 같은 이름 → 오버라이딩
return f"[사진] @{self.author} 님이 올린 사진"
PhotoPost 가 describe 를 다시 정의했어요. 부모에도 describe 가 있지만, 자식에도 같은 이름이 있으면 자식 것이 이겨요. 한번 비교해 볼까요?
post = Post("yuna", "안녕하세요")
photo = PhotoPost("jaehoon", "노을 사진")
print(post.describe()) # 부모는 그대로
print(photo.describe()) # 자식은 새로 쓴 버전
@yuna: 안녕하세요
[사진] @jaehoon 님이 올린 사진
같은 describe() 인데 결과가 다르죠? post(부모 인스턴스)는 부모 버전을, photo(자식 인스턴스)는 자식이 새로 쓴 버전을 써요. 중요한 건 부모는 전혀 바뀌지 않았다 는 거예요. 자식이 자기 것만 새로 썼을 뿐, 부모 Post 의 describe 는 멀쩡해요.
물려받은 레시피를 그대로 쓰다가, 내 입맛에 맞게 한 가지만 고쳐 쓰는 것과 같아요. 원래 레시피(부모)는 그대로 두고, 내 버전(자식)만 따로 갖는 거죠.
💡 한 줄 정리
자식이 부모와 같은 이름의 메서드를 다시 쓰면(오버라이딩) 자식 것이 이겨요. 부모 메서드는 그대로 남아 있어요.
🙋 학생 질문 — "오버라이딩하면 부모의 원래 메서드는 영영 못 쓰나요?"
좋은 질문이에요. 오버라이딩하면 자식 인스턴스에서는 기본적으로 자식 버전이 불려요. 하지만 부모 버전이 사라지는 건 아니에요. 자식 안에서 "부모의 원래 동작도 같이 쓰고 싶다" 면 부모를 콕 집어 부르는 방법이 있어요. 바로 다음 Step 에서 배울 super() 예요. 지금은 "오버라이딩해도 부모 것이 어딘가에 그대로 있다" 정도만 기억하면 충분해요.
Step 3: "부모 것도 살리고 싶다" — super()
오버라이딩에는 함정이 하나 있어요. 자식이 __init__ 을 다시 쓰면, 부모의 __init__ 이 하던 일(작성자·캡션·좋아요 채우기)이 통째로 사라져요. 그럼 사진 게시물에 필터만 있고 정작 작성자가 없는 이상한 일이 생기죠.
이때 부모의 메서드를 콕 집어 부르는 게 super() 예요. super() 는 "내 부모" 를 가리키는 통로라고 보면 돼요.
# src/instagram/d2_super.py
class Post:
def __init__(self, author, caption):
self.author = author
self.caption = caption
self.likes = 0
def describe(self):
return f"@{self.author}: {self.caption} (좋아요 {self.likes})"
class PhotoPost(Post):
def __init__(self, author, caption, filter_name):
super().__init__(author, caption) # 부모 __init__ 이 author·caption·likes 를 채워줌
self.filter_name = filter_name # 자식만의 속성을 더함
def describe(self):
base = super().describe() # 부모의 설명을 먼저 받아서
return f"{base} · 필터 {self.filter_name}" # 자식이 덧붙임
두 군데에서 super() 를 썼어요. 하나씩 볼게요.
__init__ 안의 super().__init__(author, caption) 은 "부모야, 네 __init__ 으로 작성자·캡션·좋아요를 먼저 채워줘" 라는 부탁이에요. 그러고 나서 자식은 self.filter_name 만 더하면 끝이죠. 공통 초기화를 부모에게 맡기니, 중복이 사라져요.
describe 안의 super().describe() 는 "부모가 만든 기본 설명을 먼저 받아오는" 거예요. 그 결과를 base 에 담고, 뒤에 · 필터 ... 를 덧붙였어요. 부모 것을 버리지 않고 이어서 확장 하는 거죠.
photo = PhotoPost("jaehoon", "오늘의 노을", "Clarendon")
print(photo.author) # 부모 __init__ 이 채운 속성
print(photo.filter_name) # 자식이 더한 속성
print(photo.describe())
jaehoon
Clarendon
@jaehoon: 오늘의 노을 (좋아요 0) · 필터 Clarendon
photo.author 가 잘 채워졌죠? 부모 __init__ 이 super() 덕분에 제대로 불렸으니까요. 그리고 describe() 결과를 보면 부모가 만든 @jaehoon: 오늘의 노을 (좋아요 0) 뒤에 자식이 · 필터 Clarendon 을 덧붙인 게 보여요.
부모님께 전화해서 "기본 준비는 부탁드릴게요" 하고, 나는 내 몫만 더하는 그림이에요. 오버라이딩(Step 2)이 "부모 것을 덮어쓰기" 였다면, super() 는 "부모 것을 불러와서 이어 쓰기" 예요. 이 둘을 함께 쓰면 강력하죠.
💡 한 줄 정리
super() 는 부모를 가리키는 통로예요. super().__init__(...) 으로 부모 초기화를 재사용하고, super().메서드() 로 부모 결과에 자식이 덧붙여요.
🙋 학생 질문 — "super().init 에는 왜 self 를 안 넣나요?"
지난 시간에 메서드를 부를 땐 self 를 신경 쓰지 않아도 된다고 했죠? super() 도 마찬가지예요. super() 는 이미 "지금 이 인스턴스(self)의 부모" 를 알고 있어서, self 를 자동으로 챙겨요. 그래서 우리는 super().__init__(author, caption) 처럼 진짜 넘기고 싶은 값만 적으면 돼요. self 를 직접 쓰는 건 오히려 어색하고, 거의 그럴 일이 없어요.
Step 4: "같은 이름, 다른 동작" — 다형성
이제 진짜 재미있는 부분이에요. 사진·동영상 게시물을 한 리스트에 섞어 담고, 종류를 따지지 않고 똑같이 describe() 만 불러볼게요.
# src/instagram/d2_polymorphism.py
class Post:
def __init__(self, author, caption):
self.author = author
self.caption = caption
def describe(self):
return f"@{self.author}: {self.caption}"
class PhotoPost(Post):
def describe(self):
return f"[사진] {super().describe()}"
class VideoPost(Post):
def describe(self):
return f"[동영상] {super().describe()}"
feed = [
PhotoPost("jaehoon", "노을"),
VideoPost("minji", "브이로그"),
Post("yuna", "안녕"),
]
for item in feed: # 종류를 따지지 않고
print(item.describe()) # 똑같이 describe() 만 불러요
[사진] @jaehoon: 노을
[동영상] @minji: 브이로그
@yuna: 안녕
for 루프 안을 보세요. item 이 사진이든 동영상이든 일반 게시물이든, 우리는 그냥 item.describe() 한 줄만 불러요. 그런데 결과는 사진은 [사진], 동영상은 [동영상] 으로 종류마다 알아서 다르게 나오죠.
이렇게 같은 이름의 호출(describe())이 인스턴스 종류에 따라 다르게 동작하는 것 을 다형성(polymorphism)이라고 해요. "여러(poly) 모습(morph)" 이라는 뜻이에요. 우리는 종류를 일일이 확인하지 않았는데, 파이썬이 각 인스턴스에 맞는 describe 를 알아서 골라 불러줬어요.
비유하면 이래요. 동물들에게 "소리 내!" 하고 한 마디 외치면, 강아지는 멍멍, 고양이는 야옹, 소는 음매 하고 각자 답하죠. 명령은 하나인데 반응은 종류마다 달라요. 그게 다형성이에요.
💡 한 줄 정리
여러 종류를 한데 모아 같은 메서드를 불러도, 인스턴스 종류에 맞는 동작이 자동으로 골라져요. 그게 다형성이에요.
🙋 학생 질문 — "그냥 if 로 종류를 확인해서 분기하면 안 되나요?"
물론 if 사진이면 ... elif 동영상이면 ... 식으로 짤 수도 있어요. 하지만 게시물 종류가 늘 때마다 그 if 묶음을 찾아 일일이 고쳐야 해요. 종류가 열 개면 if 도 열 갈래가 되고, 빠뜨리면 버그가 되죠. 다형성을 쓰면 새 종류를 추가할 때 그 클래스에 describe 만 새로 써주면 끝이에요. for item in feed: item.describe() 같은 호출 코드는 한 글자도 안 바뀌어요. 종류가 늘어도 호출하는 쪽이 깔끔하게 유지되는 게 다형성의 큰 장점이에요.
Step 5: "꼭 상속해야 할까?" — 덕 타이핑
지금까지는 Post 를 부모로 둔 자식들만 다뤘어요. 그런데 파이썬에는 더 느슨하고 재미있는 규칙이 있어요. 피드에 광고를 끼워 넣는다고 해볼게요. 광고는 게시물이 아니라서 Post 를 상속하지 않아요. 완전히 남남이죠. 그래도 describe() 메서드만 갖고 있으면 어떻게 될까요?
# src/instagram/d2_duck.py
class PhotoPost:
def __init__(self, author):
self.author = author
def describe(self):
return f"[사진] @{self.author}"
class Advertisement: # Post 를 상속하지 않아요. 완전히 남남.
def __init__(self, sponsor):
self.sponsor = sponsor
def describe(self): # 하지만 describe() 가 있죠
return f"[광고] {self.sponsor}"
feed = [PhotoPost("jaehoon"), Advertisement("나이키")]
for item in feed: # 가족(상속) 관계가 아니어도
print(item.describe()) # describe() 만 있으면 같은 줄에서 동작
[사진] @jaehoon
[광고] 나이키
Advertisement 는 PhotoPost 와 아무 상속 관계가 없어요. 부모를 공유하지도 않죠. 그런데도 같은 for 루프에서 나란히 describe() 가 불려요. 파이썬은 "이게 무슨 타입이냐, 어느 부모를 물려받았냐" 를 따지지 않고, 그저 "describe() 를 갖고 있느냐" 만 봐요.
이걸 덕 타이핑(duck typing) 이라고 해요. "오리처럼 걷고 오리처럼 운다면, 그건 오리로 취급하자" 는 말에서 나온 이름이에요. 어떤 종(타입)인지 족보를 따지지 않고, 필요한 행동만 할 줄 알면 받아들이는 거죠.
그럼 상속은 왜 배웠을까요? 상속은 공통 코드를 한 곳에 모아 중복을 없애줘요(Step 1~3). 덕 타이핑은 상속 없이도 같은 자리에서 함께 동작하게 해주는 파이썬의 유연함이고요. 보통은 진짜 공통점이 많을 때 상속으로 묶고, 그저 같은 동작만 필요할 땐 덕 타이핑에 기대요. 둘은 경쟁 관계가 아니라 상황에 따라 골라 쓰는 도구예요.
💡 한 줄 정리
파이썬은 타입(족보)이 아니라 "그 메서드가 있느냐" 만 봐요. 상속하지 않아도 같은 메서드만 있으면 함께 동작해요. 이게 덕 타이핑이에요.
🙋 학생 질문 — "그럼 덕 타이핑만 쓰면 상속은 필요 없는 거 아닌가요?"
상황이 달라요. 광고와 사진처럼 "딱히 공통 코드는 없는데 같은 동작(describe)만 필요" 하면 덕 타이핑이 가볍고 좋아요. 반대로 사진·동영상 게시물처럼 작성자·캡션·좋아요·add_like 같은 공통 코드가 잔뜩 겹치면, 그걸 부모에 한 번만 써두는 상속이 중복을 확 줄여줘요. 공통 코드가 많으면 상속, 동작만 맞으면 덕 타이핑 — 이렇게 기억하면 편해요.
Step 6: 종합 — 게시물 피드 만들기
배운 걸 한 파일로 모아 인스타그램 피드를 완성해 볼게요. 상속·super()·오버라이딩·다형성·덕 타이핑이 한꺼번에 들어가요. 지난 시간 Member 에서 본 클래스 변수 count 와 __str__·__repr__ 도 다시 등장해요.
# src/instagram/post.py
class Post:
"""모든 게시물의 공통 부모 클래스."""
count = 0 # 지금까지 만든 게시물 수 — 모든 게시물이 공유
def __init__(self, author, caption):
self.author = author
self.caption = caption
self.likes = 0
Post.count = Post.count + 1
def add_like(self):
self.likes = self.likes + 1
def describe(self):
return f"@{self.author}: {self.caption} (좋아요 {self.likes})"
def __str__(self):
return self.describe()
def __repr__(self):
return f"Post(author='{self.author}')"
부모 Post 에 공통을 모았어요. 클래스 변수 count 는 지난 시간 Member.count 와 똑같은 패턴이에요. 게시물이 하나 태어날 때마다 Post.count 를 1 늘려, 모든 게시물이 공유하는 "총 개수" 를 세죠. __str__ 은 describe() 를 그대로 쓰게 해뒀어요. print 하면 사람용 설명이 바로 나오게요.
이제 자식들이에요. 둘 다 super() 로 부모를 재사용하면서 describe 만 오버라이딩해요.
class PhotoPost(Post):
def __init__(self, author, caption, filter_name):
super().__init__(author, caption)
self.filter_name = filter_name
def describe(self):
base = super().describe()
return f"{base} · [사진] 필터 {self.filter_name}"
class VideoPost(Post):
def __init__(self, author, caption, duration):
super().__init__(author, caption)
self.duration = duration
def describe(self):
base = super().describe()
return f"{base} · [동영상] {self.duration}초"
그리고 부모 없는 광고도 describe() 하나만 갖춰 덕 타이핑으로 합류시켜요.
class Advertisement:
def __init__(self, sponsor):
self.sponsor = sponsor
def describe(self):
return f"[광고] {self.sponsor} 와 함께하는 콘텐츠"
이제 피드를 만들어 다형적으로 출력해요. (전체 코드는 src/instagram/post.py 에 있어요.)
feed = [
PhotoPost("jaehoon", "오늘의 노을", "Clarendon"),
VideoPost("minji", "제주 여행 브이로그", 60),
Advertisement("스파르타코딩클럽"),
]
for item in feed:
print(item.describe())
print(f"지금까지 만든 게시물 수: {Post.count}")
@jaehoon: 오늘의 노을 (좋아요 0) · [사진] 필터 Clarendon
@minji: 제주 여행 브이로그 (좋아요 0) · [동영상] 60초
[광고] 스파르타코딩클럽 와 함께하는 콘텐츠
지금까지 만든 게시물 수: 2
세 줄이 종류마다 다르게 나오죠(다형성). 사진·동영상은 super() 덕분에 부모 설명을 이어받았고, 광고는 상속 없이도 덕 타이핑으로 같이 출력됐어요.
마지막 줄을 눈여겨보세요. 게시물을 셋 만들었는데 count 가 2 예요. 광고(Advertisement)는 Post 를 상속하지 않아서 Post.__init__ 을 거치지 않거든요. 그러니 count 도 안 올라가요. "게시물 수" 에 광고가 안 끼는 게 오히려 자연스럽죠. 이런 작은 차이를 직접 눈으로 확인하면 상속의 동작이 또렷하게 이해돼요. 이 동작은 코드베이스 test_d2_post.py 가 여러 경우로 검증해 두었어요.
💡 한 줄 정리
부모에 공통을, 자식에 다른 점만 — super() 로 부모를 재사용하고, 같은 describe() 가 종류별로 다르게 동작해요. 부모 없는 광고는 덕 타이핑으로 합류하고요.
🙋 학생 질문 — "게시물 종류가 더 늘면 이 구조가 감당이 되나요?"
오히려 늘수록 빛을 봐요. 스토리·릴스 같은 새 종류가 생겨도, class 스토리(Post): 로 부모를 물려받고 describe 만 새로 써주면 끝이에요. 피드를 출력하는 for item in feed: item.describe() 코드는 손댈 필요가 없죠. 공통은 부모가 책임지고, 호출하는 쪽은 다형성 덕분에 그대로 — 이게 종류가 많아질수록 강해지는 구조예요.
마무리
오늘은 클래스에 "가족 관계" 를 더했어요. 공통을 부모에 한 번만 써두고 자식이 물려받는 상속, 자식이 부모 메서드를 다시 쓰는 오버라이딩, 부모 것을 불러와 이어 쓰는 super(), 같은 이름이 종류마다 다르게 동작하는 다형성, 그리고 족보를 안 따지는 덕 타이핑까지 — 인스타그램 게시물 피드를 직접 만들며 익혔습니다.
오늘 배운 핵심 세 가지
💡 하나: 상속은 공통 코드를 부모 클래스에 한 번만 써두고 자식이 물려받는 거예요. class 자식(부모): 로 연결하고, 자식은 다른 점만 새로 쓰면 돼요. 자식 인스턴스는 부모 타입이기도 해서 isinstance(자식, 부모) 가 True 예요.
💡 둘: 자식이 부모와 같은 이름의 메서드를 다시 쓰면 자식 것이 이겨요(오버라이딩). 부모 것도 함께 쓰고 싶으면 super() 로 불러와요. super().__init__() 으로 부모 초기화를 재사용하고, super().describe() 로 부모 결과에 덧붙이죠.
💡 셋: 같은 메서드 호출이 인스턴스 종류에 따라 다르게 동작하는 게 다형성이에요. 상속으로 묶지 않아도 같은 메서드만 있으면 함께 동작하는 건 덕 타이핑이고요. 공통 코드가 많으면 상속, 동작만 맞으면 덕 타이핑을 골라 써요.
다음 시간 예고
오늘 만든 Post 들을 좀 더 똑똑하게 만들어 볼 거예요. 지금은 두 게시물이 같은 내용인지 == 로 비교하면 엉뚱한 결과가 나오고, 좋아요순으로 정렬하는 것도 아직 안 돼요. 지난 시간 본 __str__·__repr__ 처럼 밑줄로 감싼 특별한 메서드들 — 매직 메서드(magic method)를 더하면 이게 가능해져요.
다음 시간(D-3)엔 두 객체를 == 로 비교하게 해주는 __eq__, 크기순 정렬을 가능하게 하는 __lt__, 길이를 재는 __len__ 같은 매직 메서드를 배웁니다. 거기에 메서드를 속성처럼 쓰게 해주는 @property 까지요. 오늘 만든 게시물 피드가 한층 더 풍부해질 거예요.
과제
오늘 배운 상속과 다형성을 직접 손으로 만들어 봅시다. 눈으로 읽는 것과 직접 class 를 쳐보는 건 완전히 달라요. 막히면 Step 으로 돌아가 코드를 다시 보세요.
[기초] 댓글 클래스 상속받기
게시물 옆엔 댓글이 있죠. 댓글에도 일반 댓글과 답글이 있어요. 상속으로 묶어 보세요.
- 부모 클래스
Comment를 만들고__init__에서author(작성자)와text(내용)를 받습니다. describe()메서드가@author: text형태의 문자열을 돌려주게 합니다.Comment를 물려받는 자식Reply(답글)를 만들되, 몸통은pass로 둡니다.Reply인스턴스를 하나 만들어describe()를 출력하고,isinstance(답글, Comment)가 무엇인지 확인해 봅니다.- (생각해보기)
Reply에 아무 메서드도 안 썼는데describe()가 동작하는 이유는 무엇일까요?
[응용] 답글에 super() 로 표시 더하기
기초 과제의 Reply 가 답글임을 한눈에 보이게 만들어 보세요.
Reply에describe()를 오버라이딩합니다.- 그 안에서
super().describe()로 부모의 기본 설명을 먼저 받아온 뒤, 앞이나 뒤에↳ (답글)표시를 덧붙입니다. - 일반 댓글 하나와 답글 하나를 만들어 각각
describe()를 출력해, 둘이 다르게 보이는지 확인합니다. - (생각해보기)
super().describe()를 빼고 직접 문자열을 다 새로 쓰면 어떤 점이 불편해질까요?
[심화] 알림 피드를 다형성으로 출력하기
인스타그램 알림엔 좋아요 알림과 댓글 알림이 섞여 있어요. 다형성으로 한 번에 출력해 보세요.
- 부모
Notification을 만들고__init__에서actor(행동한 사람)를 받습니다.describe()는actor 님의 알림정도의 기본 문장을 돌려줍니다. - 자식
LikeNotification과CommentNotification을 만들어 각각describe()를 오버라이딩합니다. (예:actor 님이 좋아요를 눌렀어요/actor 님이 댓글을 남겼어요) - 세 종류를 한 리스트에 섞어 담고
for로 순회하며describe()를 출력합니다. - (도전) 부모
Notification을 상속하지 않는 클래스SystemNotice(시스템 공지)를 만들되describe()만 갖추고, 같은 리스트에 넣어 함께 출력되는지 확인해 보세요. 이게 무슨 개념이었죠?
생각해볼 주제
정답이 하나로 정해진 질문이 아니에요. 혼자 곰곰이 생각해 보거나, 함께 공부하는 동료와 이야기 나눠 보면 좋습니다.
1. 상속으로 묶을까, 그냥 따로 둘까?
오늘 우리는 사진·동영상 게시물의 공통점을 부모 Post 로 묶었어요. 공통 코드가 많을 땐 상속이 중복을 확 줄여주죠.
하지만 모든 걸 상속으로 묶는 게 항상 좋은 건 아니에요. 공통점이 거의 없는데 억지로 부모-자식으로 엮으면, 부모를 한 번 고칠 때 엉뚱한 자식까지 영향을 받아 더 복잡해지기도 해요. 어떤 경우에 "이건 상속으로 묶는 게 낫다" 싶고, 어떤 경우에 "차라리 따로 두는 게 낫다" 싶은지, 여러분의 기준을 정리해 보세요.
2. 다형성은 코드를 어떻게 편하게 만들까?
오늘 피드를 출력할 때 for item in feed: item.describe() 한 줄로 모든 종류를 처리했어요. 종류를 if 로 일일이 확인하지 않았죠.
만약 다형성 없이 종류마다 if 로 분기했다면, 새 게시물 종류(스토리·릴스)가 생길 때마다 어떤 일이 벌어질까요? 반대로 다형성을 쓰면 새 종류를 추가할 때 무엇만 하면 될까요? "호출하는 쪽 코드가 바뀌지 않는다" 는 게 왜 중요한 장점인지, 여러분의 말로 설명해 보세요.
3. 덕 타이핑은 자유로울까, 위험할까?
파이썬은 타입을 따지지 않고 "그 메서드가 있느냐" 만 봐요. 덕분에 광고처럼 상속 관계가 없는 객체도 같은 피드에 쉽게 끼워 넣을 수 있었죠.
그런데 만약 어떤 객체에 describe() 가 없는데 실수로 피드에 넣으면 어떻게 될까요? 상속이 "이건 분명 게시물이다" 라고 미리 보증해 주는 것과 비교하면, 덕 타이핑의 자유로움에는 어떤 책임이 따를까요? 자유와 안전 사이에서 덕 타이핑을 어떻게 다루면 좋을지 생각해 보세요.
✅ 예시 답안정답 보기
과제와 생각해볼 주제의 예시답안이에요. 먼저 스스로 충분히 클래스를 직접 짜본 뒤에 펼쳐 보세요. 정답을 외우는 게 아니라, "내가 만든 클래스와 어디가 다른지" 를 비교하는 게 진짜 공부예요.
🎯 [과제 1 예시답안] 댓글 클래스 상속받기
채점 포인트
| 항목 | 확인 | 핵심 |
|---|---|---|
| 부모 클래스 | class Comment: |
공통 author·text 를 한 곳에 |
| 자식 상속 | class Reply(Comment): |
괄호 안에 부모 이름 |
| 물려받기 | Reply 몸통은 pass |
describe 안 써도 부모 것이 동작 |
| 타입 관계 | isinstance(reply, Comment) → True |
자식은 부모 타입이기도 함 |
풀이 예시
# src/instagram/d2_comment_solution.py
"""댓글 클래스 상속받기 (D-2 기초 과제)."""
class Comment:
def __init__(self, author, text):
self.author = author
self.text = text
def describe(self):
return f"@{self.author}: {self.text}"
class Reply(Comment): # Comment 를 물려받는 답글
pass # 아직 새로 쓸 게 없어요. 부모 것을 그대로 사용
reply = Reply("minji", "저도 가보고 싶어요!")
print(reply.describe()) # 부모 describe 를 그대로 사용
print(isinstance(reply, Comment)) # 답글은 댓글이기도 하다
실행 결과예요.
@minji: 저도 가보고 싶어요!
True
💡 튜터의 한마디: 핵심은 class Reply(Comment): 한 줄이에요. 괄호 안에 Comment 를 적는 순간 Reply 는 부모의 __init__ 과 describe 를 통째로 물려받아요. 그래서 Reply 몸통에 pass 만 뒀는데도 reply.describe() 가 잘 동작하죠. __init__ 도 따로 안 썼지만 부모 것이 author·text 를 채워줬고요. 마지막 줄의 isinstance(reply, Comment) 가 True 인 건, 답글이 곧 댓글의 한 종류이기 때문이에요. 자식은 언제나 부모 타입이기도 하답니다.
🎯 [과제 2 예시답안] 답글에 super() 로 표시 더하기
채점 포인트
| 항목 | 확인 | 핵심 |
|---|---|---|
| 오버라이딩 | Reply 에 describe() 재정의 |
부모와 같은 이름 다시 쓰기 |
super() |
base = super().describe() |
부모의 기본 설명을 받아옴 |
| 확장 | base 앞에 ↳ (답글) 덧붙임 |
덮어쓰지 않고 이어 쓰기 |
| 비교 | 댓글과 답글 출력이 다름 | 부모는 그대로, 자식만 바뀜 |
풀이 예시
# src/instagram/d2_reply_solution.py
"""답글에 super() 로 표시 더하기 (D-2 응용 과제)."""
class Comment:
def __init__(self, author, text):
self.author = author
self.text = text
def describe(self):
return f"@{self.author}: {self.text}"
class Reply(Comment):
def describe(self):
base = super().describe() # 부모의 기본 설명을 먼저 받아서
return f"↳ (답글) {base}" # 앞에 답글 표시를 덧붙임
comment = Comment("jaehoon", "노을 정말 예쁘네요")
reply = Reply("minji", "저도 가보고 싶어요!")
print(comment.describe())
print(reply.describe())
실행 결과예요.
@jaehoon: 노을 정말 예쁘네요
↳ (답글) @minji: 저도 가보고 싶어요!
💡 튜터의 한마디: 이번엔 Reply 가 describe 를 오버라이딩했어요. 하지만 부모의 설명을 통째로 버리지 않고 super().describe() 로 받아온 게 핵심이에요. 그 결과를 base 에 담고 앞에 ↳ (답글) 만 붙였죠. 만약 도전 과제처럼 super() 를 안 쓰고 f"↳ (답글) @{self.author}: {self.text}" 라고 직접 다 쓰면 당장은 똑같이 동작해요. 하지만 나중에 부모 Comment.describe 의 형식이 바뀌면(예: 작성 시간 추가) 답글은 그 변화를 못 따라가요. super() 로 부모 것을 받아 쓰면 부모가 바뀔 때 자식도 자동으로 따라가니, 중복도 줄고 일관성도 지켜져요.
🎯 [과제 3 예시답안] 알림 피드를 다형성으로 출력하기
채점 포인트
| 항목 | 확인 | 핵심 |
|---|---|---|
| 부모 클래스 | class Notification: → actor |
공통 행동자 |
| 자식 오버라이딩 | Like/Comment 가 describe 재정의 |
종류별 다른 메시지 |
| 다형성 | 한 리스트 for 순회 + describe() |
같은 호출, 다른 결과 |
| 덕 타이핑(도전) | SystemNotice 는 비상속 + describe() |
족보 없이 같은 줄에 합류 |
풀이 예시
# src/instagram/d2_notification_solution.py
"""알림 피드를 다형성으로 출력하기 (D-2 심화 과제)."""
class Notification:
def __init__(self, actor):
self.actor = actor
def describe(self):
return f"{self.actor} 님의 알림"
class LikeNotification(Notification):
def describe(self):
return f"{self.actor} 님이 좋아요를 눌렀어요"
class CommentNotification(Notification):
def describe(self):
return f"{self.actor} 님이 댓글을 남겼어요"
class SystemNotice: # Notification 을 상속하지 않는 시스템 공지 (덕 타이핑)
def __init__(self, message):
self.message = message
def describe(self):
return f"[공지] {self.message}"
feed = [
LikeNotification("jaehoon"),
CommentNotification("minji"),
Notification("seungwoo"),
SystemNotice("새 버전이 출시되었어요"),
]
for item in feed:
print(item.describe())
실행 결과예요.
jaehoon 님이 좋아요를 눌렀어요
minji 님이 댓글을 남겼어요
seungwoo 님의 알림
[공지] 새 버전이 출시되었어요
💡 튜터의 한마디: for item in feed: item.describe() 한 줄이 네 가지 알림을 모두 처리했어요. item 이 좋아요 알림이면 좋아요 메시지를, 댓글 알림이면 댓글 메시지를 알아서 골라 내죠. 이게 다형성이에요. 도전 과제의 SystemNotice 가 특히 재미있어요. 이건 Notification 을 상속하지 않은 완전한 남남인데도, describe() 메서드만 갖춘 덕분에 같은 리스트에서 나란히 출력됐어요. 파이썬이 "넌 무슨 타입이냐" 를 안 따지고 "describe() 가 있느냐" 만 봤기 때문이죠. 이게 바로 덕 타이핑이에요. 새 알림 종류가 생겨도 그 클래스에 describe 만 써주면 이 for 루프는 한 글자도 안 바뀌어요.
🤔 [생각해볼 주제 1] 상속으로 묶을까, 그냥 따로 둘까?
문제 상황 요약
사진·동영상 게시물의 공통점을 부모 Post 로 묶었어요. 공통 코드가 많을 땐 상속이 중복을 확 줄여주죠. 하지만 모든 걸 상속으로 묶는 게 늘 좋은 걸까요? 어떤 때 묶고, 어떤 때 따로 두는 게 나을까요?
튜터의 가이드 및 해설
기준은 "정말 같은 종류인가, 공통된 데이터·행동이 충분히 많은가" 예요. 상속은 "자식은 부모의 한 종류다(is-a)" 라는 관계가 자연스러울 때 어울려요.
상속으로 묶는 게 좋은 경우
- "사진 게시물은 게시물이다", "답글은 댓글이다" 처럼 자식이 부모의 한 종류라고 말해도 어색하지 않을 때.
- 작성자·캡션·좋아요·
add_like처럼 공통 데이터와 행동이 여러 클래스에 반복될 때 — 부모에 한 번만 써두면 중복이 사라져요. - 공통 부분을 한 곳에서 고치면 모든 자식에 일관되게 반영되길 바랄 때.
따로 두는 게 나은 경우
- 공통점이 거의 없는데 그저 "코드 몇 줄 재사용하려고" 억지로 엮을 때 — 부모를 한 번 고치면 엉뚱한 자식까지 영향을 받아 더 위험해져요.
- "A 는 B 의 한 종류다" 라고 말하면 어색할 때 (예: 자동차는 엔진의 한 종류가 아니에요. 이건 상속이 아니라 "가지고 있다(has-a)" 관계죠).
- 그저 같은 동작 하나만 필요할 때 — 이럴 땐 상속 대신 오늘 배운 덕 타이핑이 더 가벼워요.
"자식 is-a 부모" 가 자연스럽다 → 상속으로 묶기 (PhotoPost is-a Post)
공통은 거의 없고 동작만 같다 → 따로 두고 덕 타이핑
"가지고 있다" 관계다 → 상속 아님 (속성으로 품기)
한마디로, "is-a 관계가 자연스럽고 공통이 많으면 상속" 이에요. 애매하면 처음엔 따로 두었다가, 공통이 쌓이는 게 보일 때 부모로 묶어도 늦지 않아요.
💡 핵심을 한마디로
"자식은 부모의 한 종류다(is-a)" 가 자연스럽고 공통 데이터·행동이 많을 때만 상속으로 묶어요. 억지로 코드 재사용만 노린 상속은 부모 수정이 엉뚱한 자식을 깨뜨려 오히려 위험해요.
🤔 [생각해볼 주제 2] 다형성은 코드를 어떻게 편하게 만들까?
문제 상황 요약
피드를 출력할 때 for item in feed: item.describe() 한 줄로 모든 종류를 처리했어요. 종류를 if 로 일일이 확인하지 않았죠. 다형성이 없었다면 무슨 일이 벌어질까요?
튜터의 가이드 및 해설
핵심은 "새 종류가 생겼을 때, 호출하는 쪽 코드를 건드리느냐" 예요.
다형성이 없다면 (if 로 분기)
종류를 직접 확인하는 코드는 이렇게 돼요.
for item in feed:
if 사진이면: 사진용 출력
elif 동영상이면: 동영상용 출력
elif 광고면: 광고용 출력
elif ... (종류가 늘 때마다 여기에 한 줄씩 추가)
스토리·릴스 같은 새 종류가 생길 때마다 이 if 묶음을 찾아 한 갈래씩 더해야 해요. 곳곳에 흩어진 if 를 다 찾아 고쳐야 하고, 하나라도 빠뜨리면 그 종류만 출력이 안 되는 버그가 생기죠.
다형성을 쓰면
각 클래스가 자기 describe 를 책임지니, 새 종류는 그 클래스에 describe 하나만 써주면 끝이에요.
새 종류 추가 → 그 클래스에 describe() 만 작성
호출하는 쪽 → for item in feed: item.describe() (한 글자도 안 바뀜)
"호출하는 쪽 코드가 바뀌지 않는다" 가 왜 큰 장점이냐면, 그 코드는 이미 잘 동작하며 검증된 부분이거든요. 새 기능을 더할 때 멀쩡히 돌아가던 코드를 안 건드릴수록 새 버그가 끼어들 틈이 줄어요.
💡 핵심을 한마디로
다형성을 쓰면 새 종류를 추가할 때 그 클래스에 메서드 하나만 더하면 되고, 이미 잘 돌아가는 호출 코드(for item in feed)는 손대지 않아도 돼요. 검증된 코드를 안 건드리는 게 버그를 막는 핵심이에요.
🤔 [생각해볼 주제 3] 덕 타이핑은 자유로울까, 위험할까?
문제 상황 요약
파이썬은 타입을 따지지 않고 "그 메서드가 있느냐" 만 봐요. 덕분에 광고처럼 상속 관계가 없는 객체도 같은 피드에 쉽게 끼울 수 있었죠. 그런데 이 자유로움에는 어떤 책임이 따를까요?
튜터의 가이드 및 해설
덕 타이핑은 자유롭지만, 그 자유만큼 "약속을 지키는 책임" 이 사람에게 넘어와요.
자유로운 점
- 상속 족보를 미리 설계하지 않아도, 필요한 메서드만 갖추면 어디든 끼울 수 있어요. 광고를 게시물 피드에 넣을 때 굳이
Post를 상속시키지 않아도 됐죠. - 서로 무관한 클래스들이 같은 동작(
describe)만 맞추면 함께 어울려요. 코드가 유연해져요.
따르는 책임
만약 describe() 가 없는 객체를 실수로 피드에 넣으면 어떻게 될까요?
feed = [PhotoPost(...), 그냥_문자열_"hello"]
for item in feed:
item.describe() → 문자열엔 describe 가 없어 그 줄에서 오류!
상속이라면 "이건 분명 Post 다" 라고 타입이 미리 보증해 주지만, 덕 타이핑은 그런 보증이 없어요. "이 리스트에 들어오는 건 모두 describe() 가 있어야 한다" 는 약속을 사람이 지켜야 하죠. 즉 안전장치가 코드(타입)에서 사람의 주의로 옮겨간 거예요.
그래서 보통은 이렇게 균형을 잡아요. 진짜 같은 종류이고 공통이 많으면 상속으로 묶어 보증을 받고, 그저 같은 동작만 필요한 느슨한 합류엔 덕 타이핑을 쓰되 "어떤 메서드가 꼭 있어야 하는지" 를 분명히 약속으로 남겨요.
💡 핵심을 한마디로
덕 타이핑은 족보 없이 자유롭게 어울리게 해주지만, "필요한 메서드가 반드시 있어야 한다" 는 약속을 사람이 책임져야 해요. 자유로운 만큼 안전장치가 타입에서 주의로 옮겨가는 셈이에요.