11. 클래스와 객체

🎯 이 장의 목표
  • 객체 지향(OOP)이 "무엇을 묶는" 발상인지 이해한다.
  • class로 클래스를 정의하고 __init__·self로 객체를 만든다.
  • 속성(데이터)과 메서드(동작)를 한 객체에 담는다.
  • 클래스 변수와 인스턴스 변수를 구분한다.

먼저: 객체 지향이 뭔가요?

지금까지 우리는 데이터(변수·자료구조)와 동작(함수)을 따로 다뤘습니다. 예를 들어 은행 계좌를 다룬다면, 잔액은 변수에 담고 입금·출금은 함수로 만들었죠. 데이터와 그 데이터를 다루는 함수가 코드 여기저기 흩어집니다.

객체 지향 프로그래밍(OOP, Object-Oriented Programming)은 발상을 바꿉니다. 서로 관련된 데이터와 동작을 하나의 덩어리(객체)로 묶자는 것입니다. 은행 계좌라면 "잔액"이라는 데이터와 "입금하기·출금하기"라는 동작을 한 묶음으로 만듭니다.

붕어빵 틀에 비유하면 이해가 쉽습니다.

  • 클래스(class) = 붕어빵 . "계좌란 이런 데이터와 동작을 가진다"는 설계도.
  • 객체(object) / 인스턴스(instance) = 틀로 찍어낸 붕어빵 하나하나. 실제 민지의 계좌, 현우의 계좌.
flowchart LR
    Class["🧇 클래스 (틀/설계도)<br/>BankAccount"]:::proc
    Class -->|찍어냄| O1["민지의 계좌<br/>잔액 1000"]:::data
    Class -->|찍어냄| O2["현우의 계좌<br/>잔액 5000"]:::data
    Class -->|찍어냄| O3["수빈의 계좌<br/>잔액 200"]:::data

    classDef proc fill:#7fd8d8,stroke:#2a9d8f,color:#14532d
    classDef data fill:#a8dadc,stroke:#457b9d,color:#1d3557
📌 핵심
핵심: 클래스는 설계도, 객체는 그 설계도로 만든 실체다. 하나의 클래스로 객체를 여러 개 찍어낼 수 있고, 각 객체는 자기만의 데이터를 가진다.

사실 우리는 이미 객체를 써왔습니다. 문자열의 .upper(), 리스트의 .append() — 이 점(.) 뒤의 메서드들이 바로 객체의 동작입니다. 문자열은 str 클래스의 객체, 리스트는 list 클래스의 객체였던 것이죠. 이제 우리가 직접 클래스를 만듭니다.

가장 단순한 클래스

class 키워드로 정의합니다. 클래스 이름은 관습적으로 대문자로 시작(PascalCase)합니다 — 변수의 snake_case와 구분됩니다.

PYTHON
class Dog:
    def bark(self):
        return "멍멍!"

# 객체(인스턴스) 만들기
my_dog = Dog()          # 클래스 이름 + () = 객체 생성
print(my_dog.bark())    # 멍멍!

Dog()처럼 클래스 이름에 괄호를 붙이면 객체가 만들어집니다. 그 객체의 메서드는 객체.메서드()로 호출합니다.

⚠️ 흔한 실수
bark(self)self가 낯설 텐데, 곧 자세히 설명합니다. 지금은 "메서드의 첫 인자는 항상 self"라고만 알아두세요.

__init__: 객체를 초기화하기

Dog은 모든 강아지가 똑같습니다. 이름·나이가 제각각인 강아지를 만들려면, 객체가 생길 때 초기 데이터를 받아야 합니다. 이 역할을 하는 특별한 메서드가 __init__(initialize, 초기화)입니다.

PYTHON
class Dog:
    def __init__(self, name, age):
        self.name = name      # 받은 name을 이 객체의 속성으로 저장
        self.age = age

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

