포스트

[Python 100일 챌린지] Day 35 - 상속의 기초

[Python 100일 챌린지] Day 35 - 상속의 기초

“강아지”와 “고양이” 클래스를 만드는데, 둘 다 eat(), sleep() 메서드가 똑같다면? 중복 코드를 계속 작성해야 할까요? 🤔 ──아니요! “동물(Animal)” 클래스를 만들고 공통 기능을 물려받으면 됩니다! 😊

게임 캐릭터를 생각해보세요. 전사, 마법사, 궁수 모두 이동(), 공격() 같은 기본 기능은 같고, 특수기술만 다르죠. 매번 이동()을 다시 작성할 필요 없습니다!

스마트폰도 마찬가지입니다. 갤럭시S, 갤럭시노트, 갤럭시폴드 모두 “갤럭시” 시리즈의 기본 기능을 물려받고, 각자 고유 기능만 추가합니다.

이것이 상속(Inheritance)입니다! 코드 중복을 없애고 재사용성을 극대화하는 OOP의 핵심 원리! 💡

🎯 오늘의 학습 목표

⭐⭐⭐ (30-40분 완독)

📚 사전 지식


🎯 학습 목표 1: 상속의 개념과 필요성 이해하기

상속이란?

상속(Inheritance)은 기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 만드는 것입니다.

🏠 실생활 비유: 부모와 자식

Inheritance Concept 상속의 개념: 부모의 기능을 물려받고 새로운 기능을 추가합니다.

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
# 부모 (동물)
class Animal:
    def eat(self):
        return "먹는다"

    def sleep(self):
        return "잔다"

# 자식 (개) - 동물의 모든 것을 물려받음 + 고유 기능 추가
class Dog(Animal):
    def bark(self):
        return "멍멍!"

# 자식 (고양이) - 동물의 모든 것을 물려받음 + 고유 기능 추가
class Cat(Animal):
    def meow(self):
        return "야옹~"

dog = Dog()
print(dog.eat())    # 먹는다 (부모에게서 물려받음)
print(dog.sleep())  # 잔다 (부모에게서 물려받음)
print(dog.bark())   # 멍멍! (자신만의 기능)

cat = Cat()
print(cat.eat())    # 먹는다
print(cat.meow())   # 야옹~

🎯 학습 목표 2: 부모 클래스와 자식 클래스 정의하기

기본 상속 문법

1
2
3
4
5
6
7
class ParentClass:
    """부모 클래스"""
    pass

class ChildClass(ParentClass):
    """자식 클래스"""
    pass

예제 1: 간단한 상속

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
class Person:
    """사람 클래스 (부모)"""

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

    def introduce(self):
        return f"안녕하세요, {self.name}입니다. {self.age}살입니다."

class Student(Person):
    """학생 클래스 (자식) - Person을 상속"""

    def __init__(self, name, age, student_id):
        # 부모 클래스 초기화
        super().__init__(name, age)
        # 자식 클래스 고유 속성
        self.student_id = student_id

    def study(self):
        return f"{self.name}이(가) 공부합니다."

# 사용
person = Person("홍길동", 30)
print(person.introduce())

student = Student("김철수", 20, "2024001")
print(student.introduce())  # 부모 메서드
print(student.study())       # 자식 메서드
print(f"학번: {student.student_id}")

실행 결과:

1
2
3
4
안녕하세요, 홍길동입니다. 30살입니다.
안녕하세요, 김철수입니다. 20살입니다.
김철수이(가) 공부합니다.
학번: 2024001

🎯 학습 목표 3: super()로 부모 클래스 접근하기

super()는 부모 클래스의 메서드를 호출할 때 사용합니다.

왜 super()를 사용할까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Parent:
    def __init__(self, name):
        self.name = name
        print(f"Parent 초기화: {name}")

class Child(Parent):
    def __init__(self, name, age):
        # ❌ 방법 1: 직접 호출 (권장하지 않음)
        # Parent.__init__(self, name)

        # ✅ 방법 2: super() 사용 (권장)
        super().__init__(name)
        self.age = age
        print(f"Child 초기화: {age}")

