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의 장점:
- ✅ 캡슐화: 데이터와 기능을 하나로 묶음
- ✅ 재사용성: 상속으로 코드 재사용
- ✅ 유지보수: 변경 영향 범위 최소화
- ✅ 확장성: 새 기능 추가 용이
실무 활용 사례: 데이터 분석, 웹 개발, 자동화 프로젝트에서 실제로 사용한 패턴과 코드를 바탕으로 정리했습니다. 초보자가 흔히 겪는 오류와 해결법을 포함합니다.
실무에서 느낀 Python의 매력
처음 Python을 배울 때는 “이게 정말 프로그래밍 언어인가?” 싶을 정도로 간결했습니다. C++에서 10줄로 작성하던 코드가 Python에서는 2~3줄로 끝나는 경우가 많았죠. 특히 데이터 분석 프로젝트를 진행하면서 Pandas와 NumPy의 강력함을 체감했습니다. 엑셀로 몇 시간 걸리던 작업이 Python 스크립트로는 몇 초 만에 끝나는 걸 보고 동료들이 놀라워했던 기억이 납니다. 하지만 처음부터 순탄하지만은 않았습니다. 들여쓰기 하나 잘못해서 몇 시간을 헤맨 적도 있고, 가상환경 설정이 꼬여서 프로젝트 전체를 다시 시작한 적도 있습니다. 이런 시행착오를 겪으며 깨달은 건, 환경 설정을 처음부터 제대로 하는 것이 얼마나 중요한지였습니다. 이 글에서는 제가 겪은 실수들을 바탕으로, 여러분이 같은 시행착오를 겪지 않도록 실전 팁을 담았습니다.
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 클래스 | 객체지향 프로그래밍(OOP) 완벽 정리」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「Python 클래스 | 객체지향 프로그래밍(OOP) 완벽 정리」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Python 클래스: 객체지향 프로그래밍(OOP) 완벽 정리. 클래스 기본·생성자와 소멸자로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. Python·클래스·class 중심으로 설명합니다. Start no… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. Python 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Python 함수 | 매개변수, 반환값, 람다, 데코레이터 완벽 정리
- Python 모듈과 패키지 | import, pip, 가상환경 완벽 정리
- Python 실전 시리즈 전체 목차 | #01~#23 학습 경로·영문 글·연관 글
- JavaScript 클래스 | ES6 Class 문법 완벽 정리
이 글에서 다루는 키워드 (관련 검색어)
Python, 클래스, class, OOP, 객체지향, 상속, 캡슐화 등으로 검색하시면 이 글이 도움이 됩니다.