# Part 4: 객체 지향 프로그래밍 (OOP) 마스터하기

**⬅️ 이전 시간: [Part 3: 파이썬 컬렉션 심화](../03_python_collections/part_3_python_collections.md)**
**➡️ 다음 시간: [Part 5: AI 핵심 라이브러리](../05_ai_core_libraries/part_5_ai_core_libraries.md)**

---

## 1. 학습 목표 (Learning Objectives)

이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.

- 클래스(설계도)를 정의하고, 그로부터 고유한 속성과 행동을 가진 객체(제품)를 생성할 수 있습니다.
- 상속을 통해 기존 클래스의 기능을 재사용하고, 메서드 오버라이딩으로 기능을 확장/변경할 수 있습니다.
- 다형성을 활용하여 유연하고 확장성 높은 코드를 작성할 수 있습니다.
- 캡슐화로 데이터를 안전하게 보호하고, 추상화로 클래스의 필수 기능을 강제할 수 있습니다.
- 현실 세계의 문제를 객체들 간의 상호작용으로 모델링하여 체계적인 프로그램을 설계할 수 있습니다.

## 2. 핵심 요약 (Key Summary)
이 파트에서는 대규모 프로그램을 체계적으로 설계하기 위한 객체 지향 프로그래밍(OOP) 패러다임을 학습합니다. 클래스(설계도)와 객체(제품)의 기본 개념을 이해하고, 상속을 통해 코드를 재사용하며, 다형성을 이용해 유연한 코드를 작성하는 방법을 배웁니다. 또한, 캡슐화와 추상화를 통해 데이터의 안전성과 코드의 확장성을 높이는 방법을 익힙니다. `@dataclass`, `super()`, 매직 메서드와 같은 파이썬의 OOP 관련 고급 기능들도 다룹니다.

- **핵심 키워드**: `클래스(Class)`, `객체(Object)`, `상속(Inheritance)`, `다형성(Polymorphism)`, `캡슐화(Encapsulation)`, `추상화(Abstraction)`, `@dataclass`, `super()`, `매직 메서드(Magic Methods)`

## 3. OOP와 함께 '붕어빵 비즈니스' 시작하기 (Introduction)

