# Part 4: 객체 지향 프로그래밍 (OOP)의 이해 --- ### 💡 지난 시간 복습 [Part 3: 데이터 관리를 위한 파이썬 컬렉션](part_3_python_collections.md)에서는 리스트, 튜플, 딕셔너리, 셋의 특징과 사용법을 익혔습니다. 이를 통해 여러 개의 데이터를 목적에 맞게 효율적으로 저장하고 관리하는 방법을 배웠습니다. --- 파이썬은 모든 것이 객체(Object)로 이루어진 강력한 객체 지향 프로그래밍(OOP) 언어입니다. Scikit-learn, PyTorch, TensorFlow 등 우리가 사용하는 대부분의 AI 라이브러리는 OOP 원칙에 기반하여 설계되었습니다. 클래스(Class)와 객체(Object)의 개념을 이해하면 라이브러리의 내부 구조를 파악하고 더 깊이 있게 활용할 수 있으며, 나아가 재사용과 확장이 용이한 모듈형 코드를 직접 작성할 수 있게 됩니다. 이 섹션에서는 OOP의 핵심 개념을 실용적인 예제와 함께 단계별로 학습합니다. --- ## 1. 클래스(Class)와 객체(Object) - **클래스(Class)**: 객체를 만들기 위한 '설계도' 또는 '틀'입니다. 객체가 가지게 될 속성(데이터)과 행동(메서드)을 정의합니다. 예를 들어, '자동차'라는 클래스는 `색상`, `최대 속도`와 같은 속성과 `전진()`, `정지()`와 같은 행동을 가질 수 있습니다. - **객체(Object)** 또는 **인스턴스(Instance)**: 클래스로부터 생성된 실체를 의미합니다. '자동차' 클래스로부터 '빨간색 페라리', '검은색 아반떼'와 같은 구체적인 객체들을 만들어낼 수 있습니다. 각 객체는 클래스에서 정의한 속성과 행동을 가지지만, 속성의 값(예: 색상)은 객체마다 다를 수 있습니다.
✨ 클래스, 객체, 인스턴스 관계 다이어그램 ```mermaid graph TD A["Dog 클래스 (설계도)

속성:
- species (모든 객체 공유)
- name, age, breed (객체별 고유값)

메서드:
- describe()
- speak()"] B["bory 객체 (인스턴스)

name: '보리'
age: 3
breed: '푸들'"] C["tory 객체 (인스턴스)

name: '토리'
age: 5
breed: '진돗개'"] A -- "로부터 생성" --> B A -- "로부터 생성" --> C style A fill:#f9f9f9,stroke:#333,stroke-width:2px style B fill:#e8f4ff,stroke:#333,stroke-width:2px style C fill:#e8f4ff,stroke:#333,stroke-width:2px ```
### 예제: '강아지' 클래스와 객체 만들기 ```python # 'Dog'라는 이름의 클래스를 정의합니다. class Dog: # 클래스 속성(Class Attribute): # 이 클래스로부터 생성되는 모든 객체가 공유하는 속성입니다. species = "Canis familiaris" # 초기화 메서드 (__init__): 객체가 생성될 때(인스턴스화될 때) 자동으로 호출됩니다. # self는 생성되는 객체 자기 자신을 가리킵니다. def __init__(self, name: str, age: int, breed: str): # 인스턴스 속성(Instance Attribute): # 각 객체마다 고유하게 가지는 속성입니다. print(f"{name} 강아지 객체를 생성합니다.") self.name = name self.age = age self.breed = breed # 인스턴스 메서드(Instance Method): # 객체가 수행할 수 있는 행동을 정의합니다. # 첫 번째 인자로는 항상 self를 받습니다. def describe(self) -> str: """강아지의 정보를 문자열로 반환합니다.""" return f"{self.name}는 {self.breed} 품종이며, {self.age}살 입니다." def speak(self, sound: str): """강아지가 짖는 소리를 출력합니다.""" return f"{self.name}가 '{sound}' 하고 짖습니다." # --- 클래스 사용하기 --- # 'Dog' 클래스로부터 'bory'와 'tory'라는 두 개의 객체(인스턴스)를 생성합니다. bory = Dog("보리", 3, "푸들") tory = Dog("토리", 5, "진돗개") # 각 객체의 메서드를 호출합니다. print(bory.describe()) print(tory.describe()) # 각 객체의 속성에 접근합니다. print(f"{bory.name}의 나이는 {bory.age}살입니다.") print(f"{tory.name}의 나이는 {tory.age}살입니다.") # 클래스 속성은 모든 객체가 공유합니다. print(f"{bory.name}의 종은 {bory.species}입니다.") print(f"{tory.name}의 종은 {tory.species}입니다.") ``` > 🚫 **Common Pitfall: `self`는 대체 무엇인가?** > OOP를 처음 접할 때 가장 혼란스러운 개념 중 하나가 바로 `self`입니다. > - **`self`는 예약어가 아닙니다**: `self`는 관례적으로 사용하는 이름일 뿐, `me`, `this` 등 다른 이름으로 바꿔도 동작합니다. 하지만 모든 파이썬 개발자가 따르는 약속이므로 `self`를 사용하는 것이 좋습니다. > - **`self`는 인스턴스 자기 자신입니다**: 클래스의 메서드를 호출할 때, 파이썬은 그 메서드를 호출한 객체 자신을 첫 번째 인자로 몰래(?) 전달합니다. > ```python > # bory.describe()는 내부적으로 이렇게 동작합니다: > # Dog.describe(bory) > ``` > 따라서 `describe(self)` 메서드의 `self` 매개변수에는 `bory` 객체가 전달되는 것입니다. `self.name`은 곧 `bory.name`과 같습니다. > - **메서드를 정의할 때 `self`를 빼먹는 실수**: `def describe():` 와 같이 `self`를 빼먹으면, 파이썬이 `bory` 객체를 첫 인자로 전달할 곳이 없으므로 `TypeError: describe() takes 0 positional arguments but 1 was given` 와 같은 에러가 발생합니다. 인스턴스의 속성이나 다른 메서드를 사용하려면, 메서드의 첫 번째 인자로는 반드시 `self`를 포함해야 합니다. > 💡 **Tip: `__init__`과 `__repr__`을 자동으로? `dataclasses`** > 위 `Dog` 클래스처럼 `__init__` 메서드에서 `self.name = name`과 같이 속성을 초기화하는 코드는 매우 흔하지만, 속성이 많아지면 반복적이고 길어집니다. 파이썬 3.7부터 도입된 `dataclasses` 모듈을 사용하면 이 과정을 자동으로 처리할 수 있습니다. > > ```python > from dataclasses import dataclass > > @dataclass > class DogData: > # 이렇게 타입 힌트와 함께 속성을 선언하기만 하면... > name: str > age: int > breed: str > # __init__ 메서드가 자동으로 생성됩니다! > > # 일반 메서드도 똑같이 추가할 수 있습니다. > def describe(self) -> str: > return f"{self.name}는 {self.breed} 품종이며, {self.age}살 입니다." > > # 사용할 때는 일반 클래스와 완전히 동일합니다. > bory_data = DogData("보리", 3, "푸들") > print(bory_data) # 객체를 출력하면 자동으로 생성된 __repr__ 덕분에 보기 좋게 나옵니다. > # 출력: DogData(name='보리', age=3, breed='푸들') > print(bory_data.describe()) > ``` > `@dataclass` 데코레이터를 클래스 위에 붙여주기만 하면, `__init__`, `__repr__`(객체 출력 형식), `__eq__`(객체 비교) 등 유용한 메서드들을 자동으로 생성해줍니다. 주로 데이터를 담는 용도의 클래스를 만들 때 사용하면 코드를 매우 간결하게 유지할 수 있습니다. --- ## 2. OOP의 4대 핵심 원칙 ### 2.1. 상속 (Inheritance) 부모 클래스(Superclass)의 속성과 메서드를 자식 클래스(Subclass)가 물려받아 재사용하는 것입니다. 코드의 중복을 줄이고 논리적인 계층 구조를 만들 수 있습니다. #### 예제: `Dog` 클래스를 상속받는 `GoldenRetriever` 클래스 ```python # 위에서 정의한 Dog 클래스를 부모 클래스로 사용합니다. class GoldenRetriever(Dog): # Dog 클래스를 상속받음 # 자식 클래스에서 새로운 메서드를 추가할 수 있습니다. def fetch(self, item: str) -> str: return f"{self.name}가 {item}을 물어왔습니다!" # 부모의 메서드를 재정의(Method Overriding)할 수도 있습니다. def speak(self, sound="월! 월!"): # 골든 리트리버는 다르게 짖도록 재정의 return f"{self.name}가 행복하게 '{sound}' 하고 짖습니다." # 자식 클래스로 객체 생성 happy = GoldenRetriever("해피", 2, "골든 리트리버") # 부모 클래스(Dog)의 메서드를 그대로 사용 가능 print(happy.describe()) # 자식 클래스(GoldenRetriever)에서 추가한 메서드 사용 print(happy.fetch("공")) # 자식 클래스에서 재정의한 메서드 사용 print(happy.speak()) ``` ### 2.2. 다형성 (Polymorphism) "여러(Poly) 개의 형태(Morph)"라는 뜻으로, 동일한 이름의 메서드나 연산자가 객체의 종류에 따라 다르게 동작하는 것을 의미합니다. 상속 관계에서 메서드 오버라이딩을 통해 주로 구현됩니다. #### 예제: 동물들의 소리 ```python class Cat: def __init__(self, name): self.name = name def speak(self): return f"{self.name}가 '야옹'하고 웁니다." # Dog 클래스와 Cat 클래스는 서로 다른 클래스지만, # 동일한 이름의 speak() 메서드를 가지고 있습니다. bory = Dog("보리", 3, "푸들") nabi = Cat("나비") # animal_sound 함수는 어떤 동물 객체가 오든 상관없이 # .speak() 메서드만 호출하여 다형성을 활용합니다. def animal_sound(animal): print(animal.speak()) animal_sound(bory) # Dog 객체의 speak() 호출 -> 보리가 '멍멍' 하고 짖습니다. animal_sound(nabi) # Cat 객체의 speak() 호출 -> 나비가 '야옹'하고 웁니다. # 이처럼, 객체가 어떤 클래스인지 일일이 확인할 필요 없이 # 공통된 인터페이스(speak)를 통해 다양한 객체를 동일한 방식으로 처리할 수 있습니다. # 이는 코드의 유연성과 확장성을 크게 높여줍니다. #### ✨ AI 라이브러리 속 다형성: Scikit-learn 우리가 사용하는 Scikit-learn 라이브러리는 다형성의 원칙을 매우 잘 활용하고 있습니다. `LogisticRegression`, `DecisionTreeClassifier`, `SVM` 등 다양한 분류 모델들은 모두 이름은 다르지만, 데이터를 학습시키는 `.fit(X, y)` 메서드와 예측을 수행하는 `.predict(X)` 메서드를 공통적으로 가지고 있습니다. ```python from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier # 모델 객체 생성 model1 = LogisticRegression() model2 = DecisionTreeClassifier() # 동일한 인터페이스(.fit, .predict)를 사용 # model1.fit(X_train, y_train) # model1.predict(X_test) # model2.fit(X_train, y_train) # model2.predict(X_test) ``` 이처럼 개발자는 모델의 내부 구현이 어떻게 다른지 신경 쓸 필요 없이, `fit`과 `predict`라는 일관된 방식으로 다양한 모델을 훈련하고 사용할 수 있습니다. 이것이 바로 다형성이 코드의 재사용성과 생산성을 높이는 강력한 예시입니다. ### 2.3. 캡슐화 (Encapsulation) 객체의 속성(데이터)과 그 데이터를 처리하는 메서드를 하나로 묶고, 데이터의 일부를 외부에서 직접 접근하지 못하도록 숨기는 것입니다. 이를 **정보 은닉(Information Hiding)**이라고도 합니다. 파이썬에서는 이름 앞에 언더스코어(`_` 또는 `__`)를 붙여 캡슐화를 구현합니다. - **`_` (Protected)**: "이 속성이나 메서드는 클래스 내부용이니 가급적 직접 건드리지 마세요"라는 약속. (강제성은 없음) - **`__` (Private)**: 클래스 외부에서 직접 접근할 수 없도록 이름이 변경(Name Mangling)됨. #### 예제: 은행 계좌 ```python class BankAccount: def __init__(self, owner_name: str, initial_balance: float): self.owner_name = owner_name # 잔액(_balance)은 외부에서 직접 수정할 수 없도록 보호합니다. self.__balance = initial_balance def deposit(self, amount: float): """입금: 양수 금액만 입금 가능""" if amount > 0: self.__balance += amount print(f"{amount}원이 입금되었습니다.") else: print("입금액은 0보다 커야 합니다.") def withdraw(self, amount: float): """출금: 잔액 내에서만 출금 가능""" if 0 < amount <= self.__balance: self.__balance -= amount print(f"{amount}원이 출금되었습니다.") else: print("출금액이 유효하지 않거나 잔액이 부족합니다.") def get_balance(self) -> float: """잔액 조회 메서드를 통해서만 잔액을 확인할 수 있습니다.""" return self.__balance account = BankAccount("홍길동", 10000) # 잔액을 직접 수정하려고 하면 오류가 발생하거나 의도대로 동작하지 않습니다. # account.__balance = 999999 # private 속성은 직접 접근/수정 불가 # print(account.__balance) # AttributeError 발생 # 반드시 클래스가 제공하는 메서드를 통해 데이터에 접근하고 조작해야 합니다. print(f"현재 잔액: {account.get_balance()}") account.deposit(5000) account.withdraw(2000) print(f"최종 잔액: {account.get_balance()}") # 이렇게 캡슐화를 통해 데이터의 무결성을 지키고, # 객체의 사용법을 단순하게 만들 수 있습니다. ``` ### 2.4. 정적/클래스 메서드 (Static/Class Method) 일반적으로 클래스의 메서드는 첫 번째 인자로 객체 자기 자신(`self`)을 받는 인스턴스 메서드입니다. 하지만 때로는 객체의 상태(인스턴스 속성)와는 독립적인 기능을 클래스에 포함시켜야 할 때가 있습니다. 이럴 때 `@staticmethod`와 `@classmethod`를 사용합니다. #### 예제: 다양한 방식으로 날짜 객체를 생성하는 클래스 ```python import datetime class MyDate: def __init__(self, year, month, day): self.year = year self.month = month self.day = day def display(self): return f"{self.year}-{self.month:02d}-{self.day:02d}" @staticmethod def is_valid_date(date_string: str) -> bool: """ 날짜 문자열(YYYY-MM-DD)이 유효한지 검사하는 정적 메서드. 특정 객체의 상태(self)나 클래스 정보(cls)를 사용하지 않는 순수 함수입니다. """ try: year, month, day = map(int, date_string.split('-')) datetime.date(year, month, day) return True except ValueError: return False @classmethod def from_string(cls, date_string: str): """ "YYYY-MM-DD" 형식의 문자열로부터 클래스 객체를 생성하는 클래스 메서드. 'cls' 인자를 통해 클래스 자체에 접근하여 새로운 객체를 생성합니다. """ year, month, day = map(int, date_string.split('-')) return cls(year, month, day) # cls()는 MyDate()와 동일 # 인스턴스 메서드 호출 date_obj1 = MyDate(2023, 12, 25) print(date_obj1.display()) # 정적 메서드 호출 (객체를 생성할 필요 없음) print(f"2023-12-25는 유효한 날짜인가? {MyDate.is_valid_date('2023-12-25')}") # 클래스 메서드 호출 (객체를 생성하는 또 다른 방법) date_obj2 = MyDate.from_string("2024-01-01") print(date_obj2.display()) ``` --- ## 3. 미니 프로젝트: 텍스트 RPG 캐릭터 만들기 지금까지 배운 클래스, 상속, 메서드 오버라이딩 개념을 모두 활용하여 간단한 텍스트 기반 RPG 게임의 캐릭터 시스템을 만들어 보겠습니다. ### 3.1. 기본 `Character` 클래스 구현 모든 캐릭터(전사, 마법사 등)가 공통으로 가질 속성(이름, 체력, 공격력)과 행동(공격, 상태 보기)을 부모 클래스인 `Character`에 정의합니다. ```python import random class Character: def __init__(self, name: str, hp: int, power: int): self.name = name self.max_hp = hp self.hp = hp self.power = power def attack(self, other): damage = random.randint(self.power - 2, self.power + 2) other.hp = max(other.hp - damage, 0) print(f"🗡️ {self.name}이(가) {other.name}에게 {damage}의 피해를 입혔습니다.") if other.hp == 0: print(f"😵 {other.name}이(가) 쓰러졌습니다.") def show_status(self): print(f"[{self.name}] HP: {self.hp}/{self.max_hp} | POWER: {self.power}") ``` ### 3.2. 상속을 이용한 직업 클래스 확장 `Character` 클래스를 상속받아, 각 직업의 고유한 특징을 가진 `Warrior`와 `Magician` 클래스를 만듭니다. - **전사(Warrior)**: 기본 공격 외에, 더 큰 피해를 주는 '스킬'을 사용하지만, 스킬 사용 후에는 방어력이 약해지는 특징을 가집니다. - **마법사(Magician)**: 마나(MP)를 사용하여 강력한 광역 마법을 사용합니다. ```python # 위에서 정의한 Character 클래스를 상속받습니다. class Warrior(Character): def __init__(self, name: str, hp: int, power: int): super().__init__(name, hp, power) # 부모 클래스의 __init__ 호출 self.defense_mode = False # 메서드 오버라이딩 (부모의 attack 메서드를 재정의) def attack(self, other): if self.defense_mode: print(f"🛡️ {self.name}은(는) 방어 태세라 공격할 수 없습니다.") self.defense_mode = False # 다음 턴에 공격 가능하도록 return super().attack(other) def skill(self, other): """기존 공격력의 2배에 달하는 스킬 공격을 합니다.""" skill_damage = self.power * 2 other.hp = max(other.hp - skill_damage, 0) print(f"💥 {self.name}의 스킬! {other.name}에게 {skill_damage}의 치명적인 피해!") if other.hp == 0: print(f"😵 {other.name}이(가) 쓰러졌습니다.") # 스킬 사용 후 방어 태세로 전환 self.defense_mode = True print(f"🛡️ {self.name}이(가) 스킬 사용 후 방어 태세에 들어갑니다.") class Magician(Character): def __init__(self, name: str, hp: int, power: int, mp: int): super().__init__(name, hp, power) self.max_mp = mp self.mp = mp def magic(self, others: list): """마나를 10 소모하여 모든 적에게 마법 공격을 합니다.""" if self.mp < 10: print("MP가 부족하여 마법을 사용할 수 없습니다.") return self.mp -= 10 print(f"🔥 {self.name}의 광역 마법! (MP {self.mp}/{self.max_mp})") for target in others: damage = random.randint(self.power, self.power + 5) target.hp = max(target.hp - damage, 0) print(f"✨ {target.name}에게 {damage}의 마법 피해!") if target.hp == 0: print(f"😵 {target.name}이(가) 쓰러졌습니다.") # 메서드 오버라이딩 (마법사 정보에 맞게 상태 표시 변경) def show_status(self): print(f"[{self.name}] HP: {self.hp}/{self.max_hp} | MP: {self.mp}/{self.max_mp} | POWER: {self.power}") ``` ### 3.3. 게임 실행 생성한 클래스들로 객체를 만들어 간단한 전투 시뮬레이션을 진행합니다. ```python # 캐릭터 객체 생성 warrior = Warrior("아서스", hp=120, power=15) magician = Magician("제이나", hp=80, power=20, mp=50) monster1 = Character("고블린", 50, 5) monster2 = Character("오크", 70, 8) # 전투 시작 print("--- ⚔️ 전투 시작 ⚔️ ---\n") warrior.show_status() magician.show_status() monster1.show_status() monster2.show_status() print("\n-------------------\n") # 턴 1 print("--- 턴 1 ---") warrior.attack(monster1) magician.magic([monster1, monster2]) monster1.attack(warrior) print("\n") # 턴 2 print("--- 턴 2 ---") warrior.skill(monster2) magician.show_status() # 마법사 상태 확인 monster2.attack(magician) print("\n") # 전투 후 상태 print("--- ⚔️ 전투 종료 후 상태 ⚔️ ---") warrior.show_status() magician.show_status() monster1.show_status() monster2.show_status() ``` 이 미니 프로젝트를 통해, OOP의 핵심 원칙들이 어떻게 실제 코드의 구조를 잡고, 기능을 확장하며, 각 객체의 개성을 부여하는 데 사용되는지 경험할 수 있습니다. --- ### ✅ 정리 및 다음 단계 이번 파트에서는 객체 지향 프로그래밍의 핵심인 **클래스**와 **객체**의 개념을 배우고, **상속, 다형성, 캡슐화**와 같은 중요 원칙들을 학습했습니다. - **클래스**: 객체를 만들기 위한 설계도 - **객체**: 클래스로부터 생성된 실체 - **상속**: 코드 재사용성을 높임 - **다형성**: 코드 유연성을 높임 - **캡슐화**: 코드 안정성을 높임 OOP를 이해함으로써 우리는 앞으로 사용할 AI 라이브러리들의 동작 방식을 더 깊이 이해하고, 재사용 가능한 코드를 작성할 수 있는 튼튼한 기반을 마련했습니다. 이제 파이썬의 기본기를 모두 다졌습니다. 다음 파트에서는 드디어 AI 개발의 세계로 본격적으로 뛰어듭니다. **➡️ 다음 시간: [Part 5: AI 개발 핵심 라이브러리](part_5_ai_core_libraries.md)** 다음 시간에는 데이터 분석과 머신러닝의 필수 도구인 **NumPy, Pandas, Matplotlib, Scikit-learn** 라이브러리의 핵심 사용법을 익힙니다. 이 라이브러리들을 활용하여 데이터를 다루고, 시각화하고, 간단한 머신러닝 모델을 직접 만들어보는 실습을 진행할 것입니다.