# Part 4: 객체 지향 프로그래밍 (OOP) 마스터하기 (4주차) **⬅️ 이전 시간: [Part 3: 파이썬 컬렉션 심화](./part_3_python_collections.md)** | **➡️ 다음 시간: [Part 5: AI 핵심 라이브러리](./part_5_ai_core_libraries.md)** --- ## 📜 실습 코드 바로가기 - **`part_4_object_oriented_programming.py`**: [바로가기](./source_code/part_4_object_oriented_programming.py) - 본 파트의 모든 예제 코드는 위 파일에서 직접 실행하고 수정해볼 수 있습니다. 클래스, 상속, 다형성 등 OOP의 핵심 개념을 코드로 확인해보세요. --- 지금까지 우리는 파이썬의 문법과 다양한 데이터 구조(컬렉션)를 배웠습니다. 이는 프로그래밍의 '재료'를 다듬는 법을 익힌 것과 같습니다. 이번 주차에는 이 재료들을 조합하여 '요리'를 하는 법, 즉 더 크고 체계적인 프로그램을 만드는 '[객체 지향 프로그래밍(OOP)](./glossary.md#객체-지향-프로그래밍oop)' 패러다임을 학습합니다. **왜 OOP가 AI 개발에서 중요할까요?** 여러분이 앞으로 사용하게 될 `Scikit-learn`, `PyTorch` 같은 AI 라이브러리의 동작 방식이 바로 OOP에 기반하기 때문입니다. - `model = RandomForestClassifier()`: 이 코드는 `RandomForestClassifier`라는 **클래스(설계도)**로 `model`이라는 **객체(실체)**를 만드는 과정입니다. - `model.fit(X, y)`: `model` 객체가 가진 `fit`이라는 **메서드(행동)**를 호출하여 모델을 학습시킵니다. 이처럼 OOP를 이해하면, 복잡한 AI 라이브러리의 구조를 꿰뚫어 보고, 각 부분이 어떻게 상호작용하는지 명확하게 파악할 수 있습니다. ### 💡 쉽게 이해하기: OOP는 '붕어빵 비즈니스'입니다 > OOP의 여러 개념을 이해하기 위해, 우리는 '붕어빵 장사'라는 비즈니스 모델을 사용할 것입니다. 앞으로 나오는 모든 개념을 이 붕어빵 가게에 빗대어 생각해 보세요! > > - **[클래스(Class)](./glossary.md#클래스class)**: 붕어빵의 모양, 크기, 굽는 방식을 정의하는 **'붕어빵 틀'** 그 자체입니다. > - **[객체(Object)](./glossary.md#객체object--인스턴스instance)**: 붕어빵 틀에서 막 찍어낸, 세상에 단 하나뿐인 **'붕어빵'** 한 개입니다. > - **[속성(Attribute)](./glossary.md#속성attribute)**: 각각의 붕어빵이 가진 고유한 특징, 예를 들어 '팥 앙금', '슈크림 앙금' 같은 **'속재료'** 입니다. > - **[메서드(Method)](./glossary.md#메서드method)**: 붕어빵 틀을 이용해 할 수 있는 행동, 즉 **'굽기()', '뒤집기()', '포장하기()'** 같은 기능입니다. 파이썬은 모든 것이 객체(Object)로 이루어진 강력한 객체 지향 프로그래밍(OOP) 언어입니다. Scikit-learn, PyTorch 등 우리가 사용할 대부분의 AI 라이브러리는 OOP 원칙에 기반하여 설계되었습니다. OOP를 이해하면 라이브러리의 구조를 파악하고 더 깊이 있게 활용할 수 있으며, 재사용과 확장이 용이한 코드를 직접 작성할 수 있게 됩니다. --- ## 1일차(월): 클래스와 객체 - OOP의 시작 ### 🎯 오늘의 목표 - 클래스(Class)와 객체(Object)의 개념을 이해하고 직접 클래스를 정의합니다. - [`__init__`](./glossary.md#init)과 [`self`](./glossary.md#self)를 사용하여 객체의 고유한 상태를 만듭니다. - [`@dataclass`](./glossary.md#dataclass) 데코레이터로 상용구(boilerplate) 코드를 획기적으로 줄입니다. --- ### 1. 클래스(Class)와 객체(Object) - **[클래스(Class)](./glossary.md#클래스class)**: 객체를 만들기 위한 **'설계도'** 또는 '틀'. 객체가 가질 속성(데이터)과 행동(메서드)을 정의합니다. (예: '자동차' 설계도, **'붕어빵 틀'**) - **[객체(Object)/인스턴스(Instance)](./glossary.md#객체object--인스턴스instance)**: 클래스로부터 생성된 **'실체'**. '자동차' 클래스로부터 '빨간색 페라리', '검은색 아반떼' 같은 구체적인 객체를 만듭니다. (예: **'팥 붕어빵', '슈크림 붕어빵'**) ```python # 'Dog'라는 이름의 클래스를 정의합니다. class Dog: # 클래스 속성: 모든 Dog 객체가 공유하는 값 species = "Canis familiaris" # 초기화 메서드: 객체가 생성될 때 자동으로 호출되어 속성을 초기화합니다. def __init__(self, name: str, age: int): # 인스턴스 속성: 각 객체마다 고유하게 가지는 값 self.name = name self.age = age # 인스턴스 메서드: 객체가 수행할 수 있는 행동 def speak(self, sound: str): return f"{self.name}가 '{sound}' 하고 짖습니다." ``` > **붕어빵 비유**: > - `__init__`: 붕어빵 틀(`Dog`)에 반죽을 붓고, 각 붕어빵마다 다른 앙금(`name`, `age`)을 넣는 과정입니다. > - `self`: 지금 막 만들어지고 있는 **'바로 그 붕어빵'** 자신을 의미합니다. 이 `self` 덕분에 "보리"라는 붕어빵과 "짱구"라는 붕어빵이 서로 다른 이름(`self.name`)을 가질 수 있습니다. ### 2. `self`의 정체 `self`는 **메서드를 호출한 객체 자기 자신**을 가리키는, 파이썬 OOP의 핵심 규칙입니다. #### 💡 쉽게 이해하기: `self`는 객체의 '나 자신'이라는 대명사 > "보리야, 짖어봐!" 라고 `bory.speak("멍멍")`을 실행하면, `Dog` 클래스는 `bory`라는 객체를 `self`라는 이름으로 받습니다. 그리고 `speak` 메서드 안에서 `self.name`이라고 쓰는 것은, `bory` 객체가 " **나(bory)의** 이름은 '보리'야 " 라고 자기 자신의 속성에 접근하는 것과 같습니다. 이 `self` 덕분에 수많은 Dog 객체들이 각자 자신의 이름과 나이를 헷갈리지 않고 사용할 수 있습니다. - 메서드를 정의할 때 첫 번째 매개변수로 반드시 `self`를 써주어야 합니다. - `bory.speak("멍멍")` 코드는 내부적으로 `Dog.speak(bory, "멍멍")`과 같이 동작합니다. `self` 매개변수로 `bory` 객체 자신이 전달되는 것입니다. ### 3. 보일러플레이트 줄이기: `@dataclass` 데이터를 담는 용도의 클래스를 만들 때, `__init__`, `__repr__` (객체 출력) 같은 반복적이고 지루한 코드(Boilerplate)를 자동으로 생성해주는 매우 유용한 [데코레이터](./glossary.md#데코레이터decorator)입니다. #### 💡 쉽게 이해하기: `@dataclass`는 '클래스 자동 생성기' > [`@dataclass`](./glossary.md#dataclass)는 마치 **'클래스의 귀찮은 부분들을 알아서 만들어주는 기계'**와 같습니다. 우리는 `name: str`, `age: int`처럼 필요한 속성들만 선언해주면, 이 기계가 객체를 초기화하는 `__init__` 메서드나, 객체를 예쁘게 출력해주는 `__repr__` 메서드 같은 것들을 보이지 않는 곳에서 자동으로 뚝딱 만들어줍니다. ```python from dataclasses import dataclass @dataclass class DogData: name: str age: int species: str = "Canis familiaris" # 기본값 지정도 가능 def speak(self, sound: str): return f"{self.name}가 '{sound}' 하고 짖습니다." # __init__을 직접 작성하지 않았지만 자동으로 생성됨! bory = DogData("보리", 3) print(bory) # __repr__도 자동으로 생성되어 출력이 깔끔함 # 출력: DogData(name='보리', age=3, species='Canis familiaris') ``` --- ## 2일차(화): 상속 - 코드 재사용의 마법 ### 🎯 오늘의 목표 - 상속을 통해 부모 클래스의 기능을 물려받아 코드 중복을 줄입니다. - 메서드 오버라이딩으로 부모의 기능을 자식에 맞게 재정의합니다. - `super()`를 사용하여 부모 클래스의 메서드를 호출합니다. --- ### 1. 상속(Inheritance)과 메서드 오버라이딩 **[상속](./glossary.md#상속inheritance)**은 부모 클래스(Superclass)의 속성과 메서드를 자식 클래스(Subclass)가 물려받는 것입니다. 이는 마치 **'기본 설계도'를 바탕으로 '수정 설계도'를 만드는 것**과 같습니다. 자식 클래스는 부모의 기능을 그대로 사용하거나, **[메서드 오버라이딩(Method Overriding)](./glossary.md#메서드-오버라이딩method-overriding)**을 통해 자신에게 맞게 수정할 수 있습니다. > **붕어빵 비유**: > '기본 붕어빵 틀(`Dog`)'을 물려받아, 기능이 추가된 '피자 붕어빵 틀(`GoldenRetriever`)'을 새로 만드는 것과 같습니다. 모양은 같지만, 굽는 방식(`speak`)은 피자 토핑에 맞게 살짝 달라질 수 있습니다. ```python class GoldenRetriever(Dog): # Dog 클래스를 상속 # 메서드 오버라이딩: 부모의 speak 메서드를 재정의 def speak(self, sound="월! 월!"): return f"{self.name}가 행복하게 '{sound}' 하고 짖습니다." ``` ### 2. 부모의 능력을 빌리는 `super()` 자식 클래스에서 부모 클래스의 메서드를 호출하고 싶을 때 [`super()`](./glossary.md#super)를 사용합니다. 이를 통해 부모의 기능을 완전히 덮어쓰는 대신, **기능을 확장**할 수 있습니다. > **붕어빵 비유**: > '프리미엄 붕어빵 틀(`SpecialDog`)'을 만들 때, 기존 붕어빵 틀(`Dog`)의 모양과 앙금 넣는 방식(`__init__`)은 그대로 쓰고 싶습니다. 이때 `super().__init__()`를 호출하면, 마치 기본 틀의 기능을 그대로 가져와 사용하고, 우리는 프리미엄 재료(`ability`)를 추가하는 작업에만 집중할 수 있습니다. ```python class SpecialDog(Dog): def __init__(self, name: str, age: int, ability: str): # super()를 이용해 부모 클래스(Dog)의 __init__을 호출하여 # name과 age 속성 초기화를 위임합니다. super().__init__(name, age) # 자식 클래스만의 속성을 추가로 초기화합니다. self.ability = ability def describe(self): # 부모의 describe가 있다면 호출하고, 없다면 직접 구현 # (Dog 클래스에 describe가 없으므로 직접 구현) return f"{self.name}는 {self.age}살이며, '{self.ability}' 능력이 있습니다." special_dog = SpecialDog("슈퍼독", 5, "비행") print(special_dog.describe()) # 출력: 슈퍼독는 5살이며, '비행' 능력이 있습니다. ``` --- ## 3일차(수): 다형성과 캡슐화 - 유연하고 안전한 코드 ### 🎯 오늘의 목표 - 다형성(Polymorphism)을 통해 코드의 유연성과 확장성을 높입니다. - 캡슐화(Encapsulation)를 통해 객체의 데이터를 안전하게 보호합니다. --- ### 1. 다형성(Polymorphism): 같은 이름, 다른 행동 "여러(Poly) 개의 형태(Morph)"라는 뜻으로, 동일한 이름의 메서드가 객체의 종류에 따라 다르게 동작하는 것입니다. 이것이 바로 [다형성](./glossary.md#다형성polymorphism)입니다. > **붕어빵 비유**: > 손님이 "붕어빵 하나 주세요!"라고 말합니다. 가게 주인은 손님이 보는 앞에서 `판매하기()`라는 동일한 동작을 취합니다. 하지만 손님에게 전달되는 것은 '팥 붕어빵'일 수도, '피자 붕어빵'일 수도 있습니다. 이처럼 **같은 `판매하기()` 요청에 대해, 대상(객체)에 따라 다른 결과가 나타나는 것**이 다형성입니다. ```python class Cat: def __init__(self, name): self.name = name def speak(self): # Dog 클래스와 동일한 이름의 메서드 return "야옹" # Horse 클래스를 새로 추가해도 animal_sound 함수는 바꿀 필요가 없습니다! class Horse: def __init__(self, name): self.name = name def speak(self): return "히히힝" bory = Dog("보리", 3) nabi = Cat("나비") pony = Horse("포니") # animal_sound 함수는 객체의 클래스가 무엇인지(개가, 고양이인지, 말인지) 신경쓰지 않고 # .speak() 메서드만 호출합니다. 이것이 다형성의 힘입니다. def animal_sound(animal): print(f"{animal.name}: {animal.speak()}") animal_sound(bory) # Dog 객체의 speak()가 호출됨 animal_sound(nabi) # Cat 객체의 speak()가 호출됨 animal_sound(pony) # Horse 객체의 speak()가 호출됨 ``` > **AI 라이브러리 속 다형성**: Scikit-learn의 모든 모델(`LogisticRegression`, `DecisionTree` 등)은 `.fit()`과 `.predict()`라는 동일한 이름의 메서드를 가집니다. 덕분에 우리는 모델의 종류와 상관없이 일관된 방식으로 코드를 작성할 수 있습니다. ### 2. 캡슐화(Encapsulation): 소중한 데이터 보호하기 객체의 속성을 외부에서 직접 수정하지 못하도록 숨기고, 정해진 메서드(getter/setter)를 통해서만 접근하도록 제어하는 것, 즉 [캡슐화](./glossary.md#캡슐화encapsulation)입니다. > **붕어빵 비유**: > 붕어빵 맛의 핵심인 **'반죽의 비밀 레시피(`__balance`)'**는 아무나 볼 수도, 수정할 수도 없게 주방 깊숙한 곳에 숨겨둡니다. 레시피를 변경하려면, 반드시 점장님의 허락(메서드 호출)을 거쳐 정해진 절차(`deposit`)에 따라서만 가능합니다. 이렇게 소중한 데이터를 외부로부터 보호하는 것이 캡슐화입니다. - `_` (Protected): "내부용이니 가급적 건드리지 마세요" 라는 개발자 간의 약속. - `__` (Private): 이름 앞에 `_클래스명`이 붙어([Name Mangling](./glossary.md#네임-맹글링name-mangling)) 외부에서 직접 접근이 거의 불가능해짐. ```python class BankAccount: def __init__(self, owner, balance): self.owner = owner self.__balance = balance # 잔액은 외부에서 직접 수정 불가 (private) # getter: 잔액을 외부로 알려주는 통로 def get_balance(self): return self.__balance # setter: 잔액을 정해진 규칙에 따라 수정하는 통로 def deposit(self, amount): if amount > 0: self.__balance += amount print(f"{amount}원 입금 완료. 현재 잔액: {self.__balance}원") else: print("입금액은 0보다 커야 합니다.") my_account = BankAccount("홍길동", 10000) # my_account.__balance = 999999 # 직접 수정 시도 -> AttributeError 발생 my_account.deposit(5000) print(f"최종 잔액 조회: {my_account.get_balance()}") ``` --- ## 4일차(목): 추상화와 매직 메서드 ### 🎯 오늘의 목표 - 추상화를 통해 클래스가 반드시 구현해야 할 메서드를 '규칙'으로 강제합니다. - 매직 메서드로 파이썬 내장 기능(출력, 비교 등)을 커스터마이징합니다. --- ### 1. 추상화(Abstraction): 필수 기능 강제하기 [추상 클래스](./glossary.md#추상-클래스abstract-class)는 '미완성 설계도'와 같습니다. `abc` 모듈과 `@abstractmethod`를 사용하여 자식 클래스가 반드시 구현해야 할 메서드를 명시할 수 있습니다. 이것이 [추상화](./glossary.md#추상화abstraction)입니다. > **붕어빵 비유**: > 붕어빵 프랜차이즈 본사에서 '가맹점 운영 매뉴얼(`AbstractAnimal`)'을 내놓았습니다. 이 매뉴얼에는 "모든 가맹점은 반드시 '굽기(`speak`)' 기능을 스스로 구현해야 합니다"라는 **규칙**이 명시되어 있습니다. 본사는 어떻게 구울지(how)는 알려주지 않고, 무엇을 해야 하는지(what)만 강제합니다. 이 규칙을 따르지 않은 가게는 오픈할 수 없습니다(에러 발생). ```python from abc import ABC, abstractmethod # ABC를 상속받아 추상 클래스로 지정 class AbstractAnimal(ABC): def __init__(self, name): self.name = name # @abstractmethod: 자식 클래스는 이 메서드를 반드시 구현해야 함 @abstractmethod def speak(self): pass # 추상 클래스를 상속받는 자식 클래스 class Lion(AbstractAnimal): # 부모의 추상 메서드인 speak()를 반드시 구현해야 합니다. # 만약 구현하지 않으면 TypeError가 발생합니다. def speak(self): return "어흥" # 추상 클래스는 직접 객체로 만들 수 없습니다. # my_animal = AbstractAnimal("동물") # -> TypeError 발생 lion = Lion("사자") print(f"{lion.name}: {lion.speak()}") ``` ### 2. 파이썬의 매직 메서드(Magic Methods) [매직 메서드](./glossary.md#매직-메서드magic-methods)(또는 던더 메서드, Dunder Methods)는 `__이름__` 형태를 가지며, 파이썬의 내장 연산자나 함수(`+`, `len()`, `str()` 등)가 우리 클래스의 객체와 함께 사용될 때 호출되는 특별한 메서드입니다. > **붕어빵 비유**: > 우리가 만든 '붕어빵' 객체를 `print(my_bungeoppang)` 했을 때 "팥이 든 맛있는 붕어빵"처럼 예쁜 설명이 나오게 하거나, `bungeoppang1 + bungeoppang2` 처럼 붕어빵을 합치는 새로운 규칙을 만들 수 있게 해주는 마법 같은 기능입니다. | 매직 메서드 | 호출되는 상황 | 설명 | |---|---|---| | `__str__(self)` | `str(obj)`, `print(obj)` | 객체를 사람이 읽기 좋은 **문자열**로 표현할 때 | | `__repr__(self)`| `repr(obj)` | 객체를 **개발자가 명확히** 이해할 수 있는 문자열로 표현할 때 | | `__len__(self)` | `len(obj)` | 객체의 길이를 반환 | | `__add__(self, other)` | `obj1 + obj2` | 덧셈 연산 정의 | | `__eq__(self, other)` | `obj1 == obj2`| 동등 비교 연산 정의 | ```python @dataclass class Word: text: str count: int def __str__(self): return f"단어 '{self.text}' ({self.count}번 등장)" def __add__(self, other): # Word 객체끼리 + 연산을 하면 count를 합친 새 Word 객체 반환 if isinstance(other, Word): return Word(self.text, self.count + other.count) else: raise TypeError("Word 객체끼리만 더할 수 있습니다.") def __eq__(self, other): # 두 Word 객체는 text가 같으면 같은 것으로 취급 return self.text == other.text ``` --- ## 5일차(금): 주간 마무리 및 종합 실습 ### 문제: 온라인 서점 시스템 모델링 - `Book` (책), `Member` (회원), `Order` (주문) 이라는 3개의 클래스를 `dataclass`를 사용하여 정의하고, 각 클래스 간의 관계를 표현하는 간단한 온라인 서점 시스템을 만들어보세요. **요구사항:** 1. **`Book` 클래스**: - 속성: `isbn` (고유번호), `title` (제목), `author` (저자), `price` (가격) 2. **`Member` 클래스**: - 속성: `member_id`, `name`, `address` 3. **`Order` 클래스**: - 속성: `order_id`, `member` (`Member` 객체), `books` (`Book` 객체의 리스트) - 메서드: `total_price()` (주문한 책들의 총 가격을 계산하여 반환) 4. 객체를 생성하고, 회원이 여러 권의 책을 주문하는 시나리오를 코드로 구현하세요. ```python from dataclasses import dataclass, field from typing import List @dataclass class Book: isbn: str title: str author: str price: int @dataclass class Member: member_id: str name: str address: str @dataclass class Order: order_id: str member: Member books: List[Book] = field(default_factory=list) def total_price(self) -> int: return sum(book.price for book in self.books) # 1. 객체 생성 member1 = Member("user001", "김파이", "서울시 강남구") book1 = Book("978-0-13-235088-4", "Clean Code", "Robert C. Martin", 30000) book2 = Book("978-0-201-63361-0", "Design Patterns", "Erich Gamma", 35000) # 2. 주문 생성 및 실행 order1 = Order("order123", member1, [book1, book2]) # 3. 결과 출력 print(f"주문자: {order1.member.name}") print("주문 내역:") for book in order1.books: print(f" - {book.title} ({book.author})") print(f"총 주문 금액: {order1.total_price()}원") ``` --- ## 📝 4주차 요약 이번 주차에는 프로그램을 체계적으로 설계하는 패러다임, 객체 지향 프로그래밍(OOP)을 '붕어빵 비즈니스'에 빗대어 배웠습니다. - **클래스와 객체**: '붕어빵 틀'과 '붕어빵'의 관계를 통해, 설계도(클래스)로 실체(객체)를 만드는 기본 개념과 `__init__`, `self`의 용법을 익혔습니다. `@dataclass`로 반복적인 코드를 줄이는 법도 배웠습니다. - **상속**: 부모 클래스의 기능을 물려받아(상속) 코드 중복을 줄이고, 자신에 맞게 기능을 재정의(오버라이딩)하거나 `super()`로 부모 기능을 확장하는 방법을 학습했습니다. - **다형성과 캡슐화**: 동일한 요청에 대해 객체별로 다르게 반응하는 다형성으로 코드 유연성을 높이고, `__`를 이용한 캡슐화로 데이터를 안전하게 보호하는 방법을 배웠습니다. - **추상화와 매직 메서드**: 추상 클래스로 자식 클래스가 구현해야 할 기능을 강제하고, `__str__`이나 `__len__` 같은 매직 메서드로 파이썬의 내장 기능을 우리 객체에 맞게 커스터마이징했습니다. ## 🔍 4주차 연습 문제 **문제 1: 게임 캐릭터 클래스 만들기** - 기본 `Character` 클래스를 만드세요. - 속성: `name`, `hp`, `power` - 메서드: `attack(other)` (상대방(`other`)의 `hp`를 자신의 `power`만큼 감소시킴) - `Character`를 상속받는 `Warrior`와 `Wizard` 클래스를 만드세요. - `Warrior`: `attack` 메서드를 오버라이딩하여, 20% 확률로 2배의 데미지를 입히도록 수정하세요. - `Wizard`: `attack` 메서드를 오버라이딩하고, 공격 후 자신의 `hp`를 `power`의 절반만큼 회복하는 기능을 추가하세요. (최대 hp는 넘지 않도록) - 전사와 마법사 객체를 만들어 서로 공격하는 시나리오를 구현해보세요. **➡️ 다음 시간: [Part 5: AI 핵심 라이브러리 (NumPy & Pandas)](./part_5_ai_core_libraries.md)**