choco = Dog("초코", 3)        # __init__이 자동 호출됨 (name="초코", age=3)
baduki = Dog("바둑이", 5)

print(choco.name)             # 초코
print(choco.bark())           # 초코: 멍멍!
print(baduki.bark())          # 바둑이: 멍멍!

Dog("초코", 3)로 객체를 만들면 __init__자동으로 불립니다. "초코"3name·age로 전달되고, self.name = name으로 이 객체에 저장됩니다.

📎 __init__처럼 앞뒤로 밑줄 두 개가 붙은 메서드를 던더 메서드(dunder method, double underscore) 또는 매직 메서드라고 부릅니다. Python이 특정 순간에 자동으로 호출하는 특별한 메서드들로, 12장에서 더 다룹니다. __init__은 "객체가 생성되는 순간"에 호출됩니다.

⭐ self: 자기 자신을 가리키는 말

self는 중급 입문자가 가장 헷갈리는 부분이니 천천히 봅시다.

self"지금 이 메서드를 호출한 바로 그 객체"를 가리킵니다. choco.bark()를 호출하면, 그 순간 selfchoco가 됩니다. baduki.bark()를 호출하면 selfbaduki가 되죠.

PYTHON
choco.bark()    # 이 호출에서 self = choco → self.name = "초코"
baduki.bark()   # 이 호출에서 self = baduki → self.name = "바둑이"

그래서 같은 bark 메서드인데도 객체마다 다른 이름이 나옵니다. self.name은 "나(이 객체)의 이름"이라는 뜻입니다.

왜 self가 첫 인자일까?

"메서드를 부를 때 self를 넘기지도 않는데, 어떻게 self에 객체가 들어가지?" 하는 의문이 들 겁니다. 사실 choco.bark()는 Python이 내부적으로 다음과 똑같이 처리합니다.

PYTHON
choco.bark()          # 우리가 쓰는 방식
Dog.bark(choco)       # ← Python이 실제로 하는 일 (둘은 완전히 동등!)

직접 확인해 봅시다.

PYTHON
print(choco.bark())       # 초코: 멍멍!
print(Dog.bark(choco))    # 초코: 멍멍!   ← 똑같은 결과!

점(.) 앞의 객체가 자동으로 첫 인자 self로 들어가는 것입니다. choco.bark()에서 점 앞의 chocoself 자리로 슬쩍 넘어갑니다. 그래서 메서드 정의에는 그 객체를 받을 self가 첫 자리에 꼭 있어야 합니다.

식당에 비유하면, 손님이 "(나) 물 주세요"라고 할 때 "(나)"를 매번 말 안 해도 점원은 말한 그 손님에게 물을 줍니다. choco.bark()choco가 바로 그 "(나)"이고, 메서드 안에서는 self로 그 손님을 가리킵니다.

flowchart TD
    Call1["choco.bark() 호출"]:::user --> Auto1["Dog.bark(choco) 로 변환<br/>choco가 self 자리로"]:::proc
    Auto1 --> R1["self.name → '초코'"]:::result
    Call2["baduki.bark() 호출"]:::user --> Auto2["Dog.bark(baduki) 로 변환<br/>baduki가 self 자리로"]:::proc
    Auto2 --> R2["self.name → '바둑이'"]:::result

    classDef user fill:#fff3b0,stroke:#e0a800,color:#5c4500
    classDef proc fill:#7fd8d8,stroke:#2a9d8f,color:#14532d
    classDef result fill:#b8e6c1,stroke:#34a853,color:#14532d
