본문으로 건너뛰기
Advertisement

클래스와 인스턴스

객체지향 프로그래밍(OOP)은 데이터와 그 데이터를 처리하는 함수를 하나의 단위인 객체로 묶는 프로그래밍 패러다임입니다. Python은 처음부터 OOP를 지원하도록 설계된 언어이며, 모든 것이 객체입니다. 이 챕터에서는 클래스와 인스턴스의 기본 개념부터 시작하여 Python OOP의 핵심을 이해합니다.


클래스란 무엇인가?

**클래스(Class)**는 객체를 만들기 위한 설계도(청사진)입니다. 붕어빵 틀을 생각해보세요. 틀 자체는 클래스이고, 틀로 만든 붕어빵이 인스턴스(객체)입니다.

  • 클래스: 객체의 속성(데이터)과 행동(메서드)을 정의하는 템플릿
  • 인스턴스: 클래스를 기반으로 실제로 생성된 개별 객체
  • 속성(Attribute): 객체가 가지는 데이터 (변수)
  • 메서드(Method): 객체가 수행하는 행동 (함수)

class 키워드로 클래스 정의하기

class Dog:
"""개를 나타내는 클래스"""

# 클래스 변수 (모든 인스턴스가 공유)
species = "Canis familiaris"

# 초기화 메서드 (생성자)
def __init__(self, name: str, age: int):
# 인스턴스 변수 (각 인스턴스마다 다름)
self.name = name
self.age = age

# 인스턴스 메서드
def bark(self) -> str:
return f"{self.name}이(가) 짖습니다: 왈왈!"

def describe(self) -> str:
return f"{self.name}은(는) {self.age}{self.species}입니다."


# 인스턴스 생성
dog1 = Dog("바둑이", 3)
dog2 = Dog("초코", 5)

print(dog1.bark()) # 바둑이이(가) 짖습니다: 왈왈!
print(dog2.describe()) # 초코은(는) 5살 Canis familiaris입니다.
print(dog1.species) # Canis familiaris
print(Dog.species) # Canis familiaris (클래스에서 직접 접근)

__init__ 메서드: 객체 초기화

__init__은 객체가 생성될 때 자동으로 호출되는 초기화 메서드입니다. 인스턴스 변수를 설정하는 데 사용됩니다.

class Person:
def __init__(self, name: str, age: int, email: str = ""):
self.name = name # 필수 매개변수
self.age = age # 필수 매개변수
self.email = email # 기본값이 있는 선택적 매개변수
self._history = [] # 외부에서 직접 설정하지 않는 내부 속성

def introduce(self) -> str:
return f"안녕하세요, 저는 {self.name}이고 {self.age}살입니다."


# 다양한 방식으로 인스턴스 생성
p1 = Person("김철수", 25)
p2 = Person("이영희", 30, "lee@example.com")
p3 = Person(name="박민준", age=22, email="park@example.com")

print(p1.introduce())
print(p2.email) # lee@example.com
print(p3.name) # 박민준

self 파라미터: 인스턴스 자신을 가리킴

self는 메서드가 호출되는 인스턴스 자신을 가리키는 참조입니다. Python은 메서드를 호출할 때 해당 인스턴스를 자동으로 첫 번째 인수로 전달합니다.

class Counter:
def __init__(self):
self.count = 0

def increment(self):
self.count += 1 # self.count는 이 인스턴스의 count 속성
return self # 메서드 체이닝을 위해 self 반환

def reset(self):
self.count = 0
return self

def value(self) -> int:
return self.count


c1 = Counter()
c2 = Counter()

# 각 인스턴스는 독립적인 state를 가짐
c1.increment().increment().increment()
c2.increment()

print(c1.value()) # 3
print(c2.value()) # 1

# self가 인스턴스를 가리키는 원리 확인
print(c1 is c1.increment().__class__.__mro__[0]) # False
# Counter.increment(c1)과 c1.increment()는 동일
Counter.increment(c1)
print(c1.value()) # 4 (언바운드 메서드 호출도 가능)

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