child = Child("홍길동", 10)

실행 결과:

1
2
Parent 초기화: 홍길동
Child 초기화: 10살

실전 예제: 직원 관리 시스템

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
class Employee:
    """직원 클래스 (부모)"""

    employee_count = 0

    def __init__(self, name, employee_id, salary):
        self.name = name
        self.employee_id = employee_id
        self.salary = salary
        Employee.employee_count += 1

    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"

    def get_annual_salary(self):
        return self.salary * 12

class Developer(Employee):
    """개발자 클래스 (자식)"""

    def __init__(self, name, employee_id, salary, language):
        super().__init__(name, employee_id, salary)
        self.language = language

    def write_code(self):
        return f"{self.name}이(가) {self.language}로 코딩합니다."

    def get_info(self):
        # 부모 메서드 활용 + 추가 정보
        base_info = super().get_info()
        return f"{base_info} - {self.language} 개발자"

class Designer(Employee):
    """디자이너 클래스 (자식)"""

    def __init__(self, name, employee_id, salary, tool):
        super().__init__(name, employee_id, salary)
        self.tool = tool

    def design(self):
        return f"{self.name}이(가) {self.tool}로 디자인합니다."

    def get_info(self):
        base_info = super().get_info()
        return f"{base_info} - {self.tool} 디자이너"

# 사용
dev = Developer("홍길동", "D001", 5000000, "Python")
designer = Designer("김철수", "D002", 4500000, "Figma")

print(dev.get_info())
print(dev.write_code())
print(f"연봉: {dev.get_annual_salary():,}\n")

print(designer.get_info())
print(designer.design())
print(f"연봉: {designer.get_annual_salary():,}\n")

print(f"총 직원 수: {Employee.employee_count}")

실행 결과:

1
2
3
4
5
6
7
8
9
홍길동 (ID: D001) - Python 개발자
홍길동이(가) Python로 코딩합니다.
연봉: 60,000,000원

김철수 (ID: D002) - Figma 디자이너
김철수이(가) Figma로 디자인합니다.
연봉: 54,000,000원

총 직원 수: 2명

🎯 학습 목표 4: 메서드 오버라이딩 이해하기

메서드 오버라이딩이란?

자식 클래스에서 부모 메서드를 재정의하는 것입니다.

Method Overriding 메서드 오버라이딩: 부모의 행동을 자식이 변경합니다.

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
class Animal:
    """동물 클래스"""

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

    def speak(self):
        return f"{self.name}이(가) 소리를 냅니다."

class Dog(Animal):
    """개 클래스 - speak 오버라이딩"""

    def speak(self):
        return f"{self.name}: 멍멍!"

class Cat(Animal):
    """고양이 클래스 - speak 오버라이딩"""

    def speak(self):
        return f"{self.name}: 야옹~"

class Cow(Animal):
    """소 클래스 - speak 오버라이딩"""

    def speak(self):
        return f"{self.name}: 음메~"

# 사용
animals = [
    Dog("바둑이"),
    Cat("나비"),
    Cow("얼룩이")
]

for animal in animals:
    print(animal.speak())

실행 결과:

1
2
3
바둑이: 멍멍!
나비: 야옹~
얼룩이: 음메~

🚗 실전 예제: 차량 관리 시스템

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
class Vehicle:
    """차량 클래스 (부모)"""

    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year
        self.mileage = 0

    def drive(self, distance):
        self.mileage += distance
        return f"{distance}km 주행 완료 (총 주행거리: {self.mileage}km)"

    def get_info(self):
        return f"{self.year}년식 {self.brand} {self.model}"

    def honk(self):
        return "빵빵!"

class Car(Vehicle):
    """자동차 클래스"""

    def __init__(self, brand, model, year, num_doors):
        super().__init__(brand, model, year)
        self.num_doors = num_doors

    def get_info(self):
        base_info = super().get_info()
        return f"{base_info} ({self.num_doors}도어)"

