본문으로 건너뛰기

클래스 기초

객체지향 프로그래밍 (OOP)이란?

객체지향 프로그래밍은 데이터와 기능을 하나의 단위(객체)로 묶어 관리하는 프로그래밍 패러다임입니다.

# 절차적 프로그래밍
user_name = "홍길동"
user_age = 25
user_email = "hong@example.com"

def print_user_info(name, age, email):
print(f"{name}, {age}세, {email}")

print_user_info(user_name, user_age, user_email)

# 객체지향 프로그래밍
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email

def print_info(self):
print(f"{self.name}, {self.age}세, {self.email}")

user = User("홍길동", 25, "hong@example.com")
user.print_info()

클래스와 인스턴스

클래스 정의

# 가장 간단한 클래스
class Dog:
pass

# 인스턴스 생성
my_dog = Dog()
print(type(my_dog)) # <class '__main__.Dog'>

# 속성 추가
class Dog:
species = "Canis familiaris" # 클래스 속성

def __init__(self, name, age):
self.name = name # 인스턴스 속성
self.age = age

# 인스턴스 생성
buddy = Dog("Buddy", 3)
miles = Dog("Miles", 5)

print(buddy.name) # Buddy
print(miles.age) # 5
print(Dog.species) # Canis familiaris

init 메서드

# 생성자 (초기화 메서드)
class Person:
def __init__(self, name, age):
"""인스턴스 초기화"""
print(f"{name} 객체 생성")
self.name = name
self.age = age

person = Person("홍길동", 25) # 홍길동 객체 생성

# 기본값 사용
class Person:
def __init__(self, name, age=0, city="서울"):
self.name = name
self.age = age
self.city = city

p1 = Person("홍길동", 25, "부산")
p2 = Person("김철수") # age=0, city="서울"

print(p2.age) # 0
print(p2.city) # 서울

# 유효성 검증
class Person:
def __init__(self, name, age):
if not name:
raise ValueError("이름은 필수입니다")
if age < 0:
raise ValueError("나이는 0 이상이어야 합니다")

self.name = name
self.age = age

# person = Person("", 25) # ValueError: 이름은 필수입니다

인스턴스 메서드

기본 메서드

class Circle:
def __init__(self, radius):
self.radius = radius

def area(self):
"""원의 넓이"""
return 3.14 * self.radius ** 2

def circumference(self):
"""원의 둘레"""
return 2 * 3.14 * self.radius

def scale(self, factor):
"""반지름 조정"""
self.radius *= factor

# 사용
circle = Circle(5)
print(f"넓이: {circle.area()}") # 78.5
print(f"둘레: {circle.circumference()}") # 31.4

circle.scale(2)
print(f"조정 후 넓이: {circle.area()}") # 314.0

self의 의미

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

def increment(self):
# self는 인스턴스 자신을 가리킴
self.count += 1

def decrement(self):
self.count -= 1

def get_count(self):
return self.count

counter1 = Counter()
counter2 = Counter()

counter1.increment()
counter1.increment()
counter2.increment()

print(counter1.get_count()) # 2
print(counter2.get_count()) # 1 (독립적)

# self는 첫 번째 매개변수로 자동 전달
# counter1.increment()는 실제로
# Counter.increment(counter1)로 호출됨

메서드 체이닝

class StringBuilder:
def __init__(self):
self.string = ""

def append(self, text):
"""텍스트 추가"""
self.string += text
return self # 자기 자신 반환

def upper(self):
"""대문자 변환"""
self.string = self.string.upper()
return self

def reverse(self):
"""역순"""
self.string = self.string[::-1]
return self

def build(self):
"""결과 반환"""
return self.string

# 메서드 체이닝
result = (StringBuilder()
.append("hello")
.append(" ")
.append("world")
.upper()
.build())

print(result) # HELLO WORLD

인스턴스 변수와 클래스 변수

인스턴스 변수

class Student:
def __init__(self, name, score):
# 각 인스턴스마다 고유한 값
self.name = name
self.score = score

student1 = Student("홍길동", 85)
student2 = Student("김철수", 92)

