포스트

[Python 100일 챌린지] Day 36 - 메서드 오버라이딩과 다중 상속

[Python 100일 챌린지] Day 36 - 메서드 오버라이딩과 다중 상속

Day 35에서 부모 기능을 물려받았는데, 자식이 부모와 다르게 동작해야 한다면? 🤔 ──예를 들어 Animal.speak()는 “소리를 낸다”인데, Dog.speak()는 “멍멍!”이어야 합니다! 😊

게임 캐릭터를 생각해보세요. 모든 캐릭터가 attack() 메서드를 가지지만, 전사는 “검으로 베기”, 마법사는 “파이어볼”, 궁수는 “화살 쏘기”로 구현됩니다. 같은 이름, 다른 동작!

스마트폰 알람도 마찬가지입니다. 기본 알람은 “띠리링~”이지만, 사용자가 원하는 소리로 재정의(Override) 할 수 있죠.

오늘 배울 메서드 오버라이딩으로 부모 기능을 내 입맛에 맞게 바꿉니다! 💡

🎯 오늘의 학습 목표

⭐⭐⭐⭐ (40-50분 완독)

📚 사전 지식


🎯 학습 목표 1: 메서드 오버라이딩의 개념 이해하기

메서드 오버라이딩이란?

메서드 오버라이딩(Method Overriding)은 자식 클래스에서 부모 클래스의 메서드를 같은 이름으로 재정의하는 것입니다. Day 35에서 기초를 배웠으니, 오늘은 두 가지 방식을 비교해봅시다!

Overriding vs Extension 완전 오버라이딩과 확장 오버라이딩의 차이

완전 오버라이딩 vs 확장 오버라이딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Animal:
    def speak(self):
        return "동물이 소리를 냅니다."

class Dog(Animal):
    # 완전 오버라이딩 - 부모 메서드 무시
    def speak(self):
        return "멍멍!"

class Cat(Animal):
    # 확장 오버라이딩 - 부모 메서드 활용 + 추가
    def speak(self):
        parent_result = super().speak()
        return f"{parent_result} 그리고 야옹~"

> [!TIP]
> **super() 언제 쓰나요?**
> 부모의 원래 기능을 **유지하면서** 새로운 기능을 덧붙일  사용합니다. `super().메서드명()`으로 부모를 호출하는 것을 잊지 마세요!


dog = Dog()
cat = Cat()

print(dog.speak())  # 멍멍!
print(cat.speak())  # 동물이 소리를 냅니다. 그리고 야옹~

실전 예제: 파일 저장 시스템

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class FileWriter:
    """파일 작성 클래스 (부모)"""

    def __init__(self, filename):
        self.filename = filename

    def write(self, content):
        """기본 저장"""
        return f"'{self.filename}'에 저장됨"

class EncryptedFileWriter(FileWriter):
    """암호화 파일 작성 클래스"""

    def write(self, content):
        # 부모 메서드 호출 전 암호화
        encrypted = self._encrypt(content)
        result = super().write(encrypted)
        return f"🔒 암호화 후 {result}"

    def _encrypt(self, content):
        # 간단한 암호화 (실제로는 복잡한 알고리즘 사용)
        return f"encrypted_{content}"

class CompressedFileWriter(FileWriter):
    """압축 파일 작성 클래스"""

    def write(self, content):
        # 부모 메서드 호출 전 압축
        compressed = self._compress(content)
        result = super().write(compressed)
        return f"📦 압축 후 {result}"

    def _compress(self, content):
        return f"compressed_{content}"

# 사용
normal = FileWriter("normal.txt")
encrypted = EncryptedFileWriter("secure.txt")
compressed = CompressedFileWriter("archive.txt")

print(normal.write("Hello World"))
print(encrypted.write("Secret Data"))
print(compressed.write("Large File"))

실행 결과:

1
2
3
'normal.txt'에 저장됨
🔒 암호화 후 'secure.txt'에 저장됨
📦 압축 후 'archive.txt'에 저장됨