이 구분을 정확히 이해하는 것이 매우 중요합니다. 혼동하면 버그가 발생하기 쉽습니다.

class Student:
# 클래스 변수: 모든 인스턴스가 공유
school = "파이썬 고등학교"
total_students = 0

def __init__(self, name: str, grade: int):
# 인스턴스 변수: 각 인스턴스마다 독립적
self.name = name
self.grade = grade
Student.total_students += 1 # 클래스 변수 수정

@classmethod
def get_total(cls) -> int:
return cls.total_students


s1 = Student("Alice", 10)
s2 = Student("Bob", 11)
s3 = Student("Charlie", 10)

print(Student.total_students) # 3
print(Student.get_total()) # 3

# 클래스 변수는 모든 인스턴스에서 접근 가능
print(s1.school) # 파이썬 고등학교
print(s2.school) # 파이썬 고등학교

⚠️ 인스턴스 변수가 클래스 변수를 가리는 함정

class Config:
debug = False # 클래스 변수

def __init__(self, name: str):
self.name = name


cfg1 = Config("app1")
cfg2 = Config("app2")

# 클래스 변수 변경 → 모든 인스턴스에 영향
Config.debug = True
print(cfg1.debug) # True
print(cfg2.debug) # True

# 인스턴스를 통해 클래스 변수를 "수정"하면 인스턴스 변수가 생성됨!
cfg1.debug = False # cfg1에만 인스턴스 변수 debug 생성
print(cfg1.debug) # False (인스턴스 변수)
print(cfg2.debug) # True (여전히 클래스 변수)
print(Config.debug) # True (클래스 변수는 변하지 않음)

# MRO 확인: 인스턴스 변수 → 클래스 변수 순으로 탐색
print(cfg1.__dict__) # {'name': 'app1', 'debug': False}
print(Config.__dict__) # {..., 'debug': True, ...}

가변 객체를 클래스 변수로 사용할 때의 위험

# 잘못된 예시 ❌
class BadList:
items = [] # 가변 클래스 변수 — 모든 인스턴스가 공유!

def add(self, item):
self.items.append(item) # 공유된 리스트를 수정!


bad1 = BadList()
bad2 = BadList()
bad1.add("사과")
print(bad2.items) # ['사과'] — 의도하지 않은 공유!


# 올바른 예시 ✅
class GoodList:
def __init__(self):
self.items = [] # 인스턴스 변수로 선언

def add(self, item):
self.items.append(item)


good1 = GoodList()
good2 = GoodList()
good1.add("사과")
print(good2.items) # [] — 독립적

__dict__로 인스턴스 속성 확인하기

class Rectangle:
shape_type = "사각형" # 클래스 변수

def __init__(self, width: float, height: float):
self.width = width
self.height = height

def area(self) -> float:
return self.width * self.height


rect = Rectangle(5.0, 3.0)

# 인스턴스의 속성 딕셔너리 확인
print(rect.__dict__) # {'width': 5.0, 'height': 3.0}

# 클래스의 속성 딕셔너리 확인
print(Rectangle.__dict__)
# {'shape_type': '사각형', '__init__': <function ...>, 'area': <function ...>, ...}

# hasattr, getattr, setattr, delattr 활용
print(hasattr(rect, 'width')) # True
print(hasattr(rect, 'color')) # False
print(getattr(rect, 'width')) # 5.0
print(getattr(rect, 'color', 'white')) # 'white' (기본값)

setattr(rect, 'color', 'blue')
print(rect.color) # blue
print(rect.__dict__) # {'width': 5.0, 'height': 3.0, 'color': 'blue'}

delattr(rect, 'color')
print(rect.__dict__) # {'width': 5.0, 'height': 3.0}

실전 예제: 은행 계좌 클래스