print(student1.name) # 홍길동
print(student2.name) # 김철수 (독립적)

클래스 변수

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

def __init__(self, name, score):
self.name = name
self.score = score
Student.student_count += 1 # 클래스 변수 수정

student1 = Student("홍길동", 85)
student2 = Student("김철수", 92)

print(Student.school) # 파이썬고등학교
print(student1.school) # 파이썬고등학교
print(Student.student_count) # 2

# 클래스 변수 수정
Student.school = "자바고등학교"
print(student1.school) # 자바고등학교 (모두 변경)
print(student2.school) # 자바고등학교

# ⚠️ 주의: 인스턴스에서 대입하면 인스턴스 변수 생성
student1.school = "C++고등학교"
print(student1.school) # C++고등학교 (인스턴스 변수)
print(student2.school) # 자바고등학교 (클래스 변수)
print(Student.school) # 자바고등학교 (클래스 변수)

클래스 변수 활용

class BankAccount:
# 클래스 변수
interest_rate = 0.03 # 이자율
account_count = 0

def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
BankAccount.account_count += 1

def apply_interest(self):
"""이자 적용"""
self.balance *= (1 + BankAccount.interest_rate)

@classmethod
def set_interest_rate(cls, rate):
"""이자율 변경"""
cls.interest_rate = rate

# 사용
account1 = BankAccount("홍길동", 1000)
account2 = BankAccount("김철수", 2000)

print(f"총 계좌 수: {BankAccount.account_count}") # 2

account1.apply_interest()
account2.apply_interest()

print(account1.balance) # 1030.0
print(account2.balance) # 2060.0

# 모든 계좌의 이자율 변경
BankAccount.set_interest_rate(0.05)
account1.apply_interest()
print(account1.balance) # 1081.5

클래스 메서드와 정적 메서드

@classmethod

class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day