🎯 학습 목표 2: 다중 상속 이해하기

다중 상속 (Multiple Inheritance)이란?

Python은 여러 부모 클래스를 동시에 상속받을 수 있습니다!

Diamond Inheritance MRO 다이아몬드 상속 구조와 MRO 경로

기본 문법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Parent1:
    def method1(self):
        return "Parent1의 메서드"

class Parent2:
    def method2(self):
        return "Parent2의 메서드"

class Child(Parent1, Parent2):
    """Parent1과 Parent2를 모두 상속"""
    pass

child = Child()
print(child.method1())  # Parent1의 메서드
print(child.method2())  # Parent2의 메서드

다이아몬드 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A:
    def method(self):
        return "A"

class B(A):
    def method(self):
        return "B"

class C(A):
    def method(self):
        return "C"

class D(B, C):
    """B와 C 모두 상속 - 어떤 method가 호출될까?"""
    pass

d = D()
print(d.method())  # B (왼쪽 부모 우선)

# MRO 확인
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

🎯 학습 목표 3: MRO(메서드 검색 순서) 이해하기

MRO (Method Resolution Order)란?

Python이 메서드를 찾는 순서입니다.

MRO 확인 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

# 방법 1: __mro__ 속성
print(D.__mro__)

# 방법 2: mro() 메서드
print(D.mro())

# 방법 3: 간단하게 출력
for i, cls in enumerate(D.mro()):
    print(f"{i}. {cls.__name__}")

실행 결과:

1
2
3
4
5
0. D
1. B
2. C
3. A
4. object

실전 예제: 로깅 시스템

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Logger:
    """로깅 기능"""

    def log(self, message):
        print(f"[LOG] {message}")

class TimestampMixin:
    """타임스탬프 기능"""

    def get_timestamp(self):
        from datetime import datetime
        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

class ColorMixin:
    """색상 기능"""

    COLORS = {
        'RED': '\033[91m',
        'GREEN': '\033[92m',
        'YELLOW': '\033[93m',
        'BLUE': '\033[94m',
        'END': '\033[0m'
    }

    def colorize(self, text, color='GREEN'):
        return f"{self.COLORS.get(color, '')}{text}{self.COLORS['END']}"

class AdvancedLogger(Logger, TimestampMixin, ColorMixin):
    """고급 로거 - 다중 상속"""

    def log(self, message, level='INFO'):
        timestamp = self.get_timestamp()
        colored_level = self.colorize(level, 'YELLOW')
        colored_msg = self.colorize(message, 'GREEN')
        print(f"[{timestamp}] [{colored_level}] {colored_msg}")

# 사용
logger = AdvancedLogger()
logger.log("프로그램 시작", "INFO")
logger.log("작업 완료", "SUCCESS")
logger.log("오류 발생", "ERROR")

print("\nMRO:")
for i, cls in enumerate(AdvancedLogger.mro()):
    print(f"{i}. {cls.__name__}")

🎯 학습 목표 4: 믹스인 패턴 활용하기

믹스인 (Mixin) 패턴이란?

믹스인은 기능을 추가하기 위한 작은 클래스입니다. 마치 게임 캐릭터에 ‘아이템’을 장착하듯, 필요한 기능만 쏙쏙 골라 클래스에 붙여줄 수 있습니다.

Mixin Concept 믹스인은 레고 블록처럼 기능을 조립합니다

믹스인 설계 원칙

  1. 단일 기능에 집중
  2. 독립적으로 동작
  3. 이름에 Mixin 접미사 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class JSONMixin:
    """JSON 직렬화 기능"""

    def to_json(self):
        import json
        data = {
            key: value for key, value in self.__dict__.items()
            if not key.startswith('_')
        }
        return json.dumps(data, ensure_ascii=False, indent=2)

class ComparableMixin:
    """비교 기능"""

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        return self.__dict__ == other.__dict__

    def __ne__(self, other):
        return not self.__eq__(other)

