모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

32강 / 전체 32강

클래스와 객체 — 객체지향 프로그래밍의 사고방식 전환

10분 읽기 조회 8

절차 지향에서 객체 지향으로의 사고 전환, 클래스·인스턴스 변수의 차이, 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

불러오는 중...