지금까지 우리는 프로그래밍의 '재료'를 다듬는 법을 익혔습니다. 이번 주차에는 이 재료들을 조합하여 '요리'를 하는 법, 즉 더 크고 체계적인 프로그램을 만드는 **[객체 지향 프로그래밍(OOP)](../../glossary.md#객체-지향-프로그래밍-object-oriented-programming-oop)** 패러다임을 학습합니다.

Scikit-learn, PyTorch 등 대부분의 AI 라이브러리는 OOP에 기반하며, `model = RandomForestClassifier()` 와 같은 코드는 OOP를 이해해야 그 구조를 꿰뚫어 볼 수 있습니다.

> **💡 핵심 비유: OOP는 '붕어빵 비즈니스'입니다**
> OOP의 모든 개념을 '붕어빵 장사'에 빗대어 이해해 봅시다!
> - **클래스(Class)**: 붕어빵을 만드는 **'붕어빵 틀'**
> - **객체(Object)**: 붕어빵 틀에서 찍어낸 **'붕어빵'** 하나하나
> - **속성(Attribute)**: 붕어빵의 '팥', '슈크림' 같은 **'속재료'**
> - **메서드(Method)**: 붕어빵 틀의 **'굽기()', '뒤집기()'** 같은 **'행동'**

> [!TIP]
> 본 파트의 모든 예제 코드는 `../../source_code/part_4_object_oriented_programming.py` 파일에서 직접 실행해볼 수 있습니다.

---

## 4. 클래스와 객체: 붕어빵 틀과 붕어빵

> **🎯 1일차 목표:** 클래스(Class)와 객체(Object)의 개념을 이해하고, `__init__`과 `self`를 사용하여 객체의 고유한 상태를 만듭니다.

- **클래스(Class)**: 객체를 만들기 위한 **'설계도'** 또는 '붕어빵 틀'.
- **객체(Object)**: 클래스로부터 생성된 **'실체'** 또는 '팥 붕어빵', '슈크림 붕어빵'.

```python
from dataclasses import dataclass

@dataclass # __init__, __repr__ 등 반복적인 코드를 자동 생성
class Bungeoppang:
    # --- 속성 (붕어빵의 속재료) ---
    flavor: str
    price: int = 1000 # 기본값 설정

    # --- 메서드 (붕어빵 틀의 행동) ---
    def sell(self):
        print(f"{self.flavor} 붕어빵을 {self.price}원에 판매합니다.")

# --- 객체 생성 (붕어빵 틀로 붕어빵을 찍어냄) ---
red_bean_bpp = Bungeoppang("팥")
shu_cream_bpp = Bungeoppang("슈크림", price=1200)

# --- 메서드 호출 (붕어빵 판매) ---
red_bean_bpp.sell() # "팥 붕어빵을 1000원에 판매합니다."
shu_cream_bpp.sell() # "슈크림 붕어빵을 1200원에 판매합니다."
```
> **`__init__`과 `self`**: `@dataclass`가 자동으로 만들어주는 `__init__`은 붕어빵 틀에 반죽과 **앙금(`flavor`, `price`)**을 넣는 과정입니다. `self`는 지금 만들어지고 있는 **'바로 그 붕어빵'** 자신을 가리키는 대명사로, 붕어빵들이 서로의 앙금을 헷갈리지 않게 해줍니다.

---

## 5. 상속: 새로운 맛의 붕어빵 출시하기

> **🎯 2일차 목표:** 상속으로 코드 중복을 줄이고, 메서드 오버라이딩과 `super()`로 기능을 확장합니다.

**상속(Inheritance)**은 '기본 붕어빵 틀'을 물려받아 '피자 붕어빵 틀' 같은 신제품을 만드는 것입니다.

```python
@dataclass
class PizzaBungeoppang(Bungeoppang): # Bungeoppang 클래스를 상속
    topping: str

    # --- 메서드 오버라이딩(Method Overriding) ---
    # 부모의 sell() 메서드를 신제품에 맞게 재정의
    def sell(self):
        print(f"'{self.topping}' 토핑이 올라간 {self.flavor} 피자 붕어빵을 {self.price}원에 판매합니다!")

# super() 사용 예시
@dataclass
class PremiumBungeoppang(Bungeoppang):
    origin: str

    def __post_init__(self):
        # super()는 부모 클래스를 의미. 부모의 기능을 확장할 때 사용.
        # 여기서는 Bungeoppang의 속성을 그대로 쓰되, 가격만 500원 인상
        self.price += 500
```
> **메서드 오버라이딩과 `super()`**: 기본 붕어빵의 판매 방식(`sell`)을 피자 붕어빵에 맞게 바꾸는 것이 **오버라이딩**입니다. 프리미엄 붕어빵을 만들 때, 기존 가격 정책에 추가 금액만 더하는 것처럼 부모의 기능을 그대로 활용하며 확장할 때 **`super()`**를 사용합니다.

---

## 6. 다형성과 캡슐화: 유연하고 안전한 비즈니스

> **🎯 3일차 목표:** 다형성(Polymorphism)으로 코드의 유연성을 높이고, 캡슐화(Encapsulation)로 데이터를 안전하게 보호합니다.

- **다형성(Polymorphism)**: "주문하신 것 나왔습니다!" 라는 **동일한 요청(`sell()`)**에 대해, 대상 객체가 '팥 붕어빵'이냐 '피자 붕어빵'이냐에 따라 **다른 결과가 나오는 것**입니다. 코드의 유연성을 크게 높여줍니다.
- **캡슐화(Encapsulation)**: 붕어빵 맛의 핵심인 **'반죽의 비밀 레시피(`__secret_recipe`)'**를 외부에서 함부로 바꾸지 못하게 숨기고, 정해진 방법으로만 접근하게 하여 데이터를 보호하는 것입니다.

```python
class BungeoppangFactory:
    def __init__(self, initial_flour_kg):
        self.__flour_kg = initial_flour_kg # 밀가루 재고(비밀 데이터)

    def get_stock(self): # 데이터를 확인하는 정해진 통로 (getter)
        return self.__flour_kg

    def add_flour(self, amount): # 데이터를 변경하는 정해진 통로 (setter)
        if amount > 0: self.__flour_kg += amount

factory = BungeoppangFactory(100)
# factory.__flour_kg = 999 #! 직접 수정 불가 (AttributeError)
factory.add_flour(20) # 정해진 방법으로만 수정 가능
print(f"현재 밀가루 재고: {factory.get_stock()}kg")
```

---

## 7. 추상화와 매직 메서드: 비즈니스 규칙과 마법

> **🎯 4일차 목표:** 추상화로 클래스의 필수 기능을 강제하고, 매직 메서드로 파이썬 내장 기능을 커스터마이징합니다.

- **추상화(Abstraction)**: 프랜차이즈 본사에서 "모든 가맹점은 반드시 **'굽기()' 기능을 스스로 구현해야 한다**"는 '운영 매뉴얼'을 만들어 규칙을 강제하는 것입니다.
- **매직 메서드(Magic Methods)**: `print(붕어빵)`을 했을 때 예쁜 설명이 나오게 하거나(`__str__`), `붕어빵1 + 붕어빵2` 처럼 객체 간의 연산을 정의하는 등, 파이썬의 내장 기능을 우리 객체에 맞게 만드는 마법입니다.

```python
from abc import ABC, abstractmethod

class FranchiseManual(ABC): # 추상 클래스
    @abstractmethod # 추상 메서드
    def bake(self): pass

class MyStore(FranchiseManual): # 가맹점
    def bake(self): # '굽기' 규칙을 반드시 구현해야 함
        print("우리 가게만의 방식으로 붕어빵을 굽습니다.")

# --- 매직 메서드 예시 ---
# Bungeoppang 클래스에 아래 메서드를 추가했다고 가정
# def __str__(self): return f"{self.flavor} 붕어빵"
# def __eq__(self, other): return self.flavor == other.flavor

bpp1 = Bungeoppang("팥")
print(bpp1) # __str__ 호출 -> "팥 붕어빵"
bpp2 = Bungeoppang("팥")
print(bpp1 == bpp2) # __eq__ 호출 -> True
```

---

## 8. 연습 문제 (Exercises)

### 문제 1: 온라인 서점 시스템 모델링
- `Book`(책), `Member`(회원), `Order`(주문) 클래스를 `dataclass`를 사용하여 정의하고, 회원이 여러 권의 책을 주문하는 시나리오를 코드로 구현하세요.

**요구사항:**
- **`Book`**: `isbn`, `title`, `price` 속성
- **`Member`**: `member_id`, `name` 속성
- **`Order`**: `member`(`Member` 객체), `books`(`Book` 객체 리스트) 속성 및 `total_price()` (총 가격 계산) 메서드

### 문제 2: 게임 캐릭터 클래스 만들기
- 기본 `Character` 클래스와 이를 상속받는 `Warrior`, `Wizard` 클래스를 만드세요.

**요구사항:**
- **`Character`**: `name`, `hp`, `power` 속성 및 `attack(other)` 메서드
- **`Warrior`**: `attack` 메서드를 오버라이딩하여, 20% 확률로 2배의 데미지를 입히도록 수정
- **`Wizard`**: `attack` 메서드를 오버라이딩하고, `super()`를 사용하여 부모의 공격을 먼저 실행한 뒤, 자신의 `hp`를 회복하는 기능 추가

### 문제 3: AI 라이브러리 스타일의 클래스 설계 (Custom Dataset)
PyTorch, TensorFlow와 같은 딥러닝 라이브러리들은 상속과 다형성을 적극적으로 활용합니다. PyTorch의 `Dataset` 클래스를 모방하여, 우리만의 커스텀 데이터셋 클래스를 만들어보는 실습을 통해 라이브러리 내부 동작 원리를 이해해봅시다.

**요구사항:**
1.  `__len__`와 `__getitem__` 두 개의 추상 메서드를 가진 추상 기본 클래스 `BaseDataset`를 `abc` 모듈을 사용해 정의하세요.
2.  `BaseDataset`를 상속받는 `CustomTextDataset` 클래스를 구현하세요.
3.  `CustomTextDataset`는 생성자(`__init__`)에서 텍스트 데이터 리스트를 입력받아 저장합니다.
4.  상속받은 추상 메서드 `__len__`(데이터셋의 총 길이 반환)와 `__getitem__`(주어진 인덱스(i)에 해당하는 데이터 반환)을 구현해야 합니다.
5.  객체를 생성하고, `len()` 함수와 `[]` (인덱싱)을 사용하여 다형성이 어떻게 동작하는지 확인해보세요.

**예시 코드 구조:**
```python
from abc import ABC, abstractmethod

# 1. 추상 기본 클래스 정의
class BaseDataset(ABC):
    @abstractmethod
    def __len__(self):
        pass

    @abstractmethod
    def __getitem__(self, idx):
        pass

# 2. 상속 및 구현
class CustomTextDataset(BaseDataset):
    def __init__(self, texts):
        self.texts = texts

    def __len__(self):
        return len(self.texts)

    def __getitem__(self, idx):
        return self.texts[idx]

# 5. 다형성 동작 확인
my_texts = ["Hello AI", "Learn OOP", "PyTorch Style"]
dataset = CustomTextDataset(my_texts)

print(f"데이터셋 길이: {len(dataset)}") # __len__ 호출
print(f"첫 번째 데이터: {dataset[0]}") # __getitem__ 호출
print(f"마지막 데이터: {dataset[-1]}") # __getitem__ 호출

# for-loop도 가능해집니다.
for item in dataset:
    print(item)
```
이러한 구조는 PyTorch에서 모든 데이터셋이 공통된 인터페이스(`__len__`, `__getitem__`)를 갖도록 강제하여, `DataLoader`와 같은 다른 구성요소들이 어떤 종류의 데이터셋이든 동일한 방식으로 처리할 수 있게 하는 다형성의 핵심적인 예시입니다.

---

## 9. 트러블슈팅 (Troubleshooting)

- **`TypeError: __init__() missing 1 required positional argument: '...'` 오류가 발생했나요?**
  - 클래스의 객체를 생성할 때, `__init__` 메서드가 필요로 하는 모든 인자를 전달하지 않았다는 의미입니다. `my_object = MyClass(arg1, arg2)` 처럼 필수 인자가 모두 포함되었는지 확인하세요. `@dataclass`를 사용했다면, 기본값이 없는 모든 필드가 필수 인자입니다.
- **`self`는 왜 항상 첫 번째 매개변수로 써야 하나요?**
  - 파이썬 클래스에서 메서드가 호출될 때, 그 메서드를 호출한 객체 자신이 첫 번째 인자로 암묵적으로 전달됩니다. `self`는 이 객체 자신을 가리키는 관례적인 이름입니다. 이를 통해 메서드는 객체의 다른 속성이나 메서드(`self.attribute`, `self.method()`)에 접근할 수 있습니다.
- **`super()`는 언제 사용해야 하나요?**
  - 자식 클래스에서 부모 클래스의 메서드를 오버라이딩했지만, 부모의 원래 기능도 함께 호출하고 싶을 때 사용합니다. 예를 들어, 자식 클래스의 `__init__`에서 부모의 `__init__`을 먼저 호출하여 기본 속성을 설정한 뒤, 자식만의 속성을 추가하는 경우가 대표적입니다.
- **추상 클래스는 객체를 만들 수 없나요?**
  - 네, 없습니다. `abc.ABC`를 상속하고 `@abstractmethod`가 하나라도 있는 추상 클래스는 '미완성 설계도'와 같아서 직접 객체로 만들 수 없습니다. 반드시 이 클래스를 상"속받아 모든 추상 메서드를 구현한 자식 클래스만이 객체를 생성할 수 있습니다.

더 자세한 문제 해결 가이드나 다른 동료들이 겪은 문제에 대한 해결책이 궁금하다면, 아래 문서를 참고해주세요.

- **➡️ [Geumdo-Docs: TROUBLESHOOTING.md](../../../TROUBLESHOOTING.md)**

---

## 10. 되짚어보기 (Summary)

이번 주차에는 '붕어빵 비즈니스'를 통해 객체 지향 프로그래밍(OOP)을 배웠습니다.

- **클래스와 객체**: '붕어빵 틀'과 '붕어빵'으로 설계도와 실체를 구분했습니다.
- **상속**: '기본 틀'을 물려받아 '신제품 틀'을 만들며 코드 재사용성을 높였습니다.
- **다형성**: "주문하신 것 나왔습니다!" 라는 동일한 요청에 여러 종류의 붕어빵이 나갈 수 있는 유연성을 배웠습니다.
- **캡슐화와 추상화**: '비밀 레시피'를 보호하고(캡슐화), '프랜차이즈 매뉴얼'로 필수 기능을 강제(추상화)했습니다.

OOP는 복잡한 AI 라이브러리의 구조를 이해하고, 더 크고 체계적인 프로그램을 만드는 핵심 열쇠입니다.

## 11. 더 깊이 알아보기 (Further Reading)
- [파이썬 공식 튜토리얼: 클래스](https://docs.python.org/ko/3/tutorial/classes.html)
- [Real Python: Object-Oriented Programming (OOP) in Python 3](https://realpython.com/python3-object-oriented-programming/)

---

## 🚀 캡스톤 미니 프로젝트: 나만의 미니 AI 모델 프레임워크 구축

이번 장에서 배운 객체 지향 프로그래밍(OOP)의 꽃, 상속과 다형성을 활용하여 실제 AI 라이브러리의 구조를 모방한 미니 프레임워크를 만들어 봅니다. 이 프로젝트의 목표는 다양한 종류의 AI 모델들을 공통된 방식으로 관리하고 실행할 수 있는 체계를 설계하는 것입니다.

실제 모델의 복잡한 수학 대신, 각 모델이 어떻게 '객체'로서 추상화되고, '공통된 인터페이스'를 통해 유연하게 동작하는지에 집중합니다. 이 경험은 PyTorch, Scikit-learn과 같은 라이브러리의 내부 구조를 이해하는 훌륭한 기반이 될 것입니다.

### 프로젝트 목표

모든 AI 모델이 따라야 하는 규칙(추상 기본 클래스)을 정의하고, 이를 상속받는 여러 종류의 구체적인 모델(간단한 추천 모델, 감성 분석 모델)을 만든 뒤, 이 모델들을 일관된 방식으로 실행하는 파이프라인을 구축합니다.

**예상 실행 시나리오 및 출력:**
```
--- Running SimpleRecommender ---
[Train] 'SimpleRecommender' is training with ['songA', 'songB', 'songA']...
   - Model learned most popular items: ['songA']
[Predict] 'SimpleRecommender' is predicting...
   - Prediction result: Based on popularity, we recommend ['songA'].

--- Running SentimentClassifier ---
[Train] 'SentimentClassifier' is training with ['this is good', 'so bad', 'really great']...
   - Model learned keywords: {'positive': {'good', 'great'}, 'negative': {'bad'}}
[Predict] 'SentimentClassifier' is predicting for input: 'what a great movie'
   - Prediction result: positive
```

### 단계별 구현 가이드

**1. 모든 모델의 '설계도', `BaseModel` 추상 클래스 정의**
- `abc` 모듈을 사용하여 `BaseModel`이라는 이름의 추상 기본 클래스를 만드세요.
- `__init__` 에서는 `model_name`을 받아 속성으로 저장합니다.
- 모든 자식 클래스가 반드시 구현해야 할 두 개의 추상 메서드를 정의하세요.
  - `train(self, train_data)`
  - `predict(self, input_data)`

**2. `BaseModel`을 상속받는 구체적인 모델 클래스 구현**

**2-1. `SimpleRecommender` (인기 아이템 추천 모델)**
- `BaseModel`을 상속받습니다.
- `__init__`: 부모 클래스의 `__init__`을 `super()`로 호출합니다.
- `train`: 학습 데이터(e.g., 아이템 리스트)에서 가장 인기 있는 아이템(최빈값)을 찾아 인스턴스 변수(`self.popular_items`)에 저장하는 로직을 구현합니다. (`collections.Counter` 사용 추천)
- `predict`: `self.popular_items`에 저장된 아이템을 추천 결과로 반환합니다.

**2-2. `SentimentClassifier` (키워드 기반 감성 분석 모델)**
- `BaseModel`을 상속받습니다.
- `__init__`: 부모 클래스의 `__init__`을 `super()`로 호출합니다.
- `train`: 학습 데이터(e.g., 문장 리스트)를 받아, 'positive' 키워드('good', 'great' 등)와 'negative' 키워드('bad', 'terrible' 등)를 학습하여 인스턴스 변수(`self.keywords`)에 저장합니다.
- `predict`: 입력 문장에 학습된 키워드가 포함되어 있는지 확인하여 'positive', 'negative', 'neutral' 중 하나를 반환합니다.

**3. 모델 실행 파이프라인 구축 (다형성 활용)**
- `run_pipeline(models, train_data_dict, predict_data_dict)` 함수를 만드세요.
- 이 함수는 모델 객체의 리스트와 각 모델에 맞는 학습/예측 데이터를 받아 동작합니다.
- 리스트에 담긴 모델들을 하나씩 순회하며, 각 모델의 `train`과 `predict` 메서드를 **동일한 형태로 호출**합니다.
- `isinstance()`를 사용하여 각 모델 객체가 어떤 클래스의 인스턴스인지 확인하고, 그에 맞는 데이터를 전달해줍니다.

**4. 전체 로직 통합**

```python
from abc import ABC, abstractmethod
from collections import Counter

# 1. BaseModel 추상 클래스
class BaseModel(ABC):
    def __init__(self, model_name):
        self.model_name = model_name
    
    @abstractmethod
    def train(self, train_data):
        pass
    
    @abstractmethod
    def predict(self, input_data):
        pass

# 2-1. SimpleRecommender
class SimpleRecommender(BaseModel):
    def __init__(self, model_name="Popularity Recommender"):
        super().__init__(model_name)
        self.popular_items = None
        
    def train(self, train_data):
        print(f"[Train] '{self.model_name}' is training with {train_data}...")
        item_counts = Counter(train_data)
        self.popular_items = [item for item, count in item_counts.most_common(1)]
        print(f"   - Model learned most popular items: {self.popular_items}")
        
    def predict(self, input_data=None):
        print(f"[Predict] '{self.model_name}' is predicting...")
        if not self.popular_items:
            return "Model not trained yet."
        return f"Based on popularity, we recommend {self.popular_items}."

# 2-2. SentimentClassifier
class SentimentClassifier(BaseModel):
    def __init__(self, model_name="Keyword-based Classifier"):
        super().__init__(model_name)
        self.keywords = {'positive': set(), 'negative': set()}

    def train(self, train_data):
        print(f"[Train] '{self.model_name}' is training with {train_data}...")
        for sentence in train_data:
            if 'good' in sentence or 'great' in sentence:
                self.keywords['positive'].update(sentence.split())
            if 'bad' in sentence or 'terrible' in sentence:
                self.keywords['negative'].update(sentence.split())
        # 간단한 구현을 위해 불용어 처리 등은 생략
        self.keywords['positive'] &= {'good', 'great'} # 학습된 키워드만 남김
        self.keywords['negative'] &= {'bad', 'terrible'}
        print(f"   - Model learned keywords: {self.keywords}")

    def predict(self, input_data):
        print(f"[Predict] '{self.model_name}' is predicting for input: '{input_data}'")
        input_words = set(input_data.split())
        if self.keywords['positive'] & input_words:
            return "positive"
        elif self.keywords['negative'] & input_words:
            return "negative"
        return "neutral"

# 3. 모델 실행 파이프라인
def run_pipeline(models, train_data_dict, predict_data_dict):
    for model in models:
        model_type = type(model).__name__
        print(f"\n--- Running {model_type} ---")
        
        # 모델 타입에 맞는 데이터로 학습 및 예측
        train_data = train_data_dict.get(model_type)
        predict_data = predict_data_dict.get(model_type)
        
        if train_data:
            model.train(train_data)
        
        if predict_data:
            prediction = model.predict(predict_data)
            print(f"   - Prediction result: {prediction}")

# 4. 전체 로직 실행
if __name__ == "__main__":
    # 모델 객체 생성
    recommender = SimpleRecommender()
    classifier = SentimentClassifier()

    # 모델들을 리스트에 담기
    my_models = [recommender, classifier]
    
    # 각 모델에 맞는 데이터 준비
    train_data = {
        "SimpleRecommender": ['songA', 'songB', 'songA', 'songC', 'songA'],
        "SentimentClassifier": ['this is good movie', 'what a bad day', 'he is great person']
    }
    predict_data = {
        "SimpleRecommender": None, # 입력이 필요 없는 모델
        "SentimentClassifier": "the movie was really great"
    }

    # 파이프라인 실행
    run_pipeline(my_models, train_data, predict_data)
```

이 구조를 이해하면, 왜 AI 라이브러리들이 `model.fit(data)` 이나 `model.predict(new_data)` 와 같은 일관된 API를 제공하는지, 그리고 그것이 어떻게 다양한 종류의 모델에 적용될 수 있는지(다형성) 깊이 있게 파악할 수 있게 됩니다. 