📌 핵심
핵심: 클래스 안 모든 메서드의 첫 매개변수는 self이고, 이는 "그 메서드를 호출한 객체 자신"이다. obj.method()Class.method(obj)와 같다 — 점 앞의 객체가 self로 들어간다. self.속성은 그 객체의 데이터에 접근하는 길이다.
⚠️ 흔한 실수
흔한 함정 1 — self 빼먹기: 메서드 정의에서 self를 빠뜨리면 에러가 납니다.
```python
class Dog:
def bark(): # self 없음!
return "멍멍"
Dog().bark()
```
```text
TypeError: bark() takes 0 positional arguments but 1 was given
```
우리는 인자 없이 bark()를 불렀는데 "1개가 전달됐다"는 게 이상하죠? Python이 객체 자신(self)을 자동으로 첫 인자로 넘기기 때문입니다. 그래서 받을 self가 정의에 꼭 있어야 합니다.
⚠️ 흔한 실수
흔한 함정 2 — self. 빼먹기: 속성에 접근할 때 self.를 빠뜨리면 그냥 지역 변수를 찾다가 NameError가 나거나 의도와 다르게 동작합니다. 객체의 데이터는 항상 self.name처럼 접근하세요.

속성과 메서드: 데이터와 동작

객체에 담기는 두 가지를 정리합시다.

  • 속성(attribute): 객체가 가진 데이터. self.name, self.age 같은 것. (인스턴스 변수라고도 함)
  • 메서드(method): 객체가 할 수 있는 동작. 클래스 안에 정의된 함수. bark() 같은 것.

은행 계좌 예제로 속성과 메서드가 어우러지는 모습을 봅시다. 메서드가 self.balance를 읽고 바꾸는 것에 주목하세요.

PYTHON
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner          # 속성: 예금주
        self.balance = balance      # 속성: 잔액 (기본값 0)

    def deposit(self, amount):      # 메서드: 입금
        self.balance += amount
        return self.balance

    def withdraw(self, amount):     # 메서드: 출금
        if amount > self.balance:
            return "잔액 부족"
        self.balance -= amount
        return self.balance

acc = BankAccount("민지", 1000)
print(acc.deposit(500))     # 1500   (1000 + 500)
print(acc.withdraw(2000))   # 잔액 부족
print(acc.withdraw(300))    # 1200   (1500 - 300)
print(acc.balance)          # 1200   (속성 직접 확인)

데이터(balance)와 그것을 다루는 동작(deposit·withdraw)이 한 객체 안에 묶여 있습니다. 이것이 OOP의 핵심 이점입니다 — 관련된 것끼리 모여 있어 관리하기 쉽습니다.

클래스 변수 vs 인스턴스 변수

지금까지 본 self.name 같은 속성은 인스턴스 변수입니다. 객체마다 따로 가집니다. 반면 클래스 변수는 모든 객체가 공유합니다. __init__ 밖, 클래스 본문에 바로 적습니다.

PYTHON
class Dog:
    species = "개"              # 클래스 변수 (모든 객체가 공유)

    def __init__(self, name):
        self.name = name        # 인스턴스 변수 (객체마다 개별)

choco = Dog("초코")
baduki = Dog("바둑이")

print(choco.species, baduki.species)   # 개 개      (공유)
print(choco.name, baduki.name)         # 초코 바둑이  (개별)
구분위치누가 가지나
클래스 변수클래스 본문 직접모든 객체가 공유species = "개"
인스턴스 변수__init__self.x객체마다 개별self.name
💡 팁
클래스 변수는 "그 종류 전체에 공통인 값"(모든 개의 종은 "개")에, 인스턴스 변수는 "개체마다 다른 값"(이름)에 씁니다. 헷갈리면 대부분의 데이터는 인스턴스 변수(self.)로 두는 게 안전합니다.

나쁜 예 ❌ vs 좋은 예 ✅

관련 데이터와 동작이 흩어진 절차적 코드와, 클래스로 묶은 코드를 비교해 봅시다.

PYTHON
# ❌ 나쁜 예: 데이터와 동작이 흩어짐
account1_owner = "민지"
account1_balance = 1000
account2_owner = "현우"
account2_balance = 5000

def deposit(balance, amount):
    return balance + amount