class PrintableMixin:
    """출력 기능"""

    def print_info(self):
        print("="*50)
        for key, value in self.__dict__.items():
            if not key.startswith('_'):
                print(f"{key}: {value}")
        print("="*50)

class Person(JSONMixin, ComparableMixin, PrintableMixin):
    """믹스인을 활용한 Person 클래스"""

    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

# 사용
person1 = Person("홍길동", 30, "hong@example.com")
person2 = Person("홍길동", 30, "hong@example.com")
person3 = Person("김철수", 25, "kim@example.com")

# JSONMixin 기능
print("JSON 변환:")
print(person1.to_json())

# ComparableMixin 기능
print(f"\nperson1 == person2: {person1 == person2}")  # True
print(f"person1 == person3: {person1 == person3}")  # False

# PrintableMixin 기능
print("\n정보 출력:")
person1.print_info()

실행 결과:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JSON 변환:
{
  "name": "홍길동",
  "age": 30,
  "email": "hong@example.com"
}

person1 == person2: True
person1 == person3: False

정보 출력:
==================================================
name: 홍길동
age: 30
email: hong@example.com
==================================================

🚀 실전 예제: 완전한 데이터 모델

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import json
from datetime import datetime

# 믹스인 클래스들
class TimestampMixin:
    """생성/수정 시간 추적"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.created_at = datetime.now()
        self.updated_at = datetime.now()

    def touch(self):
        """수정 시간 업데이트"""
        self.updated_at = datetime.now()

class SerializableMixin:
    """직렬화 기능"""

    def to_dict(self):
        """딕셔너리로 변환"""
        result = {}
        for key, value in self.__dict__.items():
            if isinstance(value, datetime):
                result[key] = value.isoformat()
            else:
                result[key] = value
        return result

    def to_json(self):
        """JSON 문자열로 변환"""
        return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)

class ValidatableMixin:
    """유효성 검사 기능"""

    def validate(self):
        """자식 클래스에서 구현"""
        raise NotImplementedError("validate() 메서드를 구현하세요")

    def is_valid(self):
        """유효성 검사 실행"""
        try:
            self.validate()
            return True
        except ValueError:
            return False

# 실제 모델 클래스
class Product(TimestampMixin, SerializableMixin, ValidatableMixin):
    """상품 모델"""

    def __init__(self, name, price, stock):
        super().__init__()
        self.name = name
        self.price = price
        self.stock = stock

    def validate(self):
        """유효성 검사"""
        if not self.name or len(self.name) < 2:
            raise ValueError("상품명은 2자 이상이어야 합니다")
        if self.price < 0:
            raise ValueError("가격은 0 이상이어야 합니다")
        if self.stock < 0:
            raise ValueError("재고는 0 이상이어야 합니다")

    def update_price(self, new_price):
        """가격 변경"""
        if new_price < 0:
            raise ValueError("가격은 0 이상이어야 합니다")
        self.price = new_price
        self.touch()  # TimestampMixin에서 제공

    def sell(self, quantity):
        """판매"""
        if quantity > self.stock:
            raise ValueError("재고가 부족합니다")
        self.stock -= quantity
        self.touch()

    def __str__(self):
        return f"{self.name} ({self.price:,}원, 재고: {self.stock}개)"

# 사용
product = Product("노트북", 1500000, 10)

print("초기 상품:")
print(product)
print(f"유효성: {product.is_valid()}")

# 가격 변경
product.update_price(1400000)
print(f"\n가격 변경 후: {product}")

# 판매
product.sell(3)
print(f"판매 후: {product}")

# JSON 변환
print("\nJSON:")
print(product.to_json())

# 딕셔너리 변환
print("\n딕셔너리:")
print(product.to_dict())

# 잘못된 데이터
print("\n유효성 검사:")
invalid_product = Product("", -1000, -5)
print(f"유효성: {invalid_product.is_valid()}")
try:
    invalid_product.validate()
except ValueError as e:
    print(f"에러: {e}")

실행 결과:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
초기 상품:
노트북 (1,500,000원, 재고: 10개)
유효성: True

가격 변경 후: 노트북 (1,400,000원, 재고: 10개)
판매 후: 노트북 (1,400,000원, 재고: 7개)

JSON:
{
  "created_at": "2025-04-05T10:30:15.123456",
  "updated_at": "2025-04-05T10:30:16.789012",
  "name": "노트북",
  "price": 1400000,
  "stock": 7
}

딕셔너리:
{'created_at': '2025-04-05T10:30:15.123456', 'updated_at': '2025-04-05T10:30:16.789012', 'name': '노트북', 'price': 1400000, 'stock': 7}

유효성 검사:
유효성: False
에러: 상품명은 2자 이상이어야 합니다

💡 실전 팁 & 주의사항

Tip 1: 완전 오버라이딩보다 확장 오버라이딩 활용하기

부모의 기능을 완전히 무시하기보다는 super()를 활용하여 확장하는 것이 더 유연합니다.

1
2
3
4
5
# ✅ 권장: 확장 오버라이딩
class Child(Parent):
    def method(self):
        result = super().method()  # 부모 기능 활용
        return result + " + 추가 기능"

Tip 2: 다중 상속은 신중하게 사용하기

다중 상속은 강력하지만 복잡도를 높일 수 있습니다. 믹스인 패턴을 사용하여 깔끔하게 설계하세요.

1
2
3
4
5
6
7
# ✅ 좋은 예: 믹스인 패턴
class JSONMixin:
    def to_json(self):
        return json.dumps(self.__dict__)

class User(JSONMixin):  # 단일 기능만 추가
    pass

Tip 3: MRO를 이해하고 활용하기

복잡한 상속 구조에서는 Class.mro()로 메서드 검색 순서를 확인하세요.

1
2
# MRO 확인
print(MyClass.mro())

Tip 4: 다이아몬드 문제 주의하기

다이아몬드 상속 구조에서는 super()를 사용하여 모든 부모가 한 번씩만 호출되도록 하세요.

1
2
3
4
# ✅ super() 사용으로 안전하게 처리
class D(B, C):
    def __init__(self):
        super().__init__()  # MRO 순서대로 호출

🧪 연습 문제

문제 1: 게임 아이템 시스템

믹스인을 활용하여 게임 아이템 시스템을 구현하세요:

요구사항:

  1. EquippableMixin: 장착/해제 기능
  2. UpgradeableMixin: 강화 기능
  3. Item 클래스: 믹스인 활용
1
2
3
4
5
# 사용 예시
sword = Weapon("전설의 검", 100)
sword.equip()
sword.upgrade()
print(sword.get_power())
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class EquippableMixin:
    """장착 가능 믹스인"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.equipped = False

    def equip(self):
        if self.equipped:
            return "❌ 이미 장착 중입니다"
        self.equipped = True
        return f"{self.name}을(를) 장착했습니다"

    def unequip(self):
        if not self.equipped:
            return "❌ 장착되지 않았습니다"
        self.equipped = False
        return f"{self.name}을(를) 해제했습니다"