from datetime import datetime


class BankAccount:
"""은행 계좌를 나타내는 클래스"""

interest_rate = 0.02 # 연이자율 (클래스 변수)
_account_count = 0 # 전체 계좌 수 (클래스 변수)

def __init__(self, owner: str, initial_balance: float = 0.0):
if initial_balance < 0:
raise ValueError("초기 잔액은 0 이상이어야 합니다.")

BankAccount._account_count += 1
self.owner = owner
self._balance = initial_balance
self._account_number = f"ACC-{BankAccount._account_count:04d}"
self._transactions: list[dict] = []
self._created_at = datetime.now()

def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("입금액은 양수여야 합니다.")
self._balance += amount
self._record_transaction("입금", amount)
print(f"[{self._account_number}] {amount:,.0f}원 입금 완료. 잔액: {self._balance:,.0f}원")

def withdraw(self, amount: float) -> None:
if amount <= 0:
raise ValueError("출금액은 양수여야 합니다.")
if amount > self._balance:
raise ValueError(f"잔액이 부족합니다. 현재 잔액: {self._balance:,.0f}원")
self._balance -= amount
self._record_transaction("출금", amount)
print(f"[{self._account_number}] {amount:,.0f}원 출금 완료. 잔액: {self._balance:,.0f}원")

def get_balance(self) -> float:
return self._balance

def apply_interest(self) -> None:
interest = self._balance * self.interest_rate
self._balance += interest
self._record_transaction("이자", interest)
print(f"이자 {interest:,.0f}원 적용. 잔액: {self._balance:,.0f}원")

def get_statement(self) -> str:
lines = [
f"=== 계좌 내역 ===",
f"계좌번호: {self._account_number}",
f"소유자: {self.owner}",
f"개설일: {self._created_at.strftime('%Y-%m-%d')}",
f"현재 잔액: {self._balance:,.0f}원",
f"거래 내역:",
]
for tx in self._transactions:
lines.append(f" {tx['time']} | {tx['type']}: {tx['amount']:,.0f}원")
return "\n".join(lines)

def _record_transaction(self, tx_type: str, amount: float) -> None:
self._transactions.append({
"type": tx_type,
"amount": amount,
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
})

@classmethod
def get_account_count(cls) -> int:
return cls._account_count

def __repr__(self) -> str:
return f"BankAccount(owner={self.owner!r}, balance={self._balance})"


# 사용 예시
acc1 = BankAccount("김철수", 1_000_000)
acc2 = BankAccount("이영희", 500_000)

acc1.deposit(200_000)
acc1.withdraw(150_000)
acc1.apply_interest()

print(acc1.get_statement())
print(f"\n전체 계좌 수: {BankAccount.get_account_count()}")

실전 예제: 학생 클래스

from dataclasses import field
from typing import Optional


class Student:
"""학생 정보를 관리하는 클래스"""

_grade_scale = {
'A+': 4.5, 'A': 4.0,
'B+': 3.5, 'B': 3.0,
'C+': 2.5, 'C': 2.0,
'D': 1.0, 'F': 0.0,
}

def __init__(self, student_id: str, name: str, major: str):
self.student_id = student_id
self.name = name
self.major = major
self._grades: dict[str, str] = {} # {과목: 등급}

def add_grade(self, subject: str, grade: str) -> None:
if grade not in self._grade_scale:
raise ValueError(f"유효하지 않은 등급: {grade}")
self._grades[subject] = grade
print(f"{self.name} - {subject}: {grade} 등록 완료")

def calculate_gpa(self) -> Optional[float]:
if not self._grades:
return None
total = sum(self._grade_scale[g] for g in self._grades.values())
return round(total / len(self._grades), 2)