class Truck(Vehicle):
    """트럭 클래스"""

    def __init__(self, brand, model, year, cargo_capacity):
        super().__init__(brand, model, year)
        self.cargo_capacity = cargo_capacity

    def get_info(self):
        base_info = super().get_info()
        return f"{base_info} (적재량: {self.cargo_capacity}톤)"

    def load_cargo(self, weight):
        if weight <= self.cargo_capacity:
            return f"{weight}톤 적재 완료"
        else:
            return f"❌ 적재 불가 (최대: {self.cargo_capacity}톤)"

class Motorcycle(Vehicle):
    """오토바이 클래스"""

    def __init__(self, brand, model, year, engine_cc):
        super().__init__(brand, model, year)
        self.engine_cc = engine_cc

    def get_info(self):
        base_info = super().get_info()
        return f"{base_info} ({self.engine_cc}cc)"

    def honk(self):
        return "빠아앙!"  # 오버라이딩

# 사용
car = Car("현대", "소나타", 2024, 4)
truck = Truck("볼보", "FH16", 2023, 20)
bike = Motorcycle("혼다", "CB500X", 2024, 500)

vehicles = [car, truck, bike]

print("="*50)
print("차량 정보")
print("="*50)
for vehicle in vehicles:
    print(vehicle.get_info())
    print(vehicle.honk())
    print(vehicle.drive(100))
    print()

# 트럭 전용 기능
print(truck.load_cargo(15))
print(truck.load_cargo(25))

실행 결과:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
==================================================
차량 정보
==================================================
2024년식 현대 소나타 (4도어)
빵빵!
100km 주행 완료 (총 주행거리: 100km)

2023년식 볼보 FH16 (적재량: 20톤)
빵빵!
100km 주행 완료 (총 주행거리: 100km)

2024년식 혼다 CB500X (500cc)
빠아앙!
100km 주행 완료 (총 주행거리: 100km)

✅ 15톤 적재 완료
❌ 적재 불가 (최대: 20톤)

💡 실전 팁 & 주의사항

Tip 1: super()를 사용하여 부모 메서드 호출하기

직접 부모 클래스 이름을 사용하는 것보다 super()를 사용하는 것이 더 유연하고 안전합니다.

[!NOTE] super()self super().__init__()를 호출할 때는 self를 넘기지 않아도 됩니다. 파이썬이 알아서 처리해주기 때문입니다! 반면, Parent.__init__(self)처럼 클래스 이름을 직접 쓸 때는 반드시 self를 넣어줘야 합니다.

1
2
3
4
5
6
7
8
9
# ❌ 권장하지 않음
class Child(Parent):
    def __init__(self):
        Parent.__init__(self)

# ✅ 권장
class Child(Parent):
    def __init__(self):
        super().__init__()

Tip 2: “is-a” 관계일 때만 상속 사용하기

상속은 “~은 ~이다” 관계일 때 사용하세요. “~은 ~을 가진다” 관계라면 컴포지션을 사용하세요.

1
2
3
4
5
6
7
8
9
# ✅ 좋은 예: Student is a Person
class Student(Person):
    pass

# ❌ 나쁜 예: Car is a Engine? (No!)
# Car has an Engine이므로 컴포지션 사용
class Car:
    def __init__(self):
        self.engine = Engine()  # 컴포지션

Tip 3: 상속 깊이는 3단계 이하로 유지

너무 깊은 상속 계층은 코드를 복잡하게 만들고 유지보수를 어렵게 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ❌ 너무 깊은 상속
class A:
    pass
class B(A):
    pass
class C(B):
    pass
class D(C):  # 4단계, 너무 깊음
    pass

# ✅ 적절한 상속 깊이
class Animal:
    pass
class Dog(Animal):  # 2단계, 적절함
    pass

Tip 4: isinstance()로 타입 체크하기

런타임에 객체의 타입을 확인해야 할 때는 isinstance()를 사용하세요.

1
2
if isinstance(obj, Shape):
    print(f"넓이: {obj.area()}")

📚 isinstance()와 issubclass()

isinstance(): 인스턴스 확인

1
2
3
4
5
6
7
8
9
10
11
class Animal:
    pass