class UpgradeableMixin:
    """강화 가능 믹스인"""

    MAX_LEVEL = 10

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.level = 0

    def upgrade(self):
        if self.level >= self.MAX_LEVEL:
            return f"❌ 최대 레벨({self.MAX_LEVEL})입니다"
        self.level += 1
        return f"⬆️ {self.name}이(가) +{self.level}로 강화되었습니다!"

class Item:
    """아이템 기본 클래스"""

    def __init__(self, name):
        self.name = name

class Weapon(Item, EquippableMixin, UpgradeableMixin):
    """무기 클래스"""

    def __init__(self, name, base_power):
        super().__init__(name)
        self.base_power = base_power

    def get_power(self):
        bonus = self.base_power * (self.level * 0.1)
        return int(self.base_power + bonus)

    def __str__(self):
        power = self.get_power()
        status = "⚔️" if self.equipped else "📦"
        return f"{status} {self.name} +{self.level} (공격력: {power})"

# 테스트
sword = Weapon("전설의 검", 100)
print(sword)
print(sword.equip())
print(sword.upgrade())
print(sword.upgrade())
print(sword.upgrade())
print(sword)
print(f"공격력: {sword.get_power()}")

문제 2: 블로그 시스템

다중 상속과 믹스인으로 블로그 시스템을 구현하세요:

요구사항:

  1. Post 기본 클래스
  2. PublishableMixin: 발행/비공개 기능
  3. CommentableMixin: 댓글 기능
  4. BlogPost: 모든 기능 통합
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from datetime import datetime

class PublishableMixin:
    """발행 관리 믹스인"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.is_published = False
        self.published_at = None

    def publish(self):
        if self.is_published:
            return "❌ 이미 발행되었습니다"
        self.is_published = True
        self.published_at = datetime.now()
        return f"'{self.title}'이(가) 발행되었습니다"

    def unpublish(self):
        if not self.is_published:
            return "❌ 발행되지 않았습니다"
        self.is_published = False
        return f"'{self.title}'이(가) 비공개 처리되었습니다"

class CommentableMixin:
    """댓글 기능 믹스인"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.comments = []

    def add_comment(self, author, content):
        comment = {
            'author': author,
            'content': content,
            'created_at': datetime.now()
        }
        self.comments.append(comment)
        return f"💬 {author}님이 댓글을 작성했습니다"

    def get_comment_count(self):
        return len(self.comments)

    def print_comments(self):
        if not self.comments:
            print("댓글이 없습니다.")
            return

        print(f"\n댓글 ({self.get_comment_count()}개):")
        for i, comment in enumerate(self.comments, 1):
            time = comment['created_at'].strftime("%Y-%m-%d %H:%M")
            print(f"{i}. {comment['author']} ({time})")
            print(f"   {comment['content']}")

class Post:
    """포스트 기본 클래스"""

    def __init__(self, title, content, author):
        self.title = title
        self.content = content
        self.author = author
        self.created_at = datetime.now()

class BlogPost(Post, PublishableMixin, CommentableMixin):
    """블로그 포스트 - 모든 기능 통합"""

    def __str__(self):
        status = "🟢 발행됨" if self.is_published else "🔴 비공개"
        return f"[{status}] {self.title} by {self.author} (댓글: {self.get_comment_count()})"

# 테스트
post = BlogPost(
    title="Python 객체지향 프로그래밍",
    content="OOP는 재미있습니다!",
    author="홍길동"
)

print(post)
print(post.publish())
print(post.add_comment("김철수", "좋은 글 감사합니다!"))
print(post.add_comment("이영희", "도움이 많이 되었어요"))
print(post)
post.print_comments()

📝 오늘 배운 내용 정리

개념 설명 예시
오버라이딩 부모 메서드 재정의 def method(self):
다중 상속 여러 부모 상속 class C(A, B):
MRO 메서드 검색 순서 Class.mro()
믹스인 기능 추가 클래스 class JSONMixin:

다중 상속 사용 시 주의사항

권장:

  • 믹스인 패턴 사용
  • 명확한 역할 분리
  • MRO 이해하고 활용

주의:

  • 복잡한 상속 계층
  • 다이아몬드 문제 무시
  • 책임이 불명확한 클래스

🔗 관련 자료

📚 이전 학습

Day 35: 상속의 기초 ⭐⭐⭐

어제는 상속을 통한 코드 재사용, super()로 부모 클래스 접근하기, isinstance()와 issubclass() 활용법을 배웠습니다!

📚 다음 학습

Day 37: 캡슐화와 정보 은닉 심화 ⭐⭐⭐⭐

내일은 캡슐화 원칙 심화, 접근 제어자 패턴, Property 데코레이터 고급 활용, 불변 객체(Immutable Object) 설계를 배웁니다!


“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀

Day 36/100 Phase 4: 객체지향 프로그래밍 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.