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와 구분됩니다.
class Dog: def bark(self): return "멍멍!" # 객체(인스턴스) 만들기 my_dog = Dog() # 클래스 이름 + () = 객체 생성 print(my_dog.bark()) # 멍멍!
Dog()처럼 클래스 이름에 괄호를 붙이면 객체가 만들어집니다. 그 객체의 메서드는 객체.메서드()로 호출합니다.
bark(self)의 self가 낯설 텐데, 곧 자세히 설명합니다. 지금은 "메서드의 첫 인자는 항상 self"라고만 알아두세요.__init__: 객체를 초기화하기
위 Dog은 모든 강아지가 똑같습니다. 이름·나이가 제각각인 강아지를 만들려면, 객체가 생길 때 초기 데이터를 받아야 합니다. 이 역할을 하는 특별한 메서드가 __init__(initialize, 초기화)입니다.
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__이 자동으로 불립니다. "초코"와 3이 name·age로 전달되고, self.name = name으로 이 객체에 저장됩니다.
📎__init__처럼 앞뒤로 밑줄 두 개가 붙은 메서드를 던더 메서드(dunder method, double underscore) 또는 매직 메서드라고 부릅니다. Python이 특정 순간에 자동으로 호출하는 특별한 메서드들로, 12장에서 더 다룹니다.__init__은 "객체가 생성되는 순간"에 호출됩니다.
⭐ self: 자기 자신을 가리키는 말
self는 중급 입문자가 가장 헷갈리는 부분이니 천천히 봅시다.
self는 "지금 이 메서드를 호출한 바로 그 객체"를 가리킵니다. choco.bark()를 호출하면, 그 순간 self는 choco가 됩니다. baduki.bark()를 호출하면 self는 baduki가 되죠.
choco.bark() # 이 호출에서 self = choco → self.name = "초코" baduki.bark() # 이 호출에서 self = baduki → self.name = "바둑이"
그래서 같은 bark 메서드인데도 객체마다 다른 이름이 나옵니다. self.name은 "나(이 객체)의 이름"이라는 뜻입니다.
왜 self가 첫 인자일까?
"메서드를 부를 때 self를 넘기지도 않는데, 어떻게 self에 객체가 들어가지?" 하는 의문이 들 겁니다. 사실 choco.bark()는 Python이 내부적으로 다음과 똑같이 처리합니다.
choco.bark() # 우리가 쓰는 방식 Dog.bark(choco) # ← Python이 실제로 하는 일 (둘은 완전히 동등!)
직접 확인해 봅시다.
print(choco.bark()) # 초코: 멍멍! print(Dog.bark(choco)) # 초코: 멍멍! ← 똑같은 결과!
즉 점(.) 앞의 객체가 자동으로 첫 인자 self로 들어가는 것입니다. choco.bark()에서 점 앞의 choco가 self 자리로 슬쩍 넘어갑니다. 그래서 메서드 정의에는 그 객체를 받을 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.속성은 그 객체의 데이터에 접근하는 길이다.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가 정의에 꼭 있어야 합니다.self.를 빠뜨리면 그냥 지역 변수를 찾다가 NameError가 나거나 의도와 다르게 동작합니다. 객체의 데이터는 항상 self.name처럼 접근하세요.속성과 메서드: 데이터와 동작
객체에 담기는 두 가지를 정리합시다.
- 속성(attribute): 객체가 가진 데이터.
self.name,self.age같은 것. (인스턴스 변수라고도 함) - 메서드(method): 객체가 할 수 있는 동작. 클래스 안에 정의된 함수.
bark()같은 것.
은행 계좌 예제로 속성과 메서드가 어우러지는 모습을 봅시다. 메서드가 self.balance를 읽고 바꾸는 것에 주목하세요.
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__ 밖, 클래스 본문에 바로 적습니다.
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 좋은 예 ✅
관련 데이터와 동작이 흩어진 절차적 코드와, 클래스로 묶은 코드를 비교해 봅시다.
# ❌ 나쁜 예: 데이터와 동작이 흩어짐 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. name과 price를 받아 저장하는 Product 클래스를 만들고, "상품명: 가격원" 형태의 문자열을 돌려주는 info() 메서드를 작성하세요. Product("연필", 500).info()를 출력하세요.
문제 2. 다음 코드는 왜 에러가 날까요? 어떻게 고치나요?
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는 클래스 변수입니다.
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.
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 속성 자체가 없으니, 다음처럼 고쳐야 합니다.
class Cat: def __init__(self, name): self.name = name def meow(self): return f"{self.name} 야옹" c = Cat("나비") print(c.meow()) # 나비 야옹
3.
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.
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. 상속과 매직 메서드