class Dog(Animal):
    pass

dog = Dog()

print(isinstance(dog, Dog))     # True
print(isinstance(dog, Animal))  # True (부모 클래스도 True!)
print(isinstance(dog, str))     # False

issubclass(): 상속 관계 확인

1
2
3
print(issubclass(Dog, Animal))  # True
print(issubclass(Animal, Dog))  # False
print(issubclass(Dog, Dog))     # True (자기 자신)

활용 예제

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
class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

def print_area(shape):
    """도형의 넓이 출력"""
    if isinstance(shape, Shape):
        print(f"넓이: {shape.area():.2f}")
    else:
        print("❌ Shape 타입이 아닙니다.")

rect = Rectangle(5, 3)
circle = Circle(4)

print_area(rect)    # 넓이: 15.00
print_area(circle)  # 넓이: 50.27
print_area("문자열")  # ❌ Shape 타입이 아닙니다.

🧪 연습 문제

문제 1: 은행 계좌 시스템

다음 요구사항을 만족하는 클래스들을 작성하세요:

요구사항:

  1. BankAccount 부모 클래스: 잔액 관리, 입출금 기능
  2. SavingsAccount 자식 클래스: 이자 지급 기능 추가
  3. CheckingAccount 자식 클래스: 수표 발행 기능 추가
1
2
3
4
5
6
7
# 사용 예시
savings = SavingsAccount("홍길동", 1000000, interest_rate=0.02)
print(savings.deposit(500000))
print(savings.apply_interest())

checking = CheckingAccount("김철수", 2000000, check_limit=500000)
print(checking.issue_check(300000))
✅ 정답
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
class BankAccount:
    """은행 계좌 클래스 (부모)"""

    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        """입금"""
        if amount <= 0:
            return "❌ 입금액은 0보다 커야 합니다."
        self.balance += amount
        return f"{amount:,}원 입금 완료 (잔액: {self.balance:,}원)"

    def withdraw(self, amount):
        """출금"""
        if amount <= 0:
            return "❌ 출금액은 0보다 커야 합니다."
        if amount > self.balance:
            return f"❌ 잔액 부족 (현재 잔액: {self.balance:,}원)"
        self.balance -= amount
        return f"{amount:,}원 출금 완료 (잔액: {self.balance:,}원)"

    def get_balance(self):
        return f"{self.owner}님의 잔액: {self.balance:,}"

class SavingsAccount(BankAccount):
    """저축 계좌 클래스 (자식)"""

    def __init__(self, owner, balance=0, interest_rate=0.01):
        super().__init__(owner, balance)
        self.interest_rate = interest_rate

    def apply_interest(self):
        """이자 지급"""
        interest = int(self.balance * self.interest_rate)
        self.balance += interest
        return f"💰 이자 {interest:,}원 지급 (잔액: {self.balance:,}원)"

class CheckingAccount(BankAccount):
    """당좌 계좌 클래스 (자식)"""

    def __init__(self, owner, balance=0, check_limit=1000000):
        super().__init__(owner, balance)
        self.check_limit = check_limit

    def issue_check(self, amount):
        """수표 발행"""
        if amount <= 0:
            return "❌ 수표 금액은 0보다 커야 합니다."
        if amount > self.check_limit:
            return f"❌ 수표 한도 초과 (최대: {self.check_limit:,}원)"
        if amount > self.balance:
            return f"❌ 잔액 부족 (현재 잔액: {self.balance:,}원)"
        self.balance -= amount
        return f"📝 {amount:,}원 수표 발행 (잔액: {self.balance:,}원)"

# 테스트
savings = SavingsAccount("홍길동", 1000000, interest_rate=0.02)
print(savings.get_balance())
print(savings.deposit(500000))
print(savings.apply_interest())
print()

checking = CheckingAccount("김철수", 2000000, check_limit=500000)
print(checking.get_balance())
print(checking.issue_check(300000))
print(checking.issue_check(600000))

문제 2: 게임 캐릭터 시스템

Character 부모 클래스를 상속받는 Warrior, Mage, Archer 클래스를 작성하세요.

