절차 지향에서 객체 지향으로의 사고 전환, 클래스·인스턴스 변수의 차이, self의 의미를 깊이 이해한다.
🎯 이 강의에서 배우는 것
객체지향 프로그래밍(OOP)은 단순한 문법이 아닙니다. 사고방식의 전환입니다. 데이터와 그것을 다루는 함수를 분리하던 절차 지향에서, 둘을 하나의 단위(객체)로 묶는 방식으로 프로그램을 설계합니다. 이 강의에서 왜 OOP가 등장했고, 클래스와 인스턴스가 메모리에서 어떻게 존재하는지 배웁니다.
🔄 절차 지향 → 객체 지향 — 사고 전환
# 절차 지향: 데이터와 함수가 분리됨
account_balance = 100000
account_owner = "홍길동"
def deposit(balance, amount):
return balance + amount
def withdraw(balance, amount):
if amount > balance:
raise ValueError("잔액 부족")
return balance - amount
account_balance = deposit(account_balance, 50000)
print(f"{account_owner}: {account_balance}원")
# 계좌가 여러 개이면?
acc2_balance = 200000
acc2_owner = "이순신"
# 변수가 폭발적으로 늘어남...
# 객체 지향: 데이터와 메서드를 하나로 묶음
class BankAccount:
def __init__(self, owner: str, balance: float = 0):
self.owner = owner # 인스턴스 변수
self._balance = balance # _ 접두사: "내부 사용" 관례
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("입금액은 양수여야 합니다")
self._balance += amount
def withdraw(self, amount: float) -> None:
if amount <= 0:
raise ValueError("출금액은 양수여야 합니다")
if amount > self._balance:
raise ValueError("잔액 부족")
self._balance -= amount
def get_balance(self) -> float:
return self._balance
def __str__(self) -> str:
return f"[{self.owner}] 잔액: {self._balance:,}원"
# 여러 계좌를 쉽게 관리
acc1 = BankAccount("홍길동", 100000)
acc2 = BankAccount("이순신", 200000)
acc1.deposit(50000)
acc2.withdraw(30000)
print(acc1) # [홍길동] 잔액: 150,000원
print(acc2) # [이순신] 잔액: 170,000원
🔧 class 문의 실행 원리
print("class 정의 전")
class Dog:
species = "Canis lupus familiaris" # 클래스 변수
def __init__(self, name: str, age: int):
self.name = name # 인스턴스 변수
self.age = age
def bark(self):
return f"{self.name}: 왈왈!"
print("class 정의 후")
# class 문은 런타임에 실행됨:
# 1. 새 네임스페이스(딕셔너리) 생성
# 2. 클래스 본문을 그 네임스페이스에서 실행
# 3. type(name, bases, namespace)로 클래스 객체 생성
# 4. 이름(Dog)을 클래스 객체에 바인딩
# 클래스 자체도 객체 (type의 인스턴스)
print(type(Dog)) # <class 'type'>
print(Dog.__dict__) # 클래스의 네임스페이스
print(Dog.species) # 클래스 변수
print(Dog.__name__) # 'Dog'
print(Dog.__module__) # '__main__'
# 인스턴스 생성
d1 = Dog("뽀삐", 3)
d2 = Dog("초코", 5)
print(d1.__dict__) # {'name': '뽀삐', 'age': 3}
print(d2.__dict__) # {'name': '초코', 'age': 5}
print(d1.__class__) # <class '__main__.Dog'>
🔍 인스턴스 변수 vs 클래스 변수
class Counter:
count = 0 # 클래스 변수: 모든 인스턴스가 공유
def __init__(self, name):
self.name = name # 인스턴스 변수: 각 인스턴스마다 독립
Counter.count += 1 # 클래스 변수 수정
def get_info(self):
return f"{self.name} (전체 카운터: {Counter.count})"
c1 = Counter("첫 번째")
c2 = Counter("두 번째")
c3 = Counter("세 번째")
print(c1.get_info()) # 첫 번째 (전체 카운터: 3)
print(Counter.count) # 3 — 클래스에서 직접 접근 가능
# 속성 탐색 순서: 인스턴스 __dict__ → 클래스 __dict__ → 상위 클래스
class Shared:
value = "클래스 변수"
s1 = Shared()
s2 = Shared()
print(s1.value) # 클래스 변수 (인스턴스에 없으니 클래스에서 찾음)
s1.value = "인스턴스 변수" # s1의 __dict__에 새 항목 생성 — 클래스 변수 가리기!
print(s1.value) # 인스턴스 변수 (인스턴스에서 찾음)
print(s2.value) # 클래스 변수 (s2는 인스턴스 변수 없음)
print(Shared.value) # 클래스 변수 (변경 안 됨!)
# 가변 객체 클래스 변수의 함정
class Buggy:
items = [] # 클래스 변수가 리스트면 위험!
b1 = Buggy()
b2 = Buggy()
b1.items.append("a") # 클래스 변수 수정 (새 인스턴스 변수 생성이 아님!)
print(b2.items) # ['a'] ← b2도 영향받음!
# 올바른 패턴: __init__에서 인스턴스 변수로
class Fixed:
def __init__(self):
self.items = [] # 인스턴스마다 새 리스트
🤝 self의 의미 — 바인딩 메서드
class Person:
def __init__(self, name):
self.name = name
def greet(self):
return f"안녕하세요, {self.name}입니다!"
p = Person("홍길동")
# 두 가지 호출 방법 — 완전히 동일
print(p.greet()) # 바운드 메서드 호출
print(Person.greet(p)) # 언바운드 방식 (self 명시)
# 파이썬의 메서드 바인딩:
# p.greet 은 greet 함수와 인스턴스 p를 묶은 바운드 메서드
print(type(p.greet)) # <class 'method'>
print(type(Person.greet)) # <class 'function'>
print(p.greet.__func__ is Person.greet) # True — 같은 함수
# self는 관례 — 기술적으로는 어떤 이름이든 가능하지만 절대 바꾸지 마세요
class Weird:
def method(this): # 작동은 하지만 절대 하지 마세요!
return this
💾 __slots__ — 메모리 최적화
import sys
class NormalPoint:
def __init__(self, x, y):
self.x = x
self.y = y
class SlottedPoint:
__slots__ = ["x", "y"] # 딕셔너리 대신 고정 슬롯 사용
def __init__(self, x, y):
self.x = x
self.y = y
np = NormalPoint(1, 2)
sp = SlottedPoint(1, 2)
print(f"일반: {sys.getsizeof(np) + sys.getsizeof(np.__dict__)} bytes")
print(f"슬롯: {sys.getsizeof(sp)} bytes (dict 없음)")
# __slots__의 제한
# 1. __dict__ 없음 → 동적 속성 추가 불가
try:
sp.z = 3 # AttributeError: 'SlottedPoint' object has no attribute 'z'
except AttributeError as e:
print(e)
# 2. 상속에서 자식이 __slots__ 없으면 다시 __dict__ 생성됨
# 3. 수백만 개의 인스턴스를 만들 때 유용
⚠️ 자주 하는 실수
- 가변 객체를 클래스 변수로 선언:
class C: items = []는 모든 인스턴스가 공유합니다. 각 인스턴스에 독립적인 리스트가 필요하면def __init__(self): self.items = []로 선언하세요. - self를 빠뜨림: 인스턴스 메서드에서
self를 첫 번째 매개변수로 선언하지 않으면 TypeError가 발생합니다. 클래스 메서드와 정적 메서드는 예외입니다. - 클래스 변수와 인스턴스 변수 혼동:
instance.class_var = new_value는 클래스 변수를 수정하는 것이 아니라 인스턴스에 같은 이름의 새 변수를 만들어 클래스 변수를 가립니다.
📝 정리 및 다음 강의 예고
- 클래스는 데이터(속성)와 행동(메서드)을 묶는 설계도. 인스턴스는 그 설계도로 만든 실체.
- 클래스 변수는 모든 인스턴스 공유, 인스턴스 변수는 각 인스턴스에 독립적.
self: 인스턴스 자신.instance.method()는Class.method(instance)와 동일.__slots__: 대량 인스턴스에서 메모리 절약. __dict__ 없어 동적 속성 추가 불가.
다음 강의: 메서드의 종류 — 인스턴스 메서드, 클래스 메서드(@classmethod), 스태틱 메서드(@staticmethod)의 차이와 언제 어떤 것을 써야 하는지 배웁니다.
관련 주제
- 절차지향 vs 객체지향
- __init__ vs __new__
- 인스턴스 변수·클래스 변수
- self 바인딩 원리
- __slots__ 메모리 절약
- 은행계좌 클래스 설계
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
파이썬 기초 40강 — 처음 배우는 프로그래밍
이 강의가 속한 시리즈는 총 32강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
0/1000
불러오는 중...
