포스트

[이제와서 시작하는 Python 마스터하기 #8] 클래스와 객체지향 프로그래밍

[이제와서 시작하는 Python 마스터하기 #8] 클래스와 객체지향 프로그래밍

🏢 5분만에 만드는 회사 관리 시스템

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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
from datetime import datetime, date
from typing import List, Dict, Optional
import json

class Employee:
    """직원 클래스 - OOP 기본 개념 활용"""

    # 클래스 변수
    company_name = "파이썬 테크"
    total_employees = 0

    def __init__(self, name: str, department: str, position: str, salary: int):
        self.id = self._generate_id()
        self.name = name
        self.department = department
        self.position = position
        self._salary = salary  # 프라이빗 속성
        self.hire_date = date.today()
        self.projects = []

        Employee.total_employees += 1

    def _generate_id(self) -> str:
        """직원 ID 생성"""
        return f"EMP{Employee.total_employees + 1:04d}"

    @property
    def salary(self) -> int:
        """급여 getter"""
        return self._salary

    @salary.setter
    def salary(self, value: int):
        """급여 setter (검증 포함)"""
        if value < 0:
            raise ValueError("급여는 음수일 수 없습니다")
        self._salary = value

    def get_annual_salary(self) -> int:
        """연봉 계산"""
        return self._salary * 12

    def add_project(self, project_name: str):
        """프로젝트 추가"""
        self.projects.append({
            "name": project_name,
            "start_date": date.today().isoformat()
        })

    def __str__(self):
        return f"{self.name} ({self.position}, {self.department})"

    def __repr__(self):
        return f"Employee('{self.name}', '{self.department}', '{self.position}', {self._salary})"


class Manager(Employee):
    """매니저 클래스 - 상속 활용"""

    def __init__(self, name: str, department: str, salary: int):
        super().__init__(name, department, "Manager", salary)
        self.team_members: List[Employee] = []

    def add_team_member(self, employee: Employee):
        """팀원 추가"""
        if employee not in self.team_members:
            self.team_members.append(employee)
            print(f"{employee.name}님이 {self.name}의 팀에 추가되었습니다")

    def get_team_performance(self) -> Dict:
        """팀 성과 분석"""
        total_projects = sum(len(emp.projects) for emp in self.team_members)
        avg_salary = sum(emp.salary for emp in self.team_members) / len(self.team_members) if self.team_members else 0

        return {
            "team_size": len(self.team_members),
            "total_projects": total_projects,
            "average_salary": avg_salary,
            "manager": self.name
        }


class Company:
    """회사 클래스 - 종합 관리"""

    def __init__(self, name: str):
        self.name = name
        self.employees: List[Employee] = []
        self.departments: Dict[str, List[Employee]] = {}

    def hire(self, employee: Employee):
        """직원 채용"""
        self.employees.append(employee)

        # 부서별 분류
        if employee.department not in self.departments:
            self.departments[employee.department] = []
        self.departments[employee.department].append(employee)

        print(f"🎉 {employee.name}님이 {employee.department}팀에 입사했습니다!")

    def get_payroll(self) -> int:
        """전체 급여 총액"""
        return sum(emp.salary for emp in self.employees)

    def get_department_stats(self) -> Dict:
        """부서별 통계"""
        stats = {}
        for dept, employees in self.departments.items():
            stats[dept] = {
                "count": len(employees),
                "total_salary": sum(emp.salary for emp in employees),
                "positions": list(set(emp.position for emp in employees))
            }
        return stats

    def promote(self, employee: Employee, new_position: str, salary_increase: int):
        """승진 처리"""
        old_position = employee.position
        employee.position = new_position
        employee.salary += salary_increase

        print(f"📈 {employee.name}님이 {old_position}에서 {new_position}(으)로 승진했습니다!")
        print(f"   새 급여: {employee.salary:,}원/월")


# 사용 예제
def company_demo():
    """회사 관리 시스템 데모"""

    # 회사 생성
    company = Company("파이썬 테크")

    # 직원 채용
    emp1 = Employee("김철수", "개발팀", "Junior Developer", 3500000)
    emp2 = Employee("이영희", "개발팀", "Senior Developer", 5500000)
    emp3 = Employee("박민수", "마케팅팀", "Marketing Specialist", 4000000)

    company.hire(emp1)
    company.hire(emp2)
    company.hire(emp3)

    # 매니저 채용
    manager = Manager("정대리", "개발팀", 7000000)
    company.hire(manager)

    # 팀 구성
    manager.add_team_member(emp1)
    manager.add_team_member(emp2)

    # 프로젝트 할당
    emp1.add_project("웹사이트 리뉴얼")
    emp2.add_project("API 개발")
    emp2.add_project("데이터베이스 최적화")

    # 승진
    company.promote(emp1, "Mid-level Developer", 500000)

    # 통계 출력
    print(f"\n📊 회사 통계")
    print(f"총 직원 수: {Employee.total_employees}")
    print(f"월 급여 총액: {company.get_payroll():,}")
    print(f"\n부서별 현황:")
    for dept, stats in company.get_department_stats().items():
        print(f"  {dept}: {stats['count']}명, 급여총액: {stats['total_salary']:,}")

    # 팀 성과
    print(f"\n🏆 {manager.name} 팀 성과:")
    performance = manager.get_team_performance()
    for key, value in performance.items():
        print(f"  {key}: {value}")

# 실행
# company_demo()

🎭 객체지향 프로그래밍(OOP)이란?

객체지향 프로그래밍은 프로그램을 객체들의 모임으로 보고, 객체들 간의 상호작용으로 프로그램을 구성하는 프로그래밍 패러다임입니다.

graph TD
    A[객체지향 프로그래밍] --> B[캡슐화<br/>Encapsulation]
    A --> C[상속<br/>Inheritance]
    A --> D[다형성<br/>Polymorphism]
    A --> E[추상화<br/>Abstraction]
    
    B --> B1[데이터와 메서드를<br/>하나로 묶음]
    C --> C1[기존 클래스의<br/>기능 재사용]
    D --> D1[같은 인터페이스<br/>다른 동작]
    E --> E1[복잡함을 숨기고<br/>핵심만 노출]

📦 클래스와 객체

🎮 실전 예제: 게임 캐릭터 시스템

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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import random
from enum import Enum
from typing import Optional

class CharacterClass(Enum):
    """캐릭터 클래스 열거형"""
    WARRIOR = "전사"
    MAGE = "마법사"
    ARCHER = "궁수"
    HEALER = "힐러"

class GameCharacter:
    """게임 캐릭터 클래스"""

    # 클래스 변수
    max_level = 100
    base_hp = 100
    base_mp = 50

    def __init__(self, name: str, character_class: CharacterClass):
        # 인스턴스 변수
        self.name = name
        self.character_class = character_class
        self.level = 1
        self.exp = 0
        self.exp_to_next = 100

        # 클래스별 스탯 설정
        self._initialize_stats()
        self.current_hp = self.max_hp
        self.current_mp = self.max_mp

        # 인벤토리와 스킬
        self.inventory = []
        self.skills = self._get_initial_skills()
        self.gold = 100

    def _initialize_stats(self):
        """클래스별 초기 스탯 설정"""
        stats = {
            CharacterClass.WARRIOR: {"hp_mult": 1.5, "mp_mult": 0.5, "atk": 15, "def": 10},
            CharacterClass.MAGE: {"hp_mult": 0.7, "mp_mult": 2.0, "atk": 20, "def": 5},
            CharacterClass.ARCHER: {"hp_mult": 1.0, "mp_mult": 1.0, "atk": 12, "def": 7},
            CharacterClass.HEALER: {"hp_mult": 0.8, "mp_mult": 1.5, "atk": 8, "def": 8}
        }

        class_stats = stats[self.character_class]
        self.max_hp = int(self.base_hp * class_stats["hp_mult"])
        self.max_mp = int(self.base_mp * class_stats["mp_mult"])
        self.attack = class_stats["atk"]
        self.defense = class_stats["def"]

    def _get_initial_skills(self) -> List[str]:
        """클래스별 초기 스킬"""
        skills = {
            CharacterClass.WARRIOR: ["강타", "방어 태세"],
            CharacterClass.MAGE: ["파이어볼", "아이스 실드"],
            CharacterClass.ARCHER: ["더블 샷", "회피"],
            CharacterClass.HEALER: ["치유", "축복"]
        }
        return skills[self.character_class]

    def attack_monster(self, monster_name: str) -> Dict:
        """몬스터 공격"""
        damage = random.randint(self.attack - 2, self.attack + 5) * self.level
        crit = random.random() < 0.2  # 20% 크리티컬 확률

        if crit:
            damage *= 2
            print(f"💥 크리티컬! {self.name}이(가) {monster_name}에게 {damage} 피해를 입혔습니다!")
        else:
            print(f"⚔️ {self.name}이(가) {monster_name}에게 {damage} 피해를 입혔습니다.")

        # 경험치 획득
        exp_gained = random.randint(10, 30)
        self.gain_exp(exp_gained)

        return {"damage": damage, "critical": crit, "exp": exp_gained}

    def gain_exp(self, amount: int):
        """경험치 획득"""
        self.exp += amount
        print(f"{amount} 경험치 획득!")

        # 레벨업 체크
        while self.exp >= self.exp_to_next and self.level < self.max_level:
            self.level_up()

    def level_up(self):
        """레벨업"""
        self.level += 1
        self.exp -= self.exp_to_next
        self.exp_to_next = int(self.exp_to_next * 1.2)

        # 스탯 증가
        hp_increase = random.randint(10, 20)
        mp_increase = random.randint(5, 10)
        self.max_hp += hp_increase
        self.max_mp += mp_increase
        self.attack += random.randint(2, 4)
        self.defense += random.randint(1, 3)

        # 체력/마나 회복
        self.current_hp = self.max_hp
        self.current_mp = self.max_mp

        print(f"🎉 레벨업! {self.name}님이 레벨 {self.level}이(가) 되었습니다!")
        print(f"   HP +{hp_increase}, MP +{mp_increase}")

    def use_skill(self, skill_name: str) -> bool:
        """스킬 사용"""
        if skill_name not in self.skills:
            print(f"{skill_name} 스킬이 없습니다.")
            return False

        mp_cost = 10 * self.level
        if self.current_mp < mp_cost:
            print(f"❌ 마나가 부족합니다. (필요: {mp_cost}, 현재: {self.current_mp})")
            return False

        self.current_mp -= mp_cost
        print(f"🎯 {self.name}이(가) {skill_name}을(를) 사용했습니다! (MP -{mp_cost})")
        return True

    def add_item(self, item_name: str, quantity: int = 1):
        """아이템 추가"""
        self.inventory.append({"name": item_name, "quantity": quantity})
        print(f"📦 {item_name} x{quantity}을(를) 획득했습니다!")

    def show_stats(self):
        """캐릭터 정보 표시"""
        print(f"\n{'='*40}")
        print(f"🎮 {self.name} ({self.character_class.value})")
        print(f"{'='*40}")
        print(f"레벨: {self.level} (경험치: {self.exp}/{self.exp_to_next})")
        print(f"HP: {self.current_hp}/{self.max_hp}")
        print(f"MP: {self.current_mp}/{self.max_mp}")
        print(f"공격력: {self.attack} | 방어력: {self.defense}")
        print(f"골드: {self.gold}G")
        print(f"스킬: {', '.join(self.skills)}")
        print(f"인벤토리: {len(self.inventory)}개 아이템")
        print(f"{'='*40}")

# 사용 예제
def game_demo():
    """게임 캐릭터 시스템 데모"""

    # 캐릭터 생성
    warrior = GameCharacter("김전사", CharacterClass.WARRIOR)
    mage = GameCharacter("이마법사", CharacterClass.MAGE)

    # 전투 시뮬레이션
    print("⚔️ 전투 시작!\n")

    for round in range(1, 4):
        print(f"\n=== 라운드 {round} ===")

        # 전사 공격
        warrior.attack_monster("고블린")
        warrior.use_skill("강타")

        # 마법사 공격
        mage.attack_monster("슬라임")
        mage.use_skill("파이어볼")

    # 아이템 획득
    warrior.add_item("체력 포션", 3)
    mage.add_item("마나 포션", 2)

    # 캐릭터 정보 출력
    warrior.show_stats()
    mage.show_stats()

# 실행
# game_demo()

클래스 정의와 객체 생성

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
# 클래스 정의
class Person:
    """사람을 나타내는 클래스"""
    
    # 클래스 변수 (모든 인스턴스가 공유)
    species = "Homo sapiens"
    
    # 생성자 (초기화 메서드)
    def __init__(self, name, age):
        # 인스턴스 변수
        self.name = name
        self.age = age
    
    # 인스턴스 메서드
    def introduce(self):
        return f"안녕하세요, 저는 {self.name}이고 {self.age}살입니다."
    
    def have_birthday(self):
        """생일 - 나이 증가"""
        self.age += 1
        print(f"{self.name}님의 생일을 축하합니다! 이제 {self.age}살이 되었습니다.")

# 객체(인스턴스) 생성
person1 = Person("김철수", 25)
person2 = Person("이영희", 30)

# 메서드 호출
print(person1.introduce())
person1.have_birthday()

# 속성 접근
print(f"{person1.name}의 나이: {person1.age}")
print(f"종: {Person.species}")  # 클래스 변수는 클래스명으로도 접근 가능

속성과 메서드

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
class BankAccount:
    """은행 계좌 클래스"""
    
    # 클래스 변수
    bank_name = "파이썬 은행"
    account_count = 0
    
    def __init__(self, owner, initial_balance=0):
        # 인스턴스 변수
        self.owner = owner
        self.balance = initial_balance
        self._account_number = self._generate_account_number()
        self.__pin = None  # 프라이빗 변수 (네임 맹글링)
        
        # 클래스 변수 업데이트
        BankAccount.account_count += 1
    
    def _generate_account_number(self):
        """계좌번호 생성 (내부용 메서드)"""
        import random
        return f"PY{random.randint(10000000, 99999999)}"
    
    def deposit(self, amount):
        """입금"""
        if amount > 0:
            self.balance += amount
            print(f"{amount}원 입금 완료. 잔액: {self.balance}")
        else:
            print("입금액은 0보다 커야 합니다.")
    
    def withdraw(self, amount):
        """출금"""
        if amount <= 0:
            print("출금액은 0보다 커야 합니다.")
        elif amount > self.balance:
            print("잔액이 부족합니다.")
        else:
            self.balance -= amount
            print(f"{amount}원 출금 완료. 잔액: {self.balance}")
    
    def get_balance(self):
        """잔액 조회"""
        return self.balance
    
    def __str__(self):
        """객체의 문자열 표현"""
        return f"{self.owner}님의 계좌 (잔액: {self.balance}원)"
    
    def __repr__(self):
        """개발자를 위한 문자열 표현"""
        return f"BankAccount('{self.owner}', {self.balance})"

# 사용 예제
account = BankAccount("홍길동", 10000)
account.deposit(5000)
account.withdraw(3000)
print(account)  # __str__ 호출
print(repr(account))  # __repr__ 호출

[!TIP] __str__ vs __repr__ 차이점

  • __str__: 사용자를 위한 예쁜 출력 (print() 할 때 나옴)
  • __repr__: 개발자를 위한 엄격한 출력 (디버깅할 때 씀)

헷갈리면 일단 __str__만이라도 잘 만들어두세요! 디버깅이 훨씬 편해집니다.

🎨 Dataclass와 Pydantic으로 더 스마트한 클래스 만들기

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
from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime

# dataclass 기본 사용법
@dataclass
class Product:
    """제품 클래스 - dataclass 사용"""
    name: str
    price: float
    quantity: int = 0
    tags: List[str] = field(default_factory=list)

    def total_value(self) -> float:
        """재고 총 가치"""
        return self.price * self.quantity

    def add_tag(self, tag: str):
        """태그 추가"""
        if tag not in self.tags:
            self.tags.append(tag)

# 사용 예제
product = Product("노트북", 1500000, 10)
product.add_tag("전자제품")
product.add_tag("컴퓨터")
print(product)  # 자동으로 __repr__ 생성됨
print(f"총 가치: {product.total_value():,}")

# dataclass 고급 기능
@dataclass(frozen=True)  # 불변 객체
class Point3D:
    """3차원 좌표 - 불변 dataclass"""
    x: float
    y: float
    z: float

    def distance_from_origin(self) -> float:
        """원점으로부터의 거리"""
        return (self.x**2 + self.y**2 + self.z**2) ** 0.5

@dataclass(order=True)  # 비교 연산자 자동 생성
class Student:
    """학생 클래스 - 정렬 가능한 dataclass"""
    sort_index: float = field(init=False, repr=False)
    name: str
    grade: float
    student_id: str = field(compare=False)

    def __post_init__(self):
        """초기화 후 처리"""
        self.sort_index = self.grade

# 학생 정렬 예제
students = [
    Student("김철수", 4.2, "2024001"),
    Student("이영희", 4.5, "2024002"),
    Student("박민수", 3.8, "2024003"),
]

sorted_students = sorted(students, reverse=True)
for student in sorted_students:
    print(f"{student.name}: {student.grade}")

Pydantic으로 데이터 검증하기

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
# pip install pydantic
from pydantic import BaseModel, Field, validator
from typing import Optional, List
from datetime import datetime
from enum import Enum

class UserRole(str, Enum):
    """사용자 권한 열거형"""
    ADMIN = "admin"
    USER = "user"
    GUEST = "guest"

class User(BaseModel):
    """사용자 모델 - Pydantic으로 자동 검증"""
    username: str = Field(..., min_length=3, max_length=20, regex="^[a-zA-Z0-9_]+$")
    email: str = Field(..., regex=r"^[\w\.-]+@[\w\.-]+\.\w+$")
    age: int = Field(..., ge=0, le=150)  # 0 <= age <= 150
    role: UserRole = UserRole.USER
    is_active: bool = True
    created_at: datetime = Field(default_factory=datetime.now)
    bio: Optional[str] = Field(None, max_length=500)

    @validator('email')
    def validate_email(cls, v):
        """이메일 도메인 검증"""
        if not v.endswith('.com') and not v.endswith('.kr'):
            raise ValueError('이메일은 .com 또는 .kr 도메인이어야 합니다')
        return v.lower()

    @validator('username')
    def username_alphanumeric(cls, v):
        """사용자명 검증"""
        if v[0].isdigit():
            raise ValueError('사용자명은 숫자로 시작할 수 없습니다')
        return v

    class Config:
        """Pydantic 설정"""
        json_encoders = {
            datetime: lambda v: v.isoformat()
        }
        schema_extra = {
            "example": {
                "username": "john_doe",
                "email": "john@example.com",
                "age": 25,
                "role": "user",
                "bio": "Python 개발자입니다"
            }
        }

# 사용 예제
try:
    # 유효한 사용자
    user = User(
        username="python_lover",
        email="python@example.com",
        age=28
    )
    print(user.json(indent=2))

    # 유효하지 않은 사용자 (에러 발생)
    # invalid_user = User(
    #     username="123invalid",  # 숫자로 시작
    #     email="not_an_email",   # 잘못된 이메일 형식
    #     age=200                  # 범위 초과
    # )
except ValueError as e:
    print(f"검증 실패: {e}")

# API 응답 모델 예제
class BlogPost(BaseModel):
    """블로그 포스트 모델"""
    title: str = Field(..., min_length=1, max_length=200)
    content: str
    author: User
    tags: List[str] = []
    published: bool = False
    views: int = Field(default=0, ge=0)

    @validator('tags')
    def validate_tags(cls, v):
        """태그 검증 및 정규화"""
        return [tag.lower().strip() for tag in v if tag.strip()]

    def publish(self):
        """포스트 발행"""
        self.published = True
        return self

# 사용 예제
post = BlogPost(
    title="Python Dataclass vs Pydantic",
    content="두 라이브러리의 차이점을 알아봅시다...",
    author=user,
    tags=["Python", "OOP", "Validation"]
)

print(f"포스트 제목: {post.title}")
print(f"작성자: {post.author.username}")
print(f"태그: {post.tags}")

프로퍼티(Property)

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
"""온도 클래스 - 섭씨와 화씨 변환"""

def __init__(self, celsius=0):
    self._celsius = celsius

@property
def celsius(self):
    """섭씨 온도 getter"""
    return self._celsius

@celsius.setter
def celsius(self, value):
    """섭씨 온도 setter"""
    if value < -273.15:
        raise ValueError("절대영도(-273.15°C)보다 낮을 수 없습니다.")
    self._celsius = value

@property
def fahrenheit(self):
    """화씨 온도 getter"""
    return self._celsius * 9/5 + 32

@fahrenheit.setter
def fahrenheit(self, value):
    """화씨 온도 setter"""
    self.celsius = (value - 32) * 5/9

사용 예제

temp = Temperature() temp.celsius = 25 print(f”섭씨: {temp.celsius}°C”) print(f”화씨: {temp.fahrenheit}°F”)

temp.fahrenheit = 86 print(f”섭씨: {temp.celsius}°C”)

프로퍼티를 사용한 계산된 속성

class Circle: “"”원 클래스”””

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def __init__(self, radius):
    self._radius = radius

@property
def radius(self):
    return self._radius

@radius.setter
def radius(self, value):
    if value <= 0:
        raise ValueError("반지름은 양수여야 합니다.")
    self._radius = value

@property
def area(self):
    """원의 넓이 (읽기 전용)"""
    import math
    return math.pi * self._radius ** 2

@property
def circumference(self):
    """원의 둘레 (읽기 전용)"""
    import math
    return 2 * math.pi * self._radius

circle = Circle(5) print(f”반지름: {circle.radius}”) print(f”넓이: {circle.area:.2f}”) print(f”둘레: {circle.circumference:.2f}”)

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
## 🧬 상속(Inheritance)

### 기본 상속

```python
# 부모 클래스 (기반 클래스, 슈퍼 클래스)
class Animal:
    """동물 기반 클래스"""
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def speak(self):
        """소리내기 - 하위 클래스에서 구현"""
        raise NotImplementedError("하위 클래스에서 구현해야 합니다.")
    
    def move(self):
        print(f"{self.name}이(가) 움직입니다.")
    
    def info(self):
        return f"{self.name} ({self.age}살)"

# 자식 클래스 (파생 클래스, 서브 클래스)
class Dog(Animal):
    """개 클래스"""
    
    def __init__(self, name, age, breed):
        # 부모 클래스의 생성자 호출
        super().__init__(name, age)
        self.breed = breed
    
    def speak(self):
        """개의 짖기"""
        return f"{self.name}이(가) 멍멍 짖습니다!"
    
    def fetch(self):
        """개만의 특별한 행동"""
        return f"{self.name}이(가) 공을 가져옵니다!"

class Cat(Animal):
    """고양이 클래스"""
    
    def __init__(self, name, age, color):
        super().__init__(name, age)
        self.color = color
    
    def speak(self):
        """고양이의 울음"""
        return f"{self.name}이(가) 야옹 웁니다!"
    
    def scratch(self):
        """고양이만의 특별한 행동"""
        return f"{self.name}이(가) 긁적입니다!"

> [!WARNING]
> **상속을 남발하지 마세요!**
>
> 상속은 코드를 재사용하기 좋지만, 너무 깊게 상속하면 코드가 꼬입니다. (할아버지 -> 아버지 -> 나 -> 자식...)
> "A는 B이다(IS-A)" 관계가 확실할 때만 상속을 쓰세요.
> 단순히 기능을 가져다 쓰고 싶다면, 상속보다는 **포함(Composition)**을 쓰는 게 더 좋을 때가 많습니다.
# 사용 예제
dog = Dog("바둑이", 3, "진돗개")
cat = Cat("나비", 2, "검은색")

print(dog.info())  # 부모 클래스의 메서드
print(dog.speak())  # 오버라이딩된 메서드
print(dog.fetch())  # 자식 클래스의 메서드

print(cat.info())
print(cat.speak())
print(cat.scratch())

# isinstance() 확인
print(isinstance(dog, Dog))     # True
print(isinstance(dog, Animal))  # True (상속 관계)
print(isinstance(dog, Cat))     # False

다중 상속

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
# 믹스인(Mixin) 클래스들
class Flyable:
    """날 수 있는 능력"""
    
    def fly(self):
        return f"{self.name}이(가) 하늘을 날고 있습니다!"

class Swimmable:
    """수영할 수 있는 능력"""
    
    def swim(self):
        return f"{self.name}이(가) 물속을 헤엄치고 있습니다!"

# 다중 상속
class Duck(Animal, Flyable, Swimmable):
    """오리 클래스 - 다중 상속"""
    
    def __init__(self, name, age):
        super().__init__(name, age)
    
    def speak(self):
        return f"{self.name}이(가) 꽥꽥 웁니다!"

# 사용 예제
duck = Duck("도널드", 5)
print(duck.speak())  # Animal에서 상속
print(duck.fly())    # Flyable에서 상속
print(duck.swim())   # Swimmable에서 상속

# MRO (Method Resolution Order) 확인
print(Duck.__mro__)
# (<class '__main__.Duck'>, <class '__main__.Animal'>, 
#  <class '__main__.Flyable'>, <class '__main__.Swimmable'>, <class '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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from abc import ABC, abstractmethod

class Shape(ABC):
    """도형 추상 클래스"""
    
    def __init__(self, name):
        self.name = name
    
    @abstractmethod
    def area(self):
        """넓이 계산 - 하위 클래스에서 구현"""
        pass
    
    @abstractmethod
    def perimeter(self):
        """둘레 계산 - 하위 클래스에서 구현"""
        pass
    
    def describe(self):
        """도형 설명"""
        return f"{self.name}: 넓이={self.area():.2f}, 둘레={self.perimeter():.2f}"

class Rectangle(Shape):
    """사각형 클래스"""
    
    def __init__(self, width, height):
        super().__init__("사각형")
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    """원 클래스"""
    
    def __init__(self, radius):
        super().__init__("")
        self.radius = radius
    
    def area(self):
        import math
        return math.pi * self.radius ** 2
    
    def perimeter(self):
        import math
        return 2 * math.pi * self.radius

# 사용 예제
# shape = Shape("도형")  # TypeError: 추상 클래스는 인스턴스화할 수 없음

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

shapes = [rectangle, circle]
for shape in shapes:
    print(shape.describe())

🎯 다형성(Polymorphism)

메서드 오버라이딩

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 Employee:
    """직원 기반 클래스"""
    
    def __init__(self, name, base_salary):
        self.name = name
        self.base_salary = base_salary
    
    def calculate_salary(self):
        """급여 계산"""
        return self.base_salary
    
    def work(self):
        return f"{self.name}님이 일하고 있습니다."

class Manager(Employee):
    """관리자 클래스"""
    
    def __init__(self, name, base_salary, bonus):
        super().__init__(name, base_salary)
        self.bonus = bonus
    
    def calculate_salary(self):
        """관리자 급여 = 기본급 + 보너스"""
        return self.base_salary + self.bonus
    
    def work(self):
        return f"{self.name} 관리자님이 팀을 관리하고 있습니다."

class Developer(Employee):
    """개발자 클래스"""
    
    def __init__(self, name, base_salary, overtime_hours):
        super().__init__(name, base_salary)
        self.overtime_hours = overtime_hours
        self.overtime_rate = 50000  # 시간당 수당
    
    def calculate_salary(self):
        """개발자 급여 = 기본급 + 초과근무수당"""
        overtime_pay = self.overtime_hours * self.overtime_rate
        return self.base_salary + overtime_pay
    
    def work(self):
        return f"{self.name} 개발자님이 코딩하고 있습니다."

# 다형성 활용
employees = [
    Manager("김부장", 5000000, 1000000),
    Developer("이대리", 4000000, 20),
    Developer("박사원", 3500000, 30),
    Employee("최인턴", 2000000)
]

# 같은 메서드 호출, 다른 동작
for emp in employees:
    print(f"{emp.work()}")
    print(f"급여: {emp.calculate_salary():,}\n")

연산자 오버로딩

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
class Vector:
    """2D 벡터 클래스"""
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __add__(self, other):
        """벡터 덧셈 (+)"""
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        return NotImplemented
    
    def __sub__(self, other):
        """벡터 뺄셈 (-)"""
        if isinstance(other, Vector):
            return Vector(self.x - other.x, self.y - other.y)
        return NotImplemented
    
    def __mul__(self, scalar):
        """스칼라 곱셈 (*)"""
        if isinstance(scalar, (int, float)):
            return Vector(self.x * scalar, self.y * scalar)
        return NotImplemented
    
    def __rmul__(self, scalar):
        """역 스칼라 곱셈"""
        return self.__mul__(scalar)
    
    def __eq__(self, other):
        """동등 비교 (==)"""
        if isinstance(other, Vector):
            return self.x == other.x and self.y == other.y
        return False
    
    def __len__(self):
        """벡터의 크기 (len())"""
        import math
        return int(math.sqrt(self.x**2 + self.y**2))
    
    def __getitem__(self, index):
        """인덱싱 ([])"""
        if index == 0:
            return self.x
        elif index == 1:
            return self.y
        else:
            raise IndexError("Vector index out of range")
    
    def __bool__(self):
        """불리언 변환"""
        return self.x != 0 or self.y != 0

# 사용 예제
v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(f"v1: {v1}")
print(f"v2: {v2}")
print(f"v1 + v2: {v1 + v2}")
print(f"v1 - v2: {v1 - v2}")
print(f"v1 * 2: {v1 * 2}")
print(f"3 * v2: {3 * v2}")
print(f"v1 == v2: {v1 == v2}")
print(f"len(v1): {len(v1)}")
print(f"v1[0]: {v1[0]}, v1[1]: {v1[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
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
class DateUtils:
    """날짜 유틸리티 클래스"""
    
    date_format = "%Y-%m-%d"  # 클래스 변수
    
    def __init__(self, date_string):
        self.date = self._parse_date(date_string)
    
    def _parse_date(self, date_string):
        """날짜 파싱 (인스턴스 메서드)"""
        from datetime import datetime
        return datetime.strptime(date_string, self.date_format)
    
    @classmethod
    def from_timestamp(cls, timestamp):
        """타임스탬프로부터 객체 생성 (클래스 메서드)"""
        from datetime import datetime
        date_string = datetime.fromtimestamp(timestamp).strftime(cls.date_format)
        return cls(date_string)
    
    @classmethod
    def change_format(cls, new_format):
        """날짜 형식 변경 (클래스 메서드)"""
        cls.date_format = new_format
    
    @staticmethod
    def is_leap_year(year):
        """윤년 확인 (정적 메서드)"""
        return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
    
    @staticmethod
    def days_in_month(year, month):
        """월의 일수 반환 (정적 메서드)"""
        if month in [1, 3, 5, 7, 8, 10, 12]:
            return 31
        elif month in [4, 6, 9, 11]:
            return 30
        elif month == 2:
            return 29 if DateUtils.is_leap_year(year) else 28
        else:
            raise ValueError("Invalid month")
    
    def format_date(self, format_string=None):
        """날짜 포맷팅 (인스턴스 메서드)"""
        if format_string is None:
            format_string = self.date_format
        return self.date.strftime(format_string)

# 사용 예제
# 일반 생성자
date1 = DateUtils("2024-03-15")
print(date1.format_date())

# 클래스 메서드로 생성
import time
date2 = DateUtils.from_timestamp(time.time())
print(date2.format_date())

# 정적 메서드 사용
print(f"2024년은 윤년? {DateUtils.is_leap_year(2024)}")
print(f"2024년 2월은 {DateUtils.days_in_month(2024, 2)}")

# 클래스 변수 변경
DateUtils.change_format("%d/%m/%Y")
date3 = DateUtils("15/03/2024")
print(date3.format_date())

💡 실전 예제

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
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
from datetime import datetime, timedelta
from typing import List, Optional

class Book:
    """도서 클래스"""
    
    def __init__(self, isbn, title, author, published_year):
        self.isbn = isbn
        self.title = title
        self.author = author
        self.published_year = published_year
        self.is_available = True
        self.borrowed_by = None
        self.due_date = None
    
    def __str__(self):
        status = "대출가능" if self.is_available else f"대출중 (~{self.due_date})"
        return f"[{self.isbn}] {self.title} - {self.author} ({self.published_year}) [{status}]"

class Member:
    """회원 클래스"""
    
    def __init__(self, member_id, name, email):
        self.member_id = member_id
        self.name = name
        self.email = email
        self.borrowed_books = []
        self.borrow_history = []
    
    def __str__(self):
        return f"[{self.member_id}] {self.name} ({self.email}) - 대출: {len(self.borrowed_books)}"

class Library:
    """도서관 관리 시스템"""
    
    def __init__(self, name):
        self.name = name
        self.books = {}  # {isbn: Book}
        self.members = {}  # {member_id: Member}
        self.borrow_period = 14  # 대출 기간 (일)
    
    def add_book(self, book: Book):
        """도서 추가"""
        if book.isbn in self.books:
            print(f"이미 등록된 도서입니다: {book.isbn}")
            return False
        
        self.books[book.isbn] = book
        print(f"도서 추가 완료: {book.title}")
        return True
    
    def register_member(self, member: Member):
        """회원 등록"""
        if member.member_id in self.members:
            print(f"이미 등록된 회원입니다: {member.member_id}")
            return False
        
        self.members[member.member_id] = member
        print(f"회원 등록 완료: {member.name}")
        return True
    
    def borrow_book(self, isbn: str, member_id: str) -> bool:
        """도서 대출"""
        # 유효성 검사
        if isbn not in self.books:
            print(f"등록되지 않은 도서입니다: {isbn}")
            return False
        
        if member_id not in self.members:
            print(f"등록되지 않은 회원입니다: {member_id}")
            return False
        
        book = self.books[isbn]
        member = self.members[member_id]
        
        if not book.is_available:
            print(f"이미 대출중인 도서입니다: {book.title}")
            return False
        
        if len(member.borrowed_books) >= 5:
            print(f"대출 한도(5권)를 초과했습니다.")
            return False
        
        # 대출 처리
        book.is_available = False
        book.borrowed_by = member_id
        book.due_date = (datetime.now() + timedelta(days=self.borrow_period)).date()
        
        member.borrowed_books.append(isbn)
        member.borrow_history.append({
            "isbn": isbn,
            "borrow_date": datetime.now(),
            "status": "borrowed"
        })
        
        print(f"대출 완료: {book.title}{member.name} (반납일: {book.due_date})")
        return True
    
    def return_book(self, isbn: str, member_id: str) -> bool:
        """도서 반납"""
        if isbn not in self.books:
            print(f"등록되지 않은 도서입니다: {isbn}")
            return False
        
        book = self.books[isbn]
        
        if book.borrowed_by != member_id:
            print(f"해당 회원이 대출한 도서가 아닙니다.")
            return False
        
        member = self.members[member_id]
        
        # 반납 처리
        book.is_available = True
        book.borrowed_by = None
        book.due_date = None
        
        member.borrowed_books.remove(isbn)
        
        # 연체 확인
        is_overdue = datetime.now().date() > book.due_date
        
        # 기록 업데이트
        for record in member.borrow_history:
            if record["isbn"] == isbn and record["status"] == "borrowed":
                record["return_date"] = datetime.now()
                record["status"] = "returned"
                record["overdue"] = is_overdue
                break
        
        status = "연체 반납" if is_overdue else "정상 반납"
        print(f"{status}: {book.title}{member.name}")
        return True
    
    def search_books(self, keyword: str) -> List[Book]:
        """도서 검색"""
        results = []
        keyword_lower = keyword.lower()
        
        for book in self.books.values():
            if (keyword_lower in book.title.lower() or 
                keyword_lower in book.author.lower() or
                keyword == book.isbn):
                results.append(book)
        
        return results
    
    def get_overdue_books(self) -> List[tuple]:
        """연체 도서 목록"""
        overdue = []
        today = datetime.now().date()
        
        for book in self.books.values():
            if not book.is_available and book.due_date < today:
                member = self.members[book.borrowed_by]
                days_overdue = (today - book.due_date).days
                overdue.append((book, member, days_overdue))
        
        return overdue
    
    def member_report(self, member_id: str) -> str:
        """회원 대출 리포트"""
        if member_id not in self.members:
            return "등록되지 않은 회원입니다."
        
        member = self.members[member_id]
        report = f"\n=== {member.name}님의 대출 현황 ===\n"
        
        # 현재 대출 중인 도서
        report += "\n현재 대출 중:\n"
        if member.borrowed_books:
            for isbn in member.borrowed_books:
                book = self.books[isbn]
                report += f"  - {book.title} (반납일: {book.due_date})\n"
        else:
            report += "  없음\n"
        
        # 대출 이력
        report += "\n대출 이력:\n"
        for record in member.borrow_history[-5:]:  # 최근 5개
            book = self.books[record["isbn"]]
            status = "대출중" if record["status"] == "borrowed" else "반납완료"
            if record.get("overdue"):
                status += " (연체)"
            report += f"  - {book.title}: {record['borrow_date'].date()} [{status}]\n"
        
        return report

# 사용 예제
library = Library("파이썬 도서관")

# 도서 추가
books = [
    Book("978-89-1234-567-8", "파이썬 완벽 가이드", "김파이썬", 2023),
    Book("978-89-1234-568-5", "자료구조와 알고리즘", "이알고", 2022),
    Book("978-89-1234-569-2", "웹 개발 마스터", "박웹", 2024),
]

for book in books:
    library.add_book(book)

# 회원 등록
members = [
    Member("M001", "홍길동", "hong@email.com"),
    Member("M002", "김철수", "kim@email.com"),
]

for member in members:
    library.register_member(member)

# 도서 대출/반납
library.borrow_book("978-89-1234-567-8", "M001")
library.borrow_book("978-89-1234-568-5", "M001")

# 도서 검색
results = library.search_books("파이썬")
print("\n검색 결과:")
for book in results:
    print(f"  {book}")

# 회원 리포트
print(library.member_report("M001"))

2. 게임 캐릭터 시스템

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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
from abc import ABC, abstractmethod
import random

class Character(ABC):
    """게임 캐릭터 추상 클래스"""
    
    def __init__(self, name, hp, attack, defense):
        self.name = name
        self.max_hp = hp
        self.hp = hp
        self.attack = attack
        self.defense = defense
        self.level = 1
        self.exp = 0
        self.skills = []
    
    @abstractmethod
    def special_ability(self, target):
        """특수 능력 - 하위 클래스에서 구현"""
        pass
    
    def take_damage(self, damage):
        """데미지 받기"""
        actual_damage = max(1, damage - self.defense)
        self.hp -= actual_damage
        print(f"{self.name}이(가) {actual_damage}의 데미지를 받았습니다! (HP: {self.hp}/{self.max_hp})")
        
        if self.hp <= 0:
            self.hp = 0
            print(f"{self.name}이(가) 쓰러졌습니다!")
            return False
        return True
    
    def basic_attack(self, target):
        """기본 공격"""
        damage = self.attack + random.randint(-5, 5)
        print(f"{self.name}의 기본 공격!")
        target.take_damage(damage)
    
    def heal(self, amount):
        """체력 회복"""
        old_hp = self.hp
        self.hp = min(self.max_hp, self.hp + amount)
        healed = self.hp - old_hp
        print(f"{self.name}이(가) {healed}만큼 회복했습니다! (HP: {self.hp}/{self.max_hp})")
    
    def gain_exp(self, amount):
        """경험치 획득"""
        self.exp += amount
        print(f"{self.name}이(가) {amount} 경험치를 획득했습니다!")
        
        # 레벨업 체크
        while self.exp >= self.level * 100:
            self.exp -= self.level * 100
            self.level_up()
    
    def level_up(self):
        """레벨업"""
        self.level += 1
        self.max_hp += 20
        self.hp = self.max_hp
        self.attack += 5
        self.defense += 3
        print(f"🎉 {self.name}이(가) 레벨 {self.level}로 레벨업했습니다!")
    
    def __str__(self):
        return (f"{self.name} (Lv.{self.level})\n"
                f"HP: {self.hp}/{self.max_hp}\n"
                f"공격력: {self.attack}, 방어력: {self.defense}")

class Warrior(Character):
    """전사 클래스"""
    
    def __init__(self, name):
        super().__init__(name, hp=150, attack=25, defense=15)
        self.rage = 0
        self.skills = ["강타", "방어태세", "광폭화"]
    
    def special_ability(self, target):
        """전사의 특수 능력: 강력한 일격"""
        print(f"{self.name}의 필살기: 강력한 일격!")
        damage = self.attack * 2 + self.rage
        target.take_damage(damage)
        self.rage = 0  # 분노 초기화
    
    def basic_attack(self, target):
        """전사의 기본 공격 - 분노 축적"""
        super().basic_attack(target)
        self.rage += 10
        print(f"분노 게이지: {self.rage}")

class Mage(Character):
    """마법사 클래스"""
    
    def __init__(self, name):
        super().__init__(name, hp=80, attack=35, defense=5)
        self.mana = 100
        self.max_mana = 100
        self.skills = ["파이어볼", "아이스 쉴드", "메테오"]
    
    def special_ability(self, target):
        """마법사의 특수 능력: 메테오"""
        if self.mana >= 50:
            print(f"{self.name}의 필살기: 메테오!")
            damage = self.attack * 3
            target.take_damage(damage)
            self.mana -= 50
            print(f"마나: {self.mana}/{self.max_mana}")
        else:
            print("마나가 부족합니다!")
    
    def cast_spell(self, spell_name, target):
        """주문 시전"""
        spells = {
            "파이어볼": (30, 20),  # (마나 소모, 데미지)
            "아이스볼트": (20, 15),
            "라이트닝": (40, 35)
        }
        
        if spell_name in spells:
            mana_cost, damage = spells[spell_name]
            if self.mana >= mana_cost:
                print(f"{self.name}이(가) {spell_name}을 시전합니다!")
                target.take_damage(damage + self.attack)
                self.mana -= mana_cost
                print(f"마나: {self.mana}/{self.max_mana}")
            else:
                print("마나가 부족합니다!")
        else:
            print("알 수 없는 주문입니다!")

class Healer(Character):
    """힐러 클래스"""
    
    def __init__(self, name):
        super().__init__(name, hp=100, attack=15, defense=10)
        self.mana = 150
        self.max_mana = 150
        self.skills = ["치유", "정화", "부활"]
    
    def special_ability(self, target):
        """힐러의 특수 능력: 대규모 치유"""
        if self.mana >= 40:
            print(f"{self.name}의 특수 능력: 신성한 치유!")
            heal_amount = 50
            target.heal(heal_amount)
            self.mana -= 40
            print(f"마나: {self.mana}/{self.max_mana}")
        else:
            print("마나가 부족합니다!")
    
    def heal_spell(self, target, spell_type="normal"):
        """치유 주문"""
        heal_types = {
            "normal": (20, 30),  # (마나 소모, 치유량)
            "greater": (40, 60),
            "emergency": (60, 100)
        }
        
        if spell_type in heal_types:
            mana_cost, heal_amount = heal_types[spell_type]
            if self.mana >= mana_cost:
                print(f"{self.name}이(가) {spell_type} 치유를 시전합니다!")
                target.heal(heal_amount)
                self.mana -= mana_cost
                print(f"마나: {self.mana}/{self.max_mana}")
            else:
                print("마나가 부족합니다!")

# 전투 시뮬레이션
def battle_simulation():
    """전투 시뮬레이션"""
    # 캐릭터 생성
    warrior = Warrior("강철의 전사")
    mage = Mage("대마법사")
    healer = Healer("신성한 치유사")
    
    # 적 캐릭터 (간단한 구현)
    class Monster(Character):
        def __init__(self, name, hp, attack, defense):
            super().__init__(name, hp, attack, defense)
        
        def special_ability(self, target):
            print(f"{self.name}의 강력한 공격!")
            target.take_damage(self.attack * 1.5)
    
    dragon = Monster("고대 드래곤", 300, 40, 20)
    
    print("=== 전투 시작! ===")
    print(f"\n플레이어 팀: {warrior.name}, {mage.name}, {healer.name}")
    print(f"적: {dragon.name}\n")
    
    # 전투 라운드
    round_num = 1
    while dragon.hp > 0 and any(char.hp > 0 for char in [warrior, mage, healer]):
        print(f"\n--- 라운드 {round_num} ---")
        
        # 플레이어 턴
        if warrior.hp > 0:
            if round_num % 3 == 0:
                warrior.special_ability(dragon)
            else:
                warrior.basic_attack(dragon)
        
        if dragon.hp <= 0:
            break
            
        if mage.hp > 0:
            if round_num % 2 == 0:
                mage.special_ability(dragon)
            else:
                mage.cast_spell("파이어볼", dragon)
        
        if dragon.hp <= 0:
            break
            
        if healer.hp > 0:
            # 체력이 가장 낮은 아군 치유
            allies = [warrior, mage, healer]
            injured = min(allies, key=lambda x: x.hp if x.hp > 0 else float('inf'))
            if injured.hp < injured.max_hp * 0.5:
                healer.heal_spell(injured, "greater")
            else:
                healer.basic_attack(dragon)
        
        if dragon.hp <= 0:
            break
        
        # 드래곤 턴
        print(f"\n{dragon.name}의 차례!")
        targets = [char for char in [warrior, mage, healer] if char.hp > 0]
        if targets:
            target = random.choice(targets)
            if round_num % 4 == 0:
                dragon.special_ability(target)
            else:
                dragon.basic_attack(target)
        
        round_num += 1
    
    # 전투 결과
    print("\n=== 전투 종료! ===")
    if dragon.hp <= 0:
        print("🎉 승리! 드래곤을 물리쳤습니다!")
        for char in [warrior, mage, healer]:
            if char.hp > 0:
                char.gain_exp(200)
    else:
        print("💀 패배... 파티가 전멸했습니다.")

# 실행
# battle_simulation()

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
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
from datetime import datetime
from typing import List, Optional
import uuid

class Transaction:
    """거래 내역 클래스"""
    
    def __init__(self, transaction_type, amount, balance_after, description=""):
        self.id = str(uuid.uuid4())
        self.timestamp = datetime.now()
        self.type = transaction_type  # "deposit", "withdrawal", "transfer"
        self.amount = amount
        self.balance_after = balance_after
        self.description = description
    
    def __str__(self):
        return (f"[{self.timestamp.strftime('%Y-%m-%d %H:%M')}] "
                f"{self.type}: {self.amount:,}원 | 잔액: {self.balance_after:,}")

class Account(ABC):
    """계좌 추상 클래스"""
    
    def __init__(self, account_number, owner_name, initial_balance=0):
        self.account_number = account_number
        self.owner_name = owner_name
        self._balance = initial_balance
        self.transactions = []
        self.is_active = True
        
        if initial_balance > 0:
            self._add_transaction("deposit", initial_balance, initial_balance, "초기 입금")
    
    @property
    def balance(self):
        return self._balance
    
    def _add_transaction(self, transaction_type, amount, balance_after, description=""):
        """거래 내역 추가"""
        transaction = Transaction(transaction_type, amount, balance_after, description)
        self.transactions.append(transaction)
    
    @abstractmethod
    def withdraw(self, amount):
        """출금 - 하위 클래스에서 구현"""
        pass
    
    def deposit(self, amount):
        """입금"""
        if not self.is_active:
            raise ValueError("비활성화된 계좌입니다.")
        
        if amount <= 0:
            raise ValueError("입금액은 0보다 커야 합니다.")
        
        self._balance += amount
        self._add_transaction("deposit", amount, self._balance)
        return self._balance
    
    def get_transaction_history(self, limit=10):
        """거래 내역 조회"""
        return self.transactions[-limit:]
    
    def __str__(self):
        status = "활성" if self.is_active else "비활성"
        return f"{self.__class__.__name__}({self.account_number}) - {self.owner_name}: {self._balance:,}원 [{status}]"

class SavingsAccount(Account):
    """저축 계좌"""
    
    def __init__(self, account_number, owner_name, initial_balance=0):
        super().__init__(account_number, owner_name, initial_balance)
        self.interest_rate = 0.02  # 연 2%
        self.withdrawal_limit = 3  # 월 출금 제한
        self.monthly_withdrawals = 0
        self.last_withdrawal_month = None
    
    def withdraw(self, amount):
        """출금 (월 3회 제한)"""
        if not self.is_active:
            raise ValueError("비활성화된 계좌입니다.")
        
        current_month = datetime.now().month
        
        # 월이 바뀌면 출금 횟수 초기화
        if self.last_withdrawal_month != current_month:
            self.monthly_withdrawals = 0
            self.last_withdrawal_month = current_month
        
        if self.monthly_withdrawals >= self.withdrawal_limit:
            raise ValueError(f"월 출금 한도({self.withdrawal_limit}회)를 초과했습니다.")
        
        if amount <= 0:
            raise ValueError("출금액은 0보다 커야 합니다.")
        
        if amount > self._balance:
            raise ValueError("잔액이 부족합니다.")
        
        self._balance -= amount
        self.monthly_withdrawals += 1
        self._add_transaction("withdrawal", amount, self._balance)
        return self._balance
    
    def calculate_interest(self):
        """이자 계산 및 지급"""
        interest = self._balance * self.interest_rate
        self._balance += interest
        self._add_transaction("deposit", interest, self._balance, "이자 지급")
        return interest

class CheckingAccount(Account):
    """당좌 계좌"""
    
    def __init__(self, account_number, owner_name, initial_balance=0, overdraft_limit=0):
        super().__init__(account_number, owner_name, initial_balance)
        self.overdraft_limit = overdraft_limit  # 마이너스 한도
    
    def withdraw(self, amount):
        """출금 (마이너스 통장 기능)"""
        if not self.is_active:
            raise ValueError("비활성화된 계좌입니다.")
        
        if amount <= 0:
            raise ValueError("출금액은 0보다 커야 합니다.")
        
        if self._balance - amount < -self.overdraft_limit:
            raise ValueError("마이너스 한도를 초과합니다.")
        
        self._balance -= amount
        self._add_transaction("withdrawal", amount, self._balance)
        return self._balance

class Bank:
    """은행 시스템"""
    
    def __init__(self, name):
        self.name = name
        self.accounts = {}  # {account_number: Account}
        self._next_account_number = 1000000
    
    def _generate_account_number(self):
        """계좌번호 생성"""
        account_number = f"{self._next_account_number:07d}"
        self._next_account_number += 1
        return account_number
    
    def create_savings_account(self, owner_name, initial_balance=0):
        """저축 계좌 개설"""
        account_number = self._generate_account_number()
        account = SavingsAccount(account_number, owner_name, initial_balance)
        self.accounts[account_number] = account
        print(f"저축 계좌 개설 완료: {account}")
        return account
    
    def create_checking_account(self, owner_name, initial_balance=0, overdraft_limit=0):
        """당좌 계좌 개설"""
        account_number = self._generate_account_number()
        account = CheckingAccount(account_number, owner_name, initial_balance, overdraft_limit)
        self.accounts[account_number] = account
        print(f"당좌 계좌 개설 완료: {account}")
        return account
    
    def find_account(self, account_number) -> Optional[Account]:
        """계좌 조회"""
        return self.accounts.get(account_number)
    
    def transfer(self, from_account_number, to_account_number, amount):
        """계좌 이체"""
        from_account = self.find_account(from_account_number)
        to_account = self.find_account(to_account_number)
        
        if not from_account or not to_account:
            raise ValueError("유효하지 않은 계좌번호입니다.")
        
        # 출금
        from_account.withdraw(amount)
        from_account._add_transaction("transfer", amount, from_account.balance, 
                                    f"이체: {to_account_number}")
        
        # 입금
        to_account.deposit(amount)
        to_account._add_transaction("transfer", amount, to_account.balance, 
                                   f"이체: {from_account_number}")
        
        print(f"이체 완료: {from_account_number}{to_account_number}: {amount:,}")
    
    def get_total_deposits(self):
        """전체 예금액"""
        total = sum(account.balance for account in self.accounts.values() 
                   if account.balance > 0)
        return total
    
    def get_account_summary(self):
        """계좌 요약"""
        summary = f"\n=== {self.name} 계좌 현황 ===\n"
        summary += f"총 계좌 수: {len(self.accounts)}\n"
        summary += f"총 예금액: {self.get_total_deposits():,}\n\n"
        
        for account in self.accounts.values():
            summary += f"{account}\n"
        
        return summary

# 사용 예제
bank = Bank("파이썬 은행")

# 계좌 개설
savings1 = bank.create_savings_account("김철수", 1000000)
savings2 = bank.create_savings_account("이영희", 500000)
checking = bank.create_checking_account("박민수", 200000, overdraft_limit=500000)

# 거래 실행
savings1.deposit(200000)
savings1.withdraw(50000)

checking.withdraw(300000)  # 마이너스 통장 사용

# 이체
bank.transfer(savings1.account_number, checking.account_number, 100000)

# 이자 계산
interest = savings1.calculate_interest()
print(f"이자 지급: {interest:,.0f}")

# 거래 내역 조회
print(f"\n{savings1.owner_name}님의 거래 내역:")
for transaction in savings1.get_transaction_history():
    print(f"  {transaction}")

# 전체 계좌 요약
print(bank.get_account_summary())

⚠️ 초보자가 자주 하는 실수

1. 클래스 변수와 인스턴스 변수 혼동

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ 클래스 변수를 인스턴스에서 수정
class Counter:
    count = 0  # 클래스 변수

    def increment(self):
        self.count += 1  # 새로운 인스턴스 변수 생성!

# ✅ 클래스 변수 올바른 수정
class Counter:
    count = 0

    def increment(self):
        Counter.count += 1  # 클래스명으로 접근

2. init 메서드 실수

1
2
3
4
5
6
7
8
9
10
11
# ❌ return 값 반환
class Person:
    def __init__(self, name):
        self.name = name
        return self  # TypeError!

# ✅ __init__은 None 반환
class Person:
    def __init__(self, name):
        self.name = name
        # return 없이 자동으로 객체 반환

3. 가변 기본값 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ❌ 가변 객체를 기본값으로 사용
class Student:
    def __init__(self, name, grades=[]):  # 위험!
        self.name = name
        self.grades = grades

# 모든 인스턴스가 같은 리스트를 공유!
s1 = Student("A")
s2 = Student("B")
s1.grades.append(90)
print(s2.grades)  # [90] - 예상과 다름!

# ✅ None을 기본값으로 사용
class Student:
    def __init__(self, name, grades=None):
        self.name = name
        self.grades = grades if grades is not None else []

4. super() 사용 실수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ❌ 부모 클래스 생성자 호출 안 함
class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        # super().__init__(name) 빠짐!
        self.breed = breed  # self.name이 없음

# ✅ super() 올바른 사용
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

5. private 속성 오해

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ Python에는 진짜 private이 없음
class BankAccount:
    def __init__(self):
        self.__balance = 0  # 네임 맹글링일 뿐

# 여전히 접근 가능
account = BankAccount()
print(account._BankAccount__balance)  # 0

# ✅ 컨벤션으로 표시
class BankAccount:
    def __init__(self):
        self._balance = 0  # _ 하나는 "내부용" 표시

🎯 핵심 정리

OOP 설계 원칙 (SOLID)

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
# 1. 단일 책임 원칙 (SRP)
# 좋음: 각 클래스가 하나의 책임만 가짐
class EmailSender:
    def send_email(self, message):
        # 이메일 전송 로직
        pass

class EmailValidator:
    def validate_email(self, email):
        # 이메일 유효성 검사
        pass

# 2. 개방-폐쇄 원칙 (OCP)
# 확장에는 열려있고, 수정에는 닫혀있어야 함
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

# 새로운 도형 추가 시 기존 코드 수정 없이 확장
class Triangle(Shape):
    def area(self):
        # 삼각형 넓이 계산
        pass

# 3. 리스코프 치환 원칙 (LSP)
# 하위 타입은 상위 타입을 대체할 수 있어야 함
def process_bird(bird: Bird):
    bird.fly()  # 모든 새가 날 수 있다고 가정하면 안됨

# 4. 인터페이스 분리 원칙 (ISP)
# 클라이언트가 사용하지 않는 메서드에 의존하지 않아야 함
class Workable(ABC):
    @abstractmethod
    def work(self):
        pass

class Eatable(ABC):
    @abstractmethod
    def eat(self):
        pass

# 5. 의존성 역전 원칙 (DIP)
# 고수준 모듈은 저수준 모듈에 의존하면 안됨
class Database(ABC):
    @abstractmethod
    def save(self, data):
        pass

class MySQLDatabase(Database):
    def save(self, data):
        # MySQL 저장 로직
        pass

클래스 설계 Best Practices

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 적절한 추상화 레벨
class Animal:  # 너무 추상적이지도, 구체적이지도 않게
    pass

# 2. 명확한 이름
class UserAuthenticationService:  # 역할이 명확한 이름
    pass

# 3. 적절한 캡슐화
class BankAccount:
    def __init__(self):
        self._balance = 0  # protected
        self.__pin = 1234  # private
    
    @property
    def balance(self):
        return self._balance

# 4. 컴포지션 활용
class Car:
    def __init__(self):
        self.engine = Engine()  # Has-A 관계
        self.wheels = [Wheel() for _ in range(4)]

언제 OOP를 사용할까?

graph TD
    A[OOP 사용 시기] --> B[복잡한 상태 관리]
    A --> C[코드 재사용성]
    A --> D[확장 가능성]
    A --> E[팀 협업]
    
    F[함수형 사용 시기] --> G[단순한 데이터 변환]
    F --> H[상태가 없는 연산]
    F --> I[병렬 처리]

🎓 파이썬 마스터하기 시리즈

📚 기초편 (1-7)

  1. Python 소개와 개발 환경 설정 완벽 가이드
  2. 변수, 자료형, 연산자 완벽 정리
  3. 조건문과 반복문 마스터하기
  4. 함수와 람다 완벽 가이드
  5. 리스트, 튜플, 딕셔너리 정복하기
  6. 문자열 처리와 정규표현식
  7. 파일 입출력과 예외 처리

🚀 중급편 (8-12)

  1. 클래스와 객체지향 프로그래밍 ← 현재 글
  2. 모듈과 패키지 관리
  3. 데코레이터와 제너레이터
  4. 비동기 프로그래밍 (async/await)
  5. 데이터베이스 연동하기

💼 고급편 (13-16)

  1. 웹 스크래핑과 API 활용
  2. 테스트와 디버깅 전략
  3. 성능 최적화 기법
  4. 멀티프로세싱과 병렬 처리

이전글: 파일 입출력과 예외 처리 ⬅️ 현재글: 클래스와 객체지향 프로그래밍 다음글: 모듈과 패키지 관리 ➡️


이번 포스트에서는 Python의 객체지향 프로그래밍을 완벽히 마스터했습니다. 다음 포스트에서는 모듈과 패키지를 관리하는 방법에 대해 알아보겠습니다. Happy Coding! 🐍✨

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.