요구사항:

  1. 부모 클래스: 이름, HP, 공격력 관리
  2. 각 직업별 고유 스킬 구현
  3. attack() 메서드 오버라이딩
✅ 정답
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
class Character:
    """캐릭터 클래스 (부모)"""

    def __init__(self, name, hp, attack_power):
        self.name = name
        self.hp = hp
        self.max_hp = hp
        self.attack_power = attack_power

    def attack(self):
        return f"{self.name}의 기본 공격! (데미지: {self.attack_power})"

    def take_damage(self, damage):
        self.hp = max(0, self.hp - damage)
        if self.hp == 0:
            return f"💀 {self.name}이(가) 쓰러졌습니다!"
        return f"❤️ {self.name} HP: {self.hp}/{self.max_hp}"

class Warrior(Character):
    """전사 클래스"""

    def __init__(self, name):
        super().__init__(name, hp=150, attack_power=20)

    def attack(self):
        return f"⚔️ {self.name}의 강력한 검 공격! (데미지: {self.attack_power})"

    def shield_bash(self):
        damage = self.attack_power * 1.5
        return f"🛡️ {self.name}의 방패 강타! (데미지: {damage:.0f})"

class Mage(Character):
    """마법사 클래스"""

    def __init__(self, name):
        super().__init__(name, hp=80, attack_power=30)
        self.mana = 100

    def attack(self):
        return f"{self.name}의 마법 공격! (데미지: {self.attack_power})"

    def fireball(self):
        if self.mana >= 30:
            self.mana -= 30
            damage = self.attack_power * 2
            return f"🔥 {self.name}의 파이어볼! (데미지: {damage:.0f}, 마나: {self.mana})"
        return "❌ 마나가 부족합니다!"

class Archer(Character):
    """궁수 클래스"""

    def __init__(self, name):
        super().__init__(name, hp=100, attack_power=25)
        self.arrows = 20

    def attack(self):
        if self.arrows > 0:
            self.arrows -= 1
            return f"🏹 {self.name}의 화살 공격! (데미지: {self.attack_power}, 화살: {self.arrows})"
        return "❌ 화살이 부족합니다!"

    def multi_shot(self):
        if self.arrows >= 3:
            self.arrows -= 3
            damage = self.attack_power * 2
            return f"🎯 {self.name}의 다중 사격! (데미지: {damage:.0f}, 화살: {self.arrows})"
        return "❌ 화살이 부족합니다!"

# 테스트
warrior = Warrior("전사")
mage = Mage("마법사")
archer = Archer("궁수")

characters = [warrior, mage, archer]

for char in characters:
    print(char.attack())

print()
print(warrior.shield_bash())
print(mage.fireball())
print(archer.multi_shot())

📝 오늘 배운 내용 정리

개념 설명 예시
상속 기존 클래스 확장 class Child(Parent):
부모 클래스 상속을 제공하는 클래스 Parent
자식 클래스 상속을 받는 클래스 Child
super() 부모 메서드 호출 super().__init__()
오버라이딩 부모 메서드 재정의 같은 이름으로 메서드 작성

상속 사용 시기

상속을 사용할 때:

  • “is-a” 관계 (Student is a Person)
  • 코드 재사용이 필요할 때
  • 공통 기능을 묶을 때

상속을 피해야 할 때:

  • “has-a” 관계 (Car has a Engine) → 컴포지션 사용
  • 단순히 코드 재사용만 목적일 때
  • 너무 깊은 상속 계층 (3단계 이상)

🔗 관련 자료

📚 이전 학습

Day 34: 클래스 변수와 클래스 메서드 ⭐⭐⭐

어제는 클래스 변수와 클래스 메서드, @classmethod와 @staticmethod 데코레이터를 배웠습니다!

📚 다음 학습

Day 36: 메서드 오버라이딩과 다중 상속 ⭐⭐⭐⭐

내일은 메서드 오버라이딩 심화, 다중 상속(Multiple Inheritance), MRO(Method Resolution Order), 믹스인(Mixin) 패턴을 배웁니다!


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

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