Python 클래스 | 객체지향 프로그래밍(OOP) 완벽 정리
이 글의 핵심
Python 클래스에 대한 실전 가이드입니다. 객체지향 프로그래밍(OOP) 완벽 정리 등을 예제와 함께 상세히 설명합니다.
들어가며
객체지향 프로그래밍(OOP)이란?
객체지향 프로그래밍(Object-Oriented Programming)은 프로그램을 객체(Object)들의 모음으로 보는 프로그래밍 패러다임입니다.
절차지향 vs 객체지향:
# 절차지향: 함수 중심
def create_account(owner, balance):
return {"owner": owner, "balance": balance}
def deposit(account, amount):
account["balance"] += amount
def withdraw(account, amount):
if amount > account["balance"]:
return False
account["balance"] -= amount
return True
account = create_account("철수", 10000)
deposit(account, 5000)
print(account["balance"]) # 15000
# 객체지향: 객체 중심
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
return False
self.balance -= amount
return True
account = BankAccount("철수", 10000)
account.deposit(5000)
print(account.balance) # 15000
OOP의 장점:
- ✅ 캡슐화: 데이터와 기능을 하나로 묶음
- ✅ 재사용성: 상속으로 코드 재사용
- ✅ 유지보수: 변경 영향 범위 최소화
- ✅ 확장성: 새 기능 추가 용이
1. 클래스 기본
클래스와 객체
클래스(Class)는 객체를 만들기 위한 설계도(blueprint)입니다. 붕어빵 틀에 반죽을 붓으면 여러 개의 붕어빵이 나오듯, 같은 클래스에서 찍어낸 인스턴스들은 구조는 같고 내용(속성 값)은 다를 수 있습니다.
객체(Object) 또는 인스턴스(Instance)는 클래스로부터 만들어진 실체입니다.
# 클래스 정의 (설계도)
class Person:
pass # 아무것도 없는 클래스
# 객체 생성 (인스턴스화)
person1 = Person() # Person 클래스의 인스턴스
person2 = Person() # 또 다른 인스턴스
print(type(person1)) # <class '__main__.Person'>
print(person1 == person2) # False (서로 다른 객체)
클래스 정의와 메서드
class Person:
# 생성자: 객체 생성 시 자동 호출
def __init__(self, name, age):
self.name = name # 인스턴스 변수
self.age = age
# 인스턴스 메서드
def greet(self):
return f"안녕하세요, {self.name}입니다."
def get_age(self):
return self.age
def is_adult(self):
return self.age >= 18
# 객체 생성
person1 = Person("철수", 25)
person2 = Person("영희", 30)
# 메서드 호출
print(person1.greet()) # 안녕하세요, 철수입니다.
print(person2.age) # 30
print(person1.is_adult()) # True
# 속성 접근
print(person1.name) # 철수
# 속성 변경
person1.age = 26
print(person1.age) # 26
self의 이해
**self**는 인스턴스 자신을 가리키는 참조입니다:
class Counter:
def __init__(self):
# self: 현재 생성되는 인스턴스를 가리킴
# self.count: 이 인스턴스의 count 속성
self.count = 0
# 각 인스턴스는 독립적인 count를 가짐
def increment(self):
# self.count: 이 메서드를 호출한 인스턴스의 count
self.count += 1
return self.count
# 각 인스턴스는 독립적인 메모리 공간을 가짐
counter1 = Counter() # counter1의 count = 0
counter2 = Counter() # counter2의 count = 0 (별도 메모리)
counter1.increment() # counter1.count = 1
counter1.increment() # counter1.count = 2
print(counter1.count) # 2
counter2.increment() # counter2.count = 1
print(counter2.count) # 1 (counter1과 독립적)
# self는 자동으로 전달됨
# counter1.increment()는 내부적으로 Counter.increment(counter1)로 호출됨
# Python이 첫 번째 인자로 인스턴스를 자동 전달
self의 동작 원리:
class Example:
def __init__(self, value):
self.value = value
def show(self):
print(f"값: {self.value}")
# 인스턴스 메서드 호출
obj = Example(10)
obj.show() # 값: 10
# 위 코드는 실제로 아래와 같이 동작:
Example.show(obj) # 값: 10
# Python이 obj를 첫 번째 인자(self)로 자동 전달
# 따라서 메서드 정의 시 첫 번째 매개변수는 항상 self
self를 사용하지 않으면:
class BadCounter:
def __init__(self):
count = 0 # ❌ self 없음 → 지역 변수
def increment(self):
# count += 1 # ❌ NameError: count가 정의되지 않음
pass
# self를 붙여야 인스턴스 변수가 됨
class GoodCounter:
def __init__(self):
self.count = 0 # ✅ 인스턴스 변수
def increment(self):
self.count += 1 # ✅ 인스턴스 변수 접근
self vs 클래스 변수:
class Student:
# 클래스 변수: 모든 인스턴스가 공유
school = "서울고등학교"
def __init__(self, name):
# 인스턴스 변수: 각 인스턴스마다 독립적
self.name = name
s1 = Student("철수")
s2 = Student("영희")
print(s1.name) # 철수 (인스턴스 변수, 독립적)
print(s2.name) # 영희 (인스턴스 변수, 독립적)
print(s1.school) # 서울고등학교 (클래스 변수, 공유)
print(s2.school) # 서울고등학교 (클래스 변수, 공유)
# 클래스 변수 변경 (모든 인스턴스에 영향)
Student.school = "부산고등학교"
print(s1.school) # 부산고등학교
print(s2.school) # 부산고등학교
2. 생성자와 소멸자
init (생성자)
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
print(f"{owner}님의 계좌 생성 (잔액: {balance}원)")
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
# 사용
account = BankAccount("철수", 10000)
# 철수님의 계좌 생성 (잔액: 10000원)
account.deposit(5000)
print(account.balance) # 15000
del (소멸자)
class FileHandler:
def __init__(self, filename):
self.file = open(filename, 'w')
print(f"{filename} 열림")
def __del__(self):
self.file.close()
print("파일 닫힘")
# 사용
handler = FileHandler("test.txt")
# test.txt 열림
del handler
# 파일 닫힘
3. 클래스 변수 vs 인스턴스 변수
차이점 이해
class Dog:
# 클래스 변수: 모든 인스턴스가 공유
species = "Canis familiaris"
count = 0 # 생성된 개 수
def __init__(self, name, age):
# 인스턴스 변수: 각 인스턴스마다 독립적
self.name = name
self.age = age
Dog.count += 1 # 클래스 변수 증가
dog1 = Dog("바둑이", 3)
dog2 = Dog("멍멍이", 5)
# 클래스 변수 접근
print(dog1.species) # Canis familiaris
print(dog2.species) # Canis familiaris
print(Dog.count) # 2 (생성된 개 수)
# 인스턴스 변수 접근
print(dog1.name) # 바둑이
print(dog2.name) # 멍멍이
# 클래스 변수 변경 (모든 인스턴스 영향)
Dog.species = "개"
print(dog1.species) # 개
print(dog2.species) # 개
# 인스턴스 변수 변경 (해당 인스턴스만 영향)
dog1.name = "뽀삐"
print(dog1.name) # 뽀삐
print(dog2.name) # 멍멍이 (변경 안 됨)
클래스 변수 주의사항
class MyClass:
shared_list = [] # 클래스 변수 (위험!)
def __init__(self, value):
self.shared_list.append(value) # 모든 인스턴스가 공유
obj1 = MyClass(1)
obj2 = MyClass(2)
print(obj1.shared_list) # [1, 2] (의도하지 않은 공유!)
print(obj2.shared_list) # [1, 2]
# 올바른 방법: 인스턴스 변수 사용
class MyClass:
def __init__(self, value):
self.my_list = [] # 인스턴스 변수
self.my_list.append(value)
obj1 = MyClass(1)
obj2 = MyClass(2)
print(obj1.my_list) # [1]
print(obj2.my_list) # [2]
클래스 변수 활용 예제
class Employee:
company = "ABC Corp" # 회사명 (모든 직원 공유)
employee_count = 0 # 직원 수
def __init__(self, name, position):
self.name = name
self.position = position
Employee.employee_count += 1
@classmethod
def get_employee_count(cls):
return cls.employee_count
def __del__(self):
Employee.employee_count -= 1
# 사용
emp1 = Employee("홍길동", "개발자")
emp2 = Employee("김철수", "디자이너")
print(f"회사: {Employee.company}") # ABC Corp
print(f"직원 수: {Employee.get_employee_count()}") # 2
del emp1
print(f"직원 수: {Employee.get_employee_count()}") # 1
4. 상속 (Inheritance)
상속이란?
상속(Inheritance)은 기존 클래스의 속성과 메서드를 재사용하는 메커니즘입니다.
용어:
- 부모 클래스(Parent Class) = 기반 클래스(Base Class) = 슈퍼 클래스(Super Class)
- 자식 클래스(Child Class) = 파생 클래스(Derived Class) = 서브 클래스(Sub Class)
기본 상속
# 부모 클래스
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "소리를 냅니다"
def move(self):
return f"{self.name}이(가) 움직입니다"
# 자식 클래스 1
class Dog(Animal): # Animal 상속
def speak(self): # 메서드 오버라이딩
return f"{self.name}: 멍멍!"
# 자식 클래스 2
class Cat(Animal):
def speak(self):
return f"{self.name}: 야옹~"
# 사용
dog = Dog("바둑이")
cat = Cat("나비")
print(dog.speak()) # 바둑이: 멍멍! (오버라이딩된 메서드)
print(dog.move()) # 바둑이이(가) 움직입니다 (상속받은 메서드)
print(cat.speak()) # 나비: 야옹~
# isinstance(): 타입 확인
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True (부모 클래스도 True)
print(isinstance(dog, Cat)) # False
# issubclass(): 상속 관계 확인
print(issubclass(Dog, Animal)) # True
print(issubclass(Animal, Dog)) # False
super() 사용
**super()**는 부모 클래스의 메서드를 호출할 때 사용합니다.
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def get_info(self):
return f"{self.name} - {self.salary:,}원"
def work(self):
return f"{self.name}이(가) 일합니다"
class Manager(Employee):
def __init__(self, name, salary, team_size):
super().__init__(name, salary) # 부모 생성자 호출
self.team_size = team_size
def get_info(self):
base_info = super().get_info() # 부모 메서드 호출
return f"{base_info} (팀원: {self.team_size}명)"
def manage_team(self):
return f"{self.name}이(가) {self.team_size}명의 팀을 관리합니다"
# 사용
manager = Manager("김팀장", 5000000, 5)
print(manager.get_info()) # 김팀장 - 5,000,000원 (팀원: 5명)
print(manager.work()) # 김팀장이(가) 일합니다 (상속)
print(manager.manage_team()) # 김팀장이(가) 5명의 팀을 관리합니다
다중 상속
Python은 다중 상속을 지원합니다 (C++과 유사, Java는 불가).
class Flyer:
def fly(self):
return "날아갑니다"
class Swimmer:
def swim(self):
return "헤엄칩니다"
# 다중 상속
class Duck(Flyer, Swimmer):
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name}: 꽥꽥!"
duck = Duck("도널드")
print(duck.fly()) # 날아갑니다
print(duck.swim()) # 헤엄칩니다
print(duck.speak()) # 도널드: 꽥꽥!
# MRO (Method Resolution Order): 메서드 검색 순서
print(Duck.__mro__)
# (<class '__main__.Duck'>, <class '__main__.Flyer'>,
# <class '__main__.Swimmer'>, <class 'object'>)
상속 실전 예제
# 게임 캐릭터 시스템
class Character:
def __init__(self, name, hp, attack):
self.name = name
self.hp = hp
self.attack = attack
def take_damage(self, damage):
self.hp -= damage
if self.hp < 0:
self.hp = 0
return self.hp
def is_alive(self):
return self.hp > 0
def basic_attack(self, target):
return target.take_damage(self.attack)
class Warrior(Character):
def __init__(self, name, hp, attack, defense):
super().__init__(name, hp, attack)
self.defense = defense
def take_damage(self, damage):
reduced = max(0, damage - self.defense)
return super().take_damage(reduced)
def shield_bash(self, target):
return target.take_damage(self.attack * 1.5)
class Mage(Character):
def __init__(self, name, hp, attack, mana):
super().__init__(name, hp, attack)
self.mana = mana
def fireball(self, target):
if self.mana >= 20:
self.mana -= 20
return target.take_damage(self.attack * 2)
return 0
# 전투 시뮬레이션
warrior = Warrior("전사", 150, 20, 5)
mage = Mage("마법사", 100, 30, 50)
print(f"{warrior.name} HP: {warrior.hp}") # 전사 HP: 150
print(f"{mage.name} HP: {mage.hp}") # 마법사 HP: 100
# 전사가 마법사 공격
mage.basic_attack(warrior)
print(f"{warrior.name} HP: {warrior.hp}") # 전사 HP: 125 (30-5=25 데미지)
# 마법사가 파이어볼
mage.fireball(warrior)
print(f"{warrior.name} HP: {warrior.hp}") # 전사 HP: 70 (60-5=55 데미지)
print(f"{mage.name} 마나: {mage.mana}") # 마법사 마나: 30
5. 캡슐화 (Encapsulation)
캡슐화란?
캡슐화(Encapsulation)는 데이터를 외부로부터 보호하고, 접근을 제어하는 것입니다.
Python의 접근 제어:
public: 제한 없음 (기본)_protected: 관례상 내부 사용 (강제 아님)__private: Name Mangling으로 외부 접근 차단
Private 변수 (__prefix)
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # public
self._account_number = "123" # protected (관례)
self.__balance = balance # private (__)
def get_balance(self):
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return True
return False
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return True
return False
account = BankAccount("철수", 10000)
# public 접근
print(account.owner) # 철수
# protected 접근 (가능하지만 권장 안 함)
print(account._account_number) # 123
# private 접근 시도
# print(account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance'
# 메서드로 접근 (올바른 방법)
print(account.get_balance()) # 10000
# Name Mangling: 실제로는 _ClassName__attribute로 변환됨
print(account._BankAccount__balance) # 10000 (가능하지만 절대 하지 말 것!)
@property 데코레이터
**@property**는 메서드를 속성처럼 사용할 수 있게 합니다. Getter/Setter를 우아하게 구현합니다.
class Circle:
def __init__(self, radius):
self._radius = radius # protected (관례)
@property
def radius(self):
"""Getter: circle.radius로 접근"""
return self._radius
@radius.setter
def radius(self, value):
"""Setter: circle.radius = value로 설정"""
if value < 0:
raise ValueError("반지름은 양수여야 합니다")
self._radius = value
@property
def area(self):
"""읽기 전용 속성 (setter 없음)"""
return 3.14159 * self._radius ** 2
@property
def diameter(self):
return self._radius * 2
@diameter.setter
def diameter(self, value):
self._radius = value / 2
# 사용
circle = Circle(5)
print(circle.radius) # 5 (메서드지만 속성처럼 접근)
print(circle.area) # 78.53975
circle.radius = 10 # setter 호출
print(circle.area) # 314.159
# circle.area = 100 # AttributeError: can't set attribute (읽기 전용)
circle.diameter = 20 # diameter setter
print(circle.radius) # 10.0
# 유효성 검사
# circle.radius = -5 # ValueError: 반지름은 양수여야 합니다
캡슐화 실전 예제
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("절대영도 이하는 불가능합니다")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9
@property
def kelvin(self):
return self._celsius + 273.15
# 사용
temp = Temperature(25)
print(f"섭씨: {temp.celsius}°C") # 섭씨: 25°C
print(f"화씨: {temp.fahrenheit}°F") # 화씨: 77.0°F
print(f"켈빈: {temp.kelvin}K") # 켈빈: 298.15K
temp.fahrenheit = 100 # 화씨로 설정
print(f"섭씨: {temp.celsius}°C") # 섭씨: 37.77777777777778°C
# temp.celsius = -300 # ValueError: 절대영도 이하는 불가능합니다
6. 특수 메서드 (Magic Methods / Dunder Methods)
특수 메서드란?
특수 메서드(Magic Methods)는 __method__ 형태로, Python의 내장 연산자와 함수를 커스터마이즈합니다.
주요 특수 메서드:
| 메서드 | 설명 | 호출 방법 |
|---|---|---|
__init__ | 생성자 | obj = MyClass() |
__str__ | 문자열 표현 (사용자용) | str(obj), print(obj) |
__repr__ | 문자열 표현 (개발자용) | repr(obj) |
__len__ | 길이 | len(obj) |
__add__ | 덧셈 | obj1 + obj2 |
__eq__ | 같음 비교 | obj1 == obj2 |
__lt__ | 작음 비교 | obj1 < obj2 |
__getitem__ | 인덱싱 | obj[key] |
__setitem__ | 인덱스 할당 | obj[key] = value |
__call__ | 호출 | obj() |
특수 메서드 구현
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
"""print()에서 사용"""
return f"Vector({self.x}, {self.y})"
def __repr__(self):
"""개발자용 표현 (디버깅)"""
return f"Vector(x={self.x}, y={self.y})"
def __add__(self, other):
"""+ 연산자 오버로딩"""
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
"""- 연산자"""
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
"""* 연산자 (스칼라 곱)"""
return Vector(self.x * scalar, self.y * scalar)
def __eq__(self, other):
"""== 연산자"""
return self.x == other.x and self.y == other.y
def __len__(self):
"""len() 함수 (벡터 크기)"""
return int((self.x ** 2 + self.y ** 2) ** 0.5)
def __getitem__(self, index):
"""인덱싱: v[0], v[1]"""
if index == 0:
return self.x
elif index == 1:
return self.y
else:
raise IndexError("Vector index out of range")
# 사용
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1) # Vector(1, 2) (__str__)
print(repr(v1)) # Vector(x=1, y=2) (__repr__)
v3 = v1 + v2 # __add__
print(v3) # Vector(4, 6)
v4 = v2 - v1 # __sub__
print(v4) # Vector(2, 2)
v5 = v1 * 3 # __mul__
print(v5) # Vector(3, 6)
print(v1 == v2) # False (__eq__)
print(len(v1)) # 2 (__len__)
print(v1[0], v1[1]) # 1 2 (__getitem__)
컨테이너 특수 메서드
class Inventory:
def __init__(self):
self.items = {}
def __setitem__(self, key, value):
"""inventory[key] = value"""
self.items[key] = value
def __getitem__(self, key):
"""inventory[key]"""
return self.items.get(key, 0)
def __delitem__(self, key):
"""del inventory[key]"""
if key in self.items:
del self.items[key]
def __contains__(self, key):
"""key in inventory"""
return key in self.items
def __len__(self):
"""len(inventory)"""
return len(self.items)
def __str__(self):
return str(self.items)
# 사용
inv = Inventory()
inv["sword"] = 1 # __setitem__
inv["potion"] = 5
print(inv["sword"]) # 1 (__getitem__)
print("sword" in inv) # True (__contains__)
print(len(inv)) # 2 (__len__)
del inv["sword"] # __delitem__
print(inv) # {'potion': 5} (__str__)
call 메서드
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
"""객체를 함수처럼 호출"""
return x * self.factor
# 사용
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10 (객체를 함수처럼 호출)
print(triple(5)) # 15
# 실전 예제: 카운터
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
counter = Counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
7. 클래스 메서드와 정적 메서드
메서드 종류 비교
| 메서드 | 첫 인자 | 접근 가능 | 사용 시점 |
|---|---|---|---|
| 인스턴스 메서드 | self | 인스턴스 변수 | 인스턴스 데이터 필요 |
| 클래스 메서드 | cls | 클래스 변수 | 대체 생성자, 클래스 데이터 |
| 정적 메서드 | 없음 | 제한 없음 | 유틸리티 함수 |
클래스 메서드 (@classmethod)
from datetime import date
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_birth_year(cls, name, birth_year):
"""대체 생성자: 출생년도로 객체 생성"""
age = date.today().year - birth_year
return cls(name, age) # cls는 Person 클래스
@classmethod
def get_species(cls):
"""클래스 정보 반환"""
return "Homo sapiens"
def greet(self):
return f"안녕하세요, {self.name}입니다. {self.age}살입니다."
# 일반 생성자
person1 = Person("철수", 25)
# 클래스 메서드로 생성
person2 = Person.from_birth_year("영희", 1995)
print(person1.greet()) # 안녕하세요, 철수입니다. 25살입니다.
print(person2.greet()) # 안녕하세요, 영희입니다. 31살입니다.
print(Person.get_species()) # Homo sapiens
정적 메서드 (@staticmethod)
class MathUtils:
PI = 3.14159
@staticmethod
def is_even(n):
"""정적 메서드: self나 cls 불필요"""
return n % 2 == 0
@staticmethod
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
@staticmethod
def factorial(n):
if n <= 1:
return 1
return n * MathUtils.factorial(n - 1)
def __init__(self, radius):
self.radius = radius
def area(self):
"""인스턴스 메서드: self 필요"""
return MathUtils.PI * self.radius ** 2
# 정적 메서드 사용 (인스턴스 없이 호출 가능)
print(MathUtils.is_even(4)) # True
print(MathUtils.is_prime(7)) # True
print(MathUtils.factorial(5)) # 120
# 인스턴스 메서드 사용
circle = MathUtils(5)
print(circle.area()) # 78.53975
# 인스턴스에서도 정적 메서드 호출 가능 (권장 안 함)
print(circle.is_even(4)) # True
실전 예제: 날짜 유틸리티
from datetime import datetime
class DateUtils:
@staticmethod
def is_weekend(date_str):
"""날짜가 주말인지 확인"""
date = datetime.strptime(date_str, "%Y-%m-%d")
return date.weekday() >= 5 # 5: 토요일, 6: 일요일
@staticmethod
def days_between(date1_str, date2_str):
"""두 날짜 사이의 일수"""
date1 = datetime.strptime(date1_str, "%Y-%m-%d")
date2 = datetime.strptime(date2_str, "%Y-%m-%d")
return abs((date2 - date1).days)
@classmethod
def today(cls):
"""오늘 날짜 반환"""
return datetime.now().strftime("%Y-%m-%d")
# 사용
print(DateUtils.is_weekend("2026-03-29")) # True (일요일)
print(DateUtils.days_between("2026-03-01", "2026-03-29")) # 28
print(DateUtils.today()) # 2026-03-29
8. 추상 클래스 (Abstract Class)
추상 클래스란?
추상 클래스(Abstract Class)는 직접 인스턴스화할 수 없고, 자식 클래스가 반드시 구현해야 하는 메서드를 정의합니다.
from abc import ABC, abstractmethod
class Shape(ABC): # ABC 상속
@abstractmethod
def area(self):
"""자식 클래스에서 반드시 구현해야 함"""
pass
@abstractmethod
def perimeter(self):
pass
def describe(self):
"""일반 메서드 (구현 가능)"""
return f"넓이: {self.area()}, 둘레: {self.perimeter()}"
# shape = Shape() # TypeError: Can't instantiate abstract class
class Rectangle(Shape):
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)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
# 사용
rect = Rectangle(5, 10)
print(rect.describe()) # 넓이: 50, 둘레: 30
circle = Circle(5)
print(circle.describe()) # 넓이: 78.53975, 둘레: 31.4159
9. 다형성 (Polymorphism)
다형성이란?
다형성(Polymorphism)은 같은 인터페이스로 다른 동작을 수행하는 것입니다.
class Animal:
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "멍멍!"
class Cat(Animal):
def speak(self):
return "야옹~"
class Cow(Animal):
def speak(self):
return "음메~"
# 다형성: 같은 메서드 이름, 다른 동작
def make_sound(animal):
print(animal.speak())
animals = [Dog(), Cat(), Cow()]
for animal in animals:
make_sound(animal)
# 멍멍!
# 야옹~
# 음메~
다형성 실전 예제: 결제 시스템
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount):
pass
class CreditCard(PaymentMethod):
def __init__(self, card_number):
self.card_number = card_number
def pay(self, amount):
return f"신용카드({self.card_number[-4:]})로 {amount:,}원 결제"
class BankTransfer(PaymentMethod):
def __init__(self, account_number):
self.account_number = account_number
def pay(self, amount):
return f"계좌이체({self.account_number})로 {amount:,}원 결제"
class KakaoPay(PaymentMethod):
def __init__(self, phone):
self.phone = phone
def pay(self, amount):
return f"카카오페이({self.phone})로 {amount:,}원 결제"
# 결제 처리 함수 (다형성)
def process_payment(payment_method, amount):
print(payment_method.pay(amount))
# 사용
methods = [
CreditCard("1234-5678-9012-3456"),
BankTransfer("110-123-456789"),
KakaoPay("010-1234-5678")
]
for method in methods:
process_payment(method, 50000)
# 출력:
# 신용카드(3456)로 50,000원 결제
# 계좌이체(110-123-456789)로 50,000원 결제
# 카카오페이(010-1234-5678)로 50,000원 결제
10. 실전 프로젝트: 도서 관리 시스템
from datetime import datetime, timedelta
class Book:
def __init__(self, isbn, title, author, available=True):
self.isbn = isbn
self.title = title
self.author = author
self.available = available
def __str__(self):
status = "대출 가능" if self.available else "대출 중"
return f"[{self.isbn}] {self.title} - {self.author} ({status})"
def __eq__(self, other):
return self.isbn == other.isbn
class Member:
def __init__(self, member_id, name):
self.member_id = member_id
self.name = name
self.borrowed_books = []
def __str__(self):
return f"회원 {self.name}({self.member_id})"
class Library:
def __init__(self):
self.books = {}
self.members = {}
self.loan_records = []
def add_book(self, book):
self.books[book.isbn] = book
print(f"도서 추가: {book.title}")
def register_member(self, member):
self.members[member.member_id] = member
print(f"회원 등록: {member.name}")
def borrow_book(self, member_id, isbn):
if member_id not in self.members:
return "등록되지 않은 회원입니다"
if isbn not in self.books:
return "존재하지 않는 도서입니다"
book = self.books[isbn]
member = self.members[member_id]
if not book.available:
return f"{book.title}은(는) 이미 대출 중입니다"
book.available = False
member.borrowed_books.append(isbn)
due_date = datetime.now() + timedelta(days=14)
self.loan_records.append({
"member": member_id,
"book": isbn,
"borrow_date": datetime.now(),
"due_date": due_date
})
return f"{member.name}님이 '{book.title}'을(를) 대출했습니다 (반납일: {due_date.strftime('%Y-%m-%d')})"
def return_book(self, member_id, isbn):
if isbn not in self.books:
return "존재하지 않는 도서입니다"
book = self.books[isbn]
member = self.members[member_id]
if isbn not in member.borrowed_books:
return "대출하지 않은 도서입니다"
book.available = True
member.borrowed_books.remove(isbn)
return f"{member.name}님이 '{book.title}'을(를) 반납했습니다"
def list_available_books(self):
available = [book for book in self.books.values() if book.available]
return available
# 사용 예제
library = Library()
# 도서 추가
library.add_book(Book("978-1", "파이썬 입문", "홍길동"))
library.add_book(Book("978-2", "알고리즘 정복", "김철수"))
library.add_book(Book("978-3", "데이터 과학", "이영희"))
# 회원 등록
library.register_member(Member("M001", "박민수"))
library.register_member(Member("M002", "정수진"))
# 대출
print(library.borrow_book("M001", "978-1"))
# 박민수님이 '파이썬 입문'을(를) 대출했습니다 (반납일: 2026-04-12)
print(library.borrow_book("M002", "978-1"))
# 파이썬 입문은(는) 이미 대출 중입니다
# 반납
print(library.return_book("M001", "978-1"))
# 박민수님이 '파이썬 입문'을(를) 반납했습니다
# 대출 가능 도서 목록
print("\n대출 가능 도서:")
for book in library.list_available_books():
print(f" - {book}")
11. OOP 4대 원칙
1. 캡슐화 (Encapsulation)
데이터와 메서드를 하나로 묶고, 외부 접근을 제어합니다.
class Account:
def __init__(self, balance):
self.__balance = balance # private
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def get_balance(self):
return self.__balance
# 직접 접근 불가, 메서드로만 접근
2. 상속 (Inheritance)
기존 클래스를 확장하여 재사용합니다.
class Vehicle:
def move(self):
pass
class Car(Vehicle):
def move(self):
return "도로를 달립니다"
3. 다형성 (Polymorphism)
같은 인터페이스로 다른 동작을 수행합니다.
def process(shape):
print(shape.area()) # 각 도형마다 다른 계산
process(Rectangle(5, 10)) # 50
process(Circle(5)) # 78.54
4. 추상화 (Abstraction)
복잡한 내부 구현을 숨기고 필요한 기능만 노출합니다.
class Database(ABC):
@abstractmethod
def connect(self):
pass
@abstractmethod
def query(self, sql):
pass
# 사용자는 connect()와 query()만 알면 됨
12. 자주 하는 실수와 해결법
실수 1: __init__에서 self 누락
# ❌ 잘못된 방법
class Person:
def __init__(name, age): # self 누락!
self.name = name
self.age = age
# person = Person("철수", 25) # TypeError
# ✅ 올바른 방법
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
실수 2: 클래스 변수를 인스턴스 변수로 착각
# ❌ 잘못된 방법
class MyClass:
items = [] # 클래스 변수 (모든 인스턴스 공유!)
def add(self, item):
self.items.append(item)
obj1 = MyClass()
obj2 = MyClass()
obj1.add(1)
obj2.add(2)
print(obj1.items) # [1, 2] (의도하지 않은 공유)
# ✅ 올바른 방법
class MyClass:
def __init__(self):
self.items = [] # 인스턴스 변수
def add(self, item):
self.items.append(item)
실수 3: super() 호출 누락
# ❌ 잘못된 방법
class Parent:
def __init__(self, name):
self.name = name
class Child(Parent):
def __init__(self, name, age):
# super().__init__(name) 누락!
self.age = age
# child = Child("철수", 25)
# print(child.name) # AttributeError
# ✅ 올바른 방법
class Child(Parent):
def __init__(self, name, age):
super().__init__(name)
self.age = age
실수 4: @property setter 없이 할당
# ❌ 잘못된 방법
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
# setter 없음!
circle = Circle(5)
# circle.radius = 10 # AttributeError: can't set attribute
# ✅ 올바른 방법
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
self._radius = value
13. 연습 문제
문제 1: 은행 계좌 클래스
입금, 출금, 잔액 조회 기능을 가진 BankAccount 클래스를 구현하세요.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance
self.transaction_history = []
def deposit(self, amount):
if amount <= 0:
return "입금액은 양수여야 합니다"
self.__balance += amount
self.transaction_history.append(f"입금: +{amount:,}원")
return f"입금 완료. 잔액: {self.__balance:,}원"
def withdraw(self, amount):
if amount <= 0:
return "출금액은 양수여야 합니다"
if amount > self.__balance:
return "잔액이 부족합니다"
self.__balance -= amount
self.transaction_history.append(f"출금: -{amount:,}원")
return f"출금 완료. 잔액: {self.__balance:,}원"
@property
def balance(self):
return self.__balance
def get_history(self):
return "\n".join(self.transaction_history)
# 테스트
account = BankAccount("홍길동", 10000)
print(account.deposit(5000)) # 입금 완료. 잔액: 15,000원
print(account.withdraw(3000)) # 출금 완료. 잔액: 12,000원
print(f"현재 잔액: {account.balance:,}원") # 현재 잔액: 12,000원
print("\n거래 내역:")
print(account.get_history())
문제 2: 직원 관리 시스템
Employee 부모 클래스와 Developer, Designer 자식 클래스를 구현하세요.
class Employee:
company = "ABC Corp"
def __init__(self, name, salary):
self.name = name
self.salary = salary
def get_annual_salary(self):
return self.salary * 12
def __str__(self):
return f"{self.name} - {self.salary:,}원"
class Developer(Employee):
def __init__(self, name, salary, language):
super().__init__(name, salary)
self.language = language
def code(self):
return f"{self.name}이(가) {self.language}로 코딩합니다"
def __str__(self):
return f"{super().__str__()} [개발자 - {self.language}]"
class Designer(Employee):
def __init__(self, name, salary, tool):
super().__init__(name, salary)
self.tool = tool
def design(self):
return f"{self.name}이(가) {self.tool}로 디자인합니다"
def __str__(self):
return f"{super().__str__()} [디자이너 - {self.tool}]"
# 테스트
dev = Developer("홍길동", 5000000, "Python")
designer = Designer("김철수", 4500000, "Figma")
print(dev) # 홍길동 - 5,000,000원 [개발자 - Python]
print(designer) # 김철수 - 4,500,000원 [디자이너 - Figma]
print(dev.code()) # 홍길동이(가) Python로 코딩합니다
print(designer.design()) # 김철수이(가) Figma로 디자인합니다
print(f"연봉: {dev.get_annual_salary():,}원") # 연봉: 60,000,000원
문제 3: 쇼핑몰 상품 클래스
특수 메서드를 활용한 Product 클래스를 구현하세요.
class Product:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def __str__(self):
return f"{self.name} - {self.price:,}원 (재고: {self.quantity}개)"
def __add__(self, other):
"""두 상품의 총 가격"""
return self.price * self.quantity + other.price * other.quantity
def __mul__(self, n):
"""수량 증가"""
return Product(self.name, self.price, self.quantity * n)
def __lt__(self, other):
"""가격 비교 (정렬용)"""
return self.price < other.price
def __len__(self):
"""재고 수량"""
return self.quantity
# 테스트
p1 = Product("노트북", 1200000, 2)
p2 = Product("마우스", 30000, 5)
print(p1) # 노트북 - 1,200,000원 (재고: 2개)
print(p2) # 마우스 - 30,000원 (재고: 5개)
total = p1 + p2
print(f"총 가격: {total:,}원") # 총 가격: 2,550,000원
p3 = p1 * 2 # 수량 2배
print(p3) # 노트북 - 1,200,000원 (재고: 4개)
products = [p1, p2]
products.sort() # __lt__로 정렬
print("가격순 정렬:", [p.name for p in products]) # ['마우스', '노트북']
print(f"재고: {len(p1)}개") # 재고: 2개
정리
핵심 요약
- 클래스: 데이터 + 기능을 묶는 설계도
- 객체: 클래스로부터 생성된 실체
__init__: 생성자, 객체 초기화self: 인스턴스 자신을 가리키는 참조- 상속: 코드 재사용,
super()로 부모 호출 - 캡슐화:
__private,@property로 데이터 보호 - 다형성: 같은 인터페이스, 다른 동작
- 추상 클래스:
ABC,@abstractmethod로 인터페이스 정의 - 특수 메서드:
__str__,__add__등으로 연산자 오버로딩 - 클래스/정적 메서드:
@classmethod,@staticmethod
OOP 설계 원칙
- 단일 책임 원칙: 클래스는 하나의 책임만
- 개방-폐쇄 원칙: 확장에는 열려있고, 수정에는 닫혀있어야
- 리스코프 치환 원칙: 자식 클래스는 부모 클래스를 대체 가능해야
- 인터페이스 분리 원칙: 클라이언트는 사용하지 않는 메서드에 의존하지 않아야
- 의존성 역전 원칙: 구체화가 아닌 추상화에 의존해야
다음 단계
- Python 모듈과 패키지
- Python 예외 처리
- Python 데코레이터
관련 글
- Python 환경 설정 | Windows/Mac에서 Python 설치하고 시작하기
- Python 기본 문법 | 변수, 연산자, 조건문, 반복문 완벽 가이드