@classmethod
def from_string(cls, date_string):
"""문자열로부터 생성"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # cls는 클래스 자체

@classmethod
def today(cls):
"""오늘 날짜"""
import datetime
now = datetime.date.today()
return cls(now.year, now.month, now.day)

def __str__(self):
return f"{self.year}-{self.month:02d}-{self.day:02d}"

# 일반 생성자
date1 = Date(2024, 12, 25)
print(date1) # 2024-12-25

# 클래스 메서드로 생성
date2 = Date.from_string("2024-12-25")
print(date2) # 2024-12-25

date3 = Date.today()
print(date3) # 오늘 날짜

# 상속에서도 동작
class KoreanDate(Date):
pass

korean_date = KoreanDate.from_string("2024-12-25")
print(type(korean_date)) # <class '__main__.KoreanDate'>

@staticmethod

class Math:
@staticmethod
def add(x, y):
"""두 수 더하기"""
return x + y

@staticmethod
def is_even(n):
"""짝수 판별"""
return n % 2 == 0

@staticmethod
def factorial(n):
"""팩토리얼"""
if n <= 1:
return 1
return n * Math.factorial(n - 1)

# 인스턴스 없이 호출
print(Math.add(3, 5)) # 8
print(Math.is_even(4)) # True
print(Math.factorial(5)) # 120

# 인스턴스로도 호출 가능 (잘 안 씀)
math = Math()
print(math.add(1, 2)) # 3

메서드 타입 비교

class MyClass:
class_variable = "클래스 변수"

def __init__(self, value):
self.instance_variable = value

def instance_method(self):
"""인스턴스 메서드: self 필요"""
return f"인스턴스: {self.instance_variable}"

@classmethod
def class_method(cls):
"""클래스 메서드: cls 필요"""
return f"클래스: {cls.class_variable}"

@staticmethod
def static_method():
"""정적 메서드: 매개변수 불필요"""
return "정적 메서드"

# 사용
obj = MyClass("값")

print(obj.instance_method()) # 인스턴스: 값
print(obj.class_method()) # 클래스: 클래스 변수
print(obj.static_method()) # 정적 메서드

# 클래스에서 직접 호출
# print(MyClass.instance_method()) # ❌ 에러
print(MyClass.class_method()) # ✅ 가능
print(MyClass.static_method()) # ✅ 가능

캡슐화 (Encapsulation)

네이밍 컨벤션

class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # public
self._balance = balance # protected (관례상 private)
self.__pin = "1234" # private (name mangling)

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, pin):
"""출금"""
if pin != self.__pin:
return False, "PIN 오류"
if amount > self._balance:
return False, "잔액 부족"
self._balance -= amount
return True, "출금 완료"

account = BankAccount("홍길동", 1000)

print(account.owner) # 홍길동 (public)
print(account._balance) # 1000 (접근 가능하지만 권장 안 함)
# print(account.__pin) # ❌ 에러

# name mangling으로 접근 가능 (하지만 하지 말 것!)
print(account._BankAccount__pin) # 1234

게터와 세터

class Temperature:
def __init__(self, celsius):
self._celsius = celsius

def get_celsius(self):
"""섭씨 온도 조회"""
return self._celsius

def set_celsius(self, value):
"""섭씨 온도 설정"""
if value < -273.15:
raise ValueError("절대영도보다 낮을 수 없습니다")
self._celsius = value

def get_fahrenheit(self):
"""화씨 온도 조회"""
return self._celsius * 9/5 + 32

def set_fahrenheit(self, value):
"""화씨 온도 설정"""
celsius = (value - 32) * 5/9
self.set_celsius(celsius)

temp = Temperature(25)
print(temp.get_celsius()) # 25
print(temp.get_fahrenheit()) # 77.0

temp.set_fahrenheit(86)
print(temp.get_celsius()) # 30.0

# temp.set_celsius(-300) # ValueError

property 데코레이터

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

# 속성처럼 사용
temp = Temperature(25)
print(temp.celsius) # 25 (get_celsius() 대신)
print(temp.fahrenheit) # 77.0

temp.fahrenheit = 86
print(temp.celsius) # 30.0

# temp.celsius = -300 # ValueError

읽기 전용 속성

class Circle:
def __init__(self, radius):
self._radius = radius

@property
def radius(self):
return self._radius

@property
def area(self):
"""읽기 전용"""
return 3.14 * self._radius ** 2

@property
def circumference(self):
"""읽기 전용"""
return 2 * 3.14 * self._radius

circle = Circle(5)
print(circle.radius) # 5
print(circle.area) # 78.5
print(circle.circumference) # 31.4

# circle.area = 100 # ❌ AttributeError (setter 없음)

실전 예제

도서 관리 시스템

class Book:
"""도서 클래스"""

# 클래스 변수
total_books = 0

def __init__(self, title, author, isbn, copies=1):
self.title = title
self.author = author
self.isbn = isbn
self._copies = copies
self._available = copies
Book.total_books += 1

@property
def available(self):
"""대출 가능한 책 수"""
return self._available

def borrow(self):
"""책 대출"""
if self._available > 0:
self._available -= 1
return True, f"'{self.title}' 대출 완료"
return False, "대출 가능한 책이 없습니다"

def return_book(self):
"""책 반납"""
if self._available < self._copies:
self._available += 1
return True, f"'{self.title}' 반납 완료"
return False, "반납할 책이 없습니다"

def __str__(self):
return f"{self.title} - {self.author} (대출가능: {self._available}/{self._copies})"

@classmethod
def from_dict(cls, data):
"""딕셔너리로부터 생성"""
return cls(
data['title'],
data['author'],
data['isbn'],
data.get('copies', 1)
)

# 사용
book1 = Book("파이썬 기초", "홍길동", "1234567890", copies=3)
book2 = Book.from_dict({
'title': '자바 입문',
'author': '김철수',
'isbn': '0987654321',
'copies': 2
})

print(book1)
success, message = book1.borrow()
print(message) # '파이썬 기초' 대출 완료
print(book1) # 대출가능: 2/3

print(f"\n총 도서 수: {Book.total_books}권")

은행 계좌 시스템

class BankAccount:
"""은행 계좌"""

# 계좌번호 자동 생성
_next_account_number = 1001

def __init__(self, owner, initial_balance=0):
self._account_number = BankAccount._next_account_number
BankAccount._next_account_number += 1

self._owner = owner
self._balance = initial_balance
self._transactions = []

@property
def account_number(self):
"""계좌번호 (읽기 전용)"""
return self._account_number

@property
def owner(self):
"""소유자 (읽기 전용)"""
return self._owner

@property
def balance(self):
"""잔액 (읽기 전용)"""
return self._balance

def deposit(self, amount):
"""입금"""
if amount <= 0:
return False, "0보다 큰 금액을 입력하세요"

self._balance += amount
self._transactions.append(f"입금: +{amount:,}원")
return True, f"{amount:,}원 입금 완료"

def withdraw(self, amount):
"""출금"""
if amount <= 0:
return False, "0보다 큰 금액을 입력하세요"

if amount > self._balance:
return False, "잔액이 부족합니다"

self._balance -= amount
self._transactions.append(f"출금: -{amount:,}원")
return True, f"{amount:,}원 출금 완료"

def transfer(self, other_account, amount):
"""이체"""
success, message = self.withdraw(amount)
if not success:
return False, message

other_account._balance += amount
other_account._transactions.append(
f"입금(이체): +{amount:,}원 from {self._account_number}"
)
self._transactions[-1] = f"출금(이체): -{amount:,}원 to {other_account._account_number}"

return True, f"{other_account._owner}님께 {amount:,}원 이체 완료"

def print_statement(self):
"""거래 내역"""
print(f"\n{'='*50}")
print(f"계좌번호: {self._account_number}")
print(f"소유자: {self._owner}")
print(f"잔액: {self._balance:,}원")
print(f"{'='*50}")

if self._transactions:
print("거래 내역:")
for i, transaction in enumerate(self._transactions, 1):
print(f"{i}. {transaction}")
else:
print("거래 내역이 없습니다")
print(f"{'='*50}\n")

# 사용
account1 = BankAccount("홍길동", 10000)
account2 = BankAccount("김철수", 5000)

account1.deposit(5000)
account1.withdraw(3000)
account1.transfer(account2, 2000)

account1.print_statement()
account2.print_statement()

쇼핑 카트

class Product:
"""상품 클래스"""

def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock

def __str__(self):
return f"{self.name} - {self.price:,}원 (재고: {self.stock})"


class CartItem:
"""장바구니 항목"""

def __init__(self, product, quantity):
self.product = product
self.quantity = quantity

@property
def subtotal(self):
"""소계"""
return self.product.price * self.quantity

def __str__(self):
return f"{self.product.name} x {self.quantity} = {self.subtotal:,}원"


class ShoppingCart:
"""쇼핑 카트"""

def __init__(self):
self._items = []

def add_item(self, product, quantity=1):
"""상품 추가"""
if quantity <= 0:
return False, "수량은 1 이상이어야 합니다"

if quantity > product.stock:
return False, f"재고가 부족합니다 (현재 재고: {product.stock})"

# 이미 있는 상품인지 확인
for item in self._items:
if item.product == product:
item.quantity += quantity
return True, f"{product.name} {quantity}개 추가"

# 새 상품 추가
self._items.append(CartItem(product, quantity))
return True, f"{product.name} {quantity}개 추가"

def remove_item(self, product):
"""상품 제거"""
for item in self._items:
if item.product == product:
self._items.remove(item)
return True, f"{product.name} 제거"
return False, "장바구니에 해당 상품이 없습니다"

def update_quantity(self, product, quantity):
"""수량 변경"""
if quantity <= 0:
return self.remove_item(product)

if quantity > product.stock:
return False, f"재고가 부족합니다 (현재 재고: {product.stock})"

for item in self._items:
if item.product == product:
item.quantity = quantity
return True, f"{product.name} 수량 변경: {quantity}개"

return False, "장바구니에 해당 상품이 없습니다"

@property
def total(self):
"""총액"""
return sum(item.subtotal for item in self._items)

@property
def item_count(self):
"""총 상품 수"""
return sum(item.quantity for item in self._items)

def print_cart(self):
"""장바구니 출력"""
if not self._items:
print("장바구니가 비어있습니다")
return

print("\n" + "="*50)
print("장바구니")
print("="*50)

for i, item in enumerate(self._items, 1):
print(f"{i}. {item}")

print("-"*50)
print(f"총 {self.item_count}개 상품")
print(f"합계: {self.total:,}원")
print("="*50 + "\n")

def checkout(self):
"""결제"""
if not self._items:
return False, "장바구니가 비어있습니다"

# 재고 확인
for item in self._items:
if item.quantity > item.product.stock:
return False, f"{item.product.name} 재고 부족"

# 재고 차감
for item in self._items:
item.product.stock -= item.quantity

total = self.total
self._items.clear()

return True, f"결제 완료: {total:,}원"

# 사용
laptop = Product("노트북", 1500000, 10)
mouse = Product("마우스", 30000, 50)
keyboard = Product("키보드", 80000, 30)

cart = ShoppingCart()

cart.add_item(laptop, 1)
cart.add_item(mouse, 2)
cart.add_item(keyboard, 1)

cart.print_cart()

cart.update_quantity(mouse, 3)
cart.print_cart()

success, message = cart.checkout()
print(message)

# 재고 확인
print(f"\n남은 재고:")
print(laptop)
print(mouse)
print(keyboard)

자주 묻는 질문

Q1. 언제 클래스를 만들어야 하나요?

A: 다음 경우에 클래스를 고려하세요

# 클래스가 유용한 경우
# 1. 관련된 데이터를 묶어야 할 때
# 2. 상태와 행동을 함께 관리할 때
# 3. 같은 타입의 객체가 여러 개 필요할 때
# 4. 코드 재사용과 확장이 필요할 때

# 예: 학생 관리
class Student:
def __init__(self, name, scores):
self.name = name
self.scores = scores

def average(self):
return sum(self.scores) / len(self.scores)

# 여러 학생 객체
students = [
Student("홍길동", [85, 90, 88]),
Student("김철수", [92, 88, 95])
]

Q2. 인스턴스 변수와 클래스 변수는 언제 사용하나요?

A: 공유 여부에 따라 선택

class Product:
# 클래스 변수: 모든 인스턴스가 공유
tax_rate = 0.1 # 세율

def __init__(self, name, price):
# 인스턴스 변수: 각 인스턴스마다 다름
self.name = name
self.price = price

def total_price(self):
return self.price * (1 + Product.tax_rate)

# 모든 상품의 세율 변경
Product.tax_rate = 0.15

# 각 상품은 독립적인 이름과 가격
product1 = Product("노트북", 1000)
product2 = Product("마우스", 30)

Q3. self는 왜 필요한가요?

A: 인스턴스 자신을 참조하기 위해

class Counter:
def __init__(self):
# self 없으면 지역 변수가 됨
self.count = 0 # 인스턴스 변수

def increment(self):
# self로 인스턴스 변수 접근
self.count += 1

# self가 없다면?
# def increment():
# count += 1 # 어떤 count? 에러!

counter = Counter()
counter.increment()
# 실제로는 Counter.increment(counter)로 호출

Q4. private 변수는 왜 사용하나요?

A: 외부 접근을 제한하여 데이터 무결성 유지

class BankAccount:
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

account = BankAccount(1000)

# ✅ 올바른 방법
account.deposit(500)
print(account.get_balance()) # 1500

# ❌ 직접 접근 불가 (보호)
# account.__balance = -1000 # 새로운 속성 생성 (원본 아님)

# 메서드를 통해서만 수정 가능 → 검증 로직 추가 가능

다음 단계

클래스의 기초를 마스터했습니다!

핵심 정리:
✅ 클래스와 인스턴스 개념
✅ __init__으로 초기화
✅ 인스턴스 메서드와 self
✅ 클래스 변수 vs 인스턴스 변수
✅ @classmethod와 @staticmethod
✅ 캡슐화와 property

다음 단계: 고급 OOP에서 상속, 다형성, 매직 메서드를 배워보세요!