def get_transcript(self) -> str:
gpa = self.calculate_gpa()
lines = [
f"학번: {self.student_id}",
f"이름: {self.name}",
f"전공: {self.major}",
f"성적표:",
]
for subject, grade in self._grades.items():
points = self._grade_scale[grade]
lines.append(f" {subject}: {grade} ({points})")
lines.append(f"평점: {gpa if gpa is not None else 'N/A'}")
return "\n".join(lines)

def __str__(self) -> str:
gpa = self.calculate_gpa()
return f"Student({self.student_id}, {self.name}, GPA={gpa})"


# 사용 예시
student = Student("2024001", "김파이썬", "컴퓨터공학")
student.add_grade("자료구조", "A+")
student.add_grade("알고리즘", "A")
student.add_grade("데이터베이스", "B+")
student.add_grade("운영체제", "A")

print(student.get_transcript())
print(f"\nGPA: {student.calculate_gpa()}")
print(str(student))

고수 팁

1. __init__ 에서 유효성 검사를 철저히 하라

class Temperature:
def __init__(self, celsius: float):
if celsius < -273.15:
raise ValueError(f"절대 영도({celsius}°C)보다 낮은 온도는 없습니다.")
self._celsius = celsius

@property
def celsius(self) -> float:
return self._celsius

@property
def fahrenheit(self) -> float:
return self._celsius * 9/5 + 32

@property
def kelvin(self) -> float:
return self._celsius + 273.15

2. vars() 내장 함수로 속성 딕셔너리를 얻어라

class Point:
def __init__(self, x: float, y: float, z: float = 0.0):
self.x = x
self.y = y
self.z = z


p = Point(1.0, 2.0, 3.0)
print(vars(p)) # {'x': 1.0, 'y': 2.0, 'z': 3.0}

# 딕셔너리로 인스턴스 생성
data = {"x": 4.0, "y": 5.0, "z": 6.0}
p2 = Point(**data)
print(p2.x, p2.y, p2.z) # 4.0 5.0 6.0

3. 클래스 정의 후 동적으로 메서드 추가 (몽키 패칭)

class Cat:
def __init__(self, name: str):
self.name = name


def meow(self) -> str:
return f"{self.name}: 야옹~"


# 클래스에 메서드 동적 추가
Cat.meow = meow

c = Cat("나비")
print(c.meow()) # 나비: 야옹~

4. __init_subclass__로 서브클래스 등록 자동화

class Plugin:
_registry: dict[str, type] = {}

def __init_subclass__(cls, plugin_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if plugin_name:
Plugin._registry[plugin_name] = cls

@classmethod
def get_plugin(cls, name: str) -> type | None:
return cls._registry.get(name)


class AudioPlugin(Plugin, plugin_name="audio"):
pass

class VideoPlugin(Plugin, plugin_name="video"):
pass

print(Plugin._registry)
# {'audio': <class 'AudioPlugin'>, 'video': <class 'VideoPlugin'>}
print(Plugin.get_plugin("audio")) # <class 'AudioPlugin'>

5. __class__ 속성으로 동적 클래스 정보 얻기

class Animal:
def describe(self) -> str:
return f"나는 {self.__class__.__name__} 클래스의 인스턴스입니다."


class Dog(Animal):
pass

class Cat(Animal):
pass

d = Dog()
c = Cat()
print(d.describe()) # 나는 Dog 클래스의 인스턴스입니다.
print(c.describe()) # 나는 Cat 클래스의 인스턴스입니다.

정리

개념설명접근 방식
클래스 변수모든 인스턴스가 공유ClassName.var 또는 self.var
인스턴스 변수각 인스턴스만의 데이터self.var
__init__객체 초기화 메서드인스턴스 생성 시 자동 호출
self인스턴스 자신에 대한 참조모든 인스턴스 메서드의 첫 번째 매개변수
__dict__인스턴스/클래스의 속성 딕셔너리obj.__dict__, Class.__dict__

다음 챕터에서는 인스턴스 메서드, 클래스 메서드, 정적 메서드의 차이와 올바른 사용법을 배웁니다.

Advertisement