account1_balance = deposit(account1_balance, 500)
# 계좌가 늘어날수록 변수와 함수 관리가 악몽이 됨

# ✅ 좋은 예: 클래스로 묶기
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
    def deposit(self, amount):
        self.balance += amount

acc1 = BankAccount("민지", 1000)
acc2 = BankAccount("현우", 5000)
acc1.deposit(500)       # 계좌가 자기 데이터와 동작을 가짐

클래스를 쓰면 계좌가 100개여도 객체 100개로 깔끔하게 관리됩니다. 각 객체가 자기 데이터를 책임지기 때문입니다.

이 장에서 배운 것

  • OOP는 관련된 데이터와 동작을 객체로 묶는 발상이다. 클래스는 설계도, 객체(인스턴스)는 그것으로 만든 실체다.
  • class 이름:으로 정의하고(대문자 시작), 이름()으로 객체를 만든다.
  • __init__은 객체 생성 시 자동 호출되는 초기화 메서드다. self.속성 = 값으로 데이터를 저장한다.
  • self는 "메서드를 호출한 객체 자신"이다. 모든 메서드의 첫 인자이며, self.속성으로 객체 데이터에 접근한다.
  • 인스턴스 변수(self.x)는 객체마다 개별, 클래스 변수(클래스 본문 직접)는 모든 객체가 공유한다.

🧪 실습 문제

문제 1. nameprice를 받아 저장하는 Product 클래스를 만들고, "상품명: 가격원" 형태의 문자열을 돌려주는 info() 메서드를 작성하세요. Product("연필", 500).info()를 출력하세요.

문제 2. 다음 코드는 왜 에러가 날까요? 어떻게 고치나요?

PYTHON
class Cat:
    def meow(self):
        return f"{name} 야옹"   # ?
c = Cat()
print(c.meow())

문제 3. Counter 클래스를 만드세요. __init__에서 self.count = 0으로 시작하고, increment() 메서드를 부를 때마다 1씩 증가시킵니다. 객체를 만들어 increment()를 3번 부른 뒤 count를 출력하세요.

문제 4. 다음 코드의 출력은? species는 클래스 변수입니다.

PYTHON
class Bird:
    species = "새"
    def __init__(self, name):
        self.name = name

a = Bird("참새")
b = Bird("까치")
print(a.species, a.name, b.name)

문제 5. Rectangle 클래스를 만드세요. width·height를 받아 저장하고, 넓이를 돌려주는 area()와 둘레를 돌려주는 perimeter() 메서드를 작성하세요. Rectangle(4, 5)의 넓이와 둘레를 출력하세요.

<details>

<summary>✅ 정답·해설 보기</summary>

1.

PYTHON
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    def info(self):
        return f"{self.name}: {self.price}원"

print(Product("연필", 500).info())   # 연필: 500원

2. meow 안에서 name을 그냥 썼는데, 객체의 속성은 self.name으로 접근해야 합니다. 그대로 두면 NameError. 또한 __init__이 없어 name 속성 자체가 없으니, 다음처럼 고쳐야 합니다.

PYTHON
class Cat:
    def __init__(self, name):
        self.name = name
    def meow(self):
        return f"{self.name} 야옹"
c = Cat("나비")
print(c.meow())   # 나비 야옹

3.

PYTHON
class Counter:
    def __init__(self):
        self.count = 0
    def increment(self):
        self.count += 1

c = Counter()
c.increment(); c.increment(); c.increment()
print(c.count)   # 3

4. 새 참새 까치. species는 클래스 변수라 공유되고, name은 인스턴스마다 개별입니다.

5.

PYTHON
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def area(self):
        return self.width * self.height
    def perimeter(self):
        return 2 * (self.width + self.height)

r = Rectangle(4, 5)
print(r.area())        # 20
print(r.perimeter())   # 18

</details>

◀️ 목차 | ▶️ 다음 장: 12. 상속과 매직 메서드