파이썬(Python) 공부 10편
클래스(Class)와 객체지향 기초 완전 정복
지금까지 함수·리스트·딕셔너리를 배웠다면, 이제는 이것들을 하나로 묶어 구조화하는 방법을 익힐 차례입니다. 객체지향 프로그래밍(OOP)은 현실의 사물을 코드로 표현하는 방법론입니다. 클래스 정의부터 생성자·상속·캡슐화·매직 메서드까지 핵심 개념을 한 번에 정리합니다.
💡 클래스(Class)란 — 설계도와 실제 물건
객체지향 프로그래밍에서 클래스(Class)는 설계도이고, 인스턴스(Instance)는 그 설계도로 만들어진 실제 물건입니다. 붕어빵 틀이 클래스라면, 틀로 찍어낸 붕어빵 하나하나가 인스턴스입니다.
속성(데이터)과 메서드(동작)를 하나로 묶은 틀. 한 번 정의하면 인스턴스를 여러 개 만들 수 있다.
예: Dog 클래스 → 모든 개의 공통 특성 정의
클래스로 만들어진 개별 객체. 같은 클래스에서 만들어도 각자 고유한 데이터를 가진다.
예: buddy = Dog("버디") → 버디라는 개 한 마리
📝 클래스 정의 기본 — class 키워드와 __init__
# 클래스 정의 class Dog: """개를 표현하는 클래스""" # __init__ : 생성자 — 인스턴스가 만들어질 때 자동 호출 def __init__(self, name: str, breed: str, age: int): self.name = name # 인스턴스 변수 (각 인스턴스마다 독립) self.breed = breed self.age = age # 인스턴스 메서드 — 첫 번째 인수는 항상 self def bark(self): print(f"{self.name}: 멍멍!") def info(self): print(f"{self.name} | {self.breed} | {self.age}살") def birthday(self): """나이를 1 늘린다""" self.age += 1 print(f"{self.name} 생일 축하! 이제 {self.age}살") # 인스턴스 생성 — 클래스명(인수) 형태 buddy = Dog("버디", "골든리트리버", 3) navi = Dog("나비", "말티즈", 5) # 메서드 호출 buddy.bark() # 버디: 멍멍! navi.info() # 나비 | 말티즈 | 5살 buddy.birthday() # 버디 생일 축하! 이제 4살 # 속성 직접 접근·수정 print(buddy.name) # 버디 buddy.name = "버디2" # 직접 수정도 가능
self는 메서드가 호출될 때 파이썬이 자동으로 넘겨주는 인스턴스 자신의 참조입니다. buddy.bark()를 호출하면 파이썬이 내부적으로 Dog.bark(buddy)처럼 처리합니다.
- 인스턴스 변수를 읽거나 쓸 때: self.name
- 같은 클래스의 다른 메서드를 호출할 때: self.bark()
- self는 관례적인 이름 — 파이썬 문법상 강제는 아니지만 반드시 지킬 것
📊 클래스 변수 vs 인스턴스 변수
class Dog: # 클래스 변수 — 모든 인스턴스가 공유 species = "Canis lupus familiaris" count = 0 # 생성된 개 수 추적 def __init__(self, name: str): self.name = name # 인스턴스 변수 — 각자 독립 Dog.count += 1 # 클래스 변수 수정은 클래스명으로 d1 = Dog("버디") d2 = Dog("나비") d3 = Dog("콩이") print(Dog.count) # 3 (클래스 변수 — 공유) print(d1.count) # 3 (인스턴스로도 접근 가능) print(d1.name) # 버디 (인스턴스 변수 — 독립) print(d2.name) # 나비 print(Dog.species) # Canis lupus familiaris # ⚠️ 인스턴스로 클래스 변수를 '수정'하면 인스턴스 변수가 새로 생성됨 d1.species = "개" # d1만의 species가 생김 (클래스 변수 영향 없음) print(d1.species) # 개 (d1 인스턴스 변수) print(Dog.species) # Canis lupus familiaris (클래스 변수 그대로)
🔨 메서드 3종류 — 인스턴스·클래스·정적 메서드
class MathHelper: pi = 3.14159 # 클래스 변수 # ① 인스턴스 메서드 — self 받음 / 인스턴스·클래스 변수 모두 접근 def circle_area(self, r: float) -> float: return self.pi * r ** 2 # ② 클래스 메서드 — cls 받음 / 클래스 변수 접근 / @classmethod 데코레이터 @classmethod def set_pi(cls, value: float): cls.pi = value # 클래스 변수 수정 # ③ 정적 메서드 — self/cls 없음 / 독립적인 유틸리티 함수 @staticmethod def add(a: float, b: float) -> float: return a + b m = MathHelper() print(m.circle_area(5)) # 78.53975 (인스턴스 메서드) MathHelper.set_pi(3.14) # 클래스 메서드 — 클래스명으로 호출 print(MathHelper.add(3, 4)) # 7 (정적 메서드)
- 인스턴스 메서드: 인스턴스의 상태(self.변수)를 읽거나 수정해야 할 때 → 대부분의 경우
- 클래스 메서드: 클래스 변수를 수정하거나, 대체 생성자(from_xxx 패턴)를 만들 때
- 정적 메서드: 클래스·인스턴스와 관계없는 독립 유틸리티 함수 — 클래스 안에 묶어두는 것이 논리적으로 맞을 때
🌀 상속(Inheritance) — 부모 클래스 기능 물려받기
상속은 기존 클래스(부모·슈퍼클래스)의 속성과 메서드를 새 클래스(자식·서브클래스)가 그대로 물려받는 기능입니다. 공통 코드를 한 곳에서 관리하고, 자식 클래스에서 필요한 부분만 추가·수정(오버라이딩)할 수 있습니다.
# 부모 클래스 (Parent / Super / Base class) class Animal: def __init__(self, name: str, age: int): self.name = name self.age = age def introduce(self): print(f"이름: {self.name}, 나이: {self.age}살") def sound(self): print("...") # 자식에서 오버라이드할 예정 # 자식 클래스 (Child / Sub / Derived class) class Dog(Animal): # Animal을 상속 def __init__(self, name: str, age: int, breed: str): super().__init__(name, age) # 부모 __init__ 호출 self.breed = breed # Dog만의 속성 추가 def sound(self): # 메서드 오버라이딩 print(f"{self.name}: 멍멍!") def fetch(self): # Dog만의 새 메서드 print(f"{self.name}가 공을 가져옵니다!") class Cat(Animal): def sound(self): print(f"{self.name}: 야옹~") dog = Dog("버디", 3, "골든리트리버") cat = Cat("나비", 5) dog.introduce() # 이름: 버디, 나이: 3살 (부모 메서드 그대로 사용) dog.sound() # 버디: 멍멍! (오버라이딩된 메서드) dog.fetch() # 버디가 공을 가져옵니다! cat.sound() # 나비: 야옹~ # isinstance — 인스턴스 타입 확인 print(isinstance(dog, Dog)) # True print(isinstance(dog, Animal)) # True (부모 타입으로도 True) print(isinstance(dog, Cat)) # False
super()는 부모 클래스를 참조합니다. 자식 클래스의 __init__에서 super().__init__(...)을 호출하면 부모의 초기화 로직을 그대로 실행하면서 자식의 추가 속성만 따로 설정할 수 있습니다.
- 부모 __init__ 호출을 빠뜨리면 부모 속성(name, age)이 초기화되지 않음
- 오버라이드한 메서드 안에서 부모 버전도 실행하고 싶을 때 super().메서드명() 활용
🔒 캡슐화(Encapsulation) — 데이터 보호
캡슐화는 객체 내부 데이터를 외부에서 함부로 바꾸지 못하도록 보호하는 개념입니다. 파이썬은 완벽한 접근 제한 대신 이름 관례(naming convention)로 접근 수준을 표시합니다.
class BankAccount: def __init__(self, owner: str, balance: int): self.owner = owner # 공개 (public) self._balance = balance # 보호 (protected) — 밑줄 1개: "내부용이니 직접 건드리지 마세요" self.__pin = "1234" # 비공개 (private) — 밑줄 2개: 이름 맹글링 적용 # property — getter (읽기 전용 속성처럼 사용) @property def balance(self) -> int: return self._balance # setter — 유효성 검사 포함 @balance.setter def balance(self, value: int): if value < 0: raise ValueError("잔액은 0원 이상이어야 합니다") self._balance = value def deposit(self, amount: int): self.balance += amount # setter 호출 print(f"입금 {amount}원 → 잔액 {self.balance}원") def withdraw(self, amount: int): if amount > self._balance: print("잔액 부족") return self.balance -= amount print(f"출금 {amount}원 → 잔액 {self.balance}원") acc = BankAccount("Alice", 10000) print(acc.balance) # 10000 (property getter 호출) acc.deposit(5000) # 입금 5000원 → 잔액 15000원 acc.withdraw(3000) # 출금 3000원 → 잔액 12000원 # property setter 유효성 검사 try: acc.balance = -100 # setter → ValueError 발생 except ValueError as e: print(e) # __ (이름 맹글링) — 클래스 외부에서 직접 접근 시 이름이 바뀜 # acc.__pin → AttributeError! # acc._BankAccount__pin → 우회 가능하지만 하면 안 됨
- name (밑줄 없음) → 공개(public): 누구나 접근 가능
- _name (밑줄 1개) → 보호(protected): 관례상 "내부 또는 서브클래스에서만 사용" 표시. 기술적으론 막지 않음
- __name (밑줄 2개) → 비공개(private): 이름 맹글링으로 _클래스명__name으로 변환 → 외부 접근 어렵게 만듦
⚡ 매직 메서드(Magic Method) — 파이썬 내장 동작 정의하기
앞뒤로 언더스코어 두 개(__메서드명__)가 붙은 메서드를 매직 메서드(Dunder Method)라고 합니다. 파이썬이 특정 상황에서 자동으로 호출하며, 이를 정의하면 클래스가 내장 타입처럼 동작하게 됩니다.
| 메서드 | 호출 시점 | 예시 |
|---|---|---|
| __init__(self, ...) | 인스턴스 생성 시 | Dog("버디") |
| __str__(self) | str() 또는 print() 호출 시 | print(dog) |
| __repr__(self) | repr() 또는 REPL 출력 시 | repr(dog) |
| __len__(self) | len() 호출 시 | len(my_obj) |
| __eq__(self, other) | == 비교 시 | a == b |
| __lt__(self, other) | < 비교 시 | a < b |
| __add__(self, other) | + 연산 시 | a + b |
| __getitem__(self, key) | obj[key] 접근 시 | obj[0] |
| __iter__(self) | for문 또는 iter() 시 | for x in obj |
| __contains__(self, item) | in 연산 시 | "x" in obj |
| __del__(self) | 인스턴스 소멸 시 | 가비지 컬렉션 |
class Vector: """2D 벡터 클래스 — 매직 메서드 활용 예시""" def __init__(self, x: float, y: float): self.x = x self.y = y def __str__(self) -> str: return f"Vector({self.x}, {self.y})" def __repr__(self) -> str: return f"Vector(x={self.x}, y={self.y})" def __add__(self, other: 'Vector') -> 'Vector': return Vector(self.x + other.x, self.y + other.y) def __eq__(self, other) -> bool: if not isinstance(other, Vector): return NotImplemented return self.x == other.x and self.y == other.y def __len__(self) -> int: return 2 # 2D 벡터는 항상 요소 2개 def __abs__(self) -> float: return (self.x**2 + self.y**2) ** 0.5 # 벡터 크기 v1 = Vector(1, 2) v2 = Vector(3, 4) print(v1) # Vector(1, 2) ← __str__ print(v1 + v2) # Vector(4, 6) ← __add__ print(v1 == Vector(1, 2)) # True ← __eq__ print(len(v1)) # 2 ← __len__ print(abs(v2)) # 5.0 ← __abs__
📝 10편 실습 문제
- Book 클래스: title·author·price·is_available 속성
- 대출(borrow())·반납(return_book()) 메서드 구현
- __str__으로 책 정보를 보기 좋게 출력
- price는 property로 구현해 음수 입력 방어
- Employee(name, salary) 부모 클래스 → introduce() 메서드
- Manager(name, salary, team_size) 자식 클래스 → introduce() 오버라이딩 (부모 호출 후 팀 사이즈 추가)
# 실습 1 — Book 클래스 class Book: def __init__(self, title: str, author: str, price: int): self.title = title self.author = author self._price = price self.is_available = True @property def price(self): return self._price @price.setter def price(self, v): if v < 0: raise ValueError("가격은 0원 이상") self._price = v def borrow(self): if not self.is_available: print(f"'{self.title}'은 이미 대출 중"); return self.is_available = False print(f"'{self.title}' 대출 완료") def return_book(self): self.is_available = True print(f"'{self.title}' 반납 완료") def __str__(self): status = "대출 가능" if self.is_available else "대출 중" return f"[{status}] {self.title} / {self.author} / {self.price}원" b = Book("파이썬 완전 정복", "홍길동", 25000) print(b) # [대출 가능] 파이썬 완전 정복 / 홍길동 / 25000원 b.borrow() # '파이썬 완전 정복' 대출 완료 b.borrow() # '파이썬 완전 정복'은 이미 대출 중 b.return_book() # '파이썬 완전 정복' 반납 완료 # 실습 2 — 상속 class Employee: def __init__(self, name: str, salary: int): self.name = name self.salary = salary def introduce(self): print(f"이름: {self.name}, 연봉: {self.salary:,}원") class Manager(Employee): def __init__(self, name, salary, team_size: int): super().__init__(name, salary) self.team_size = team_size def introduce(self): super().introduce() print(f"담당 팀원: {self.team_size}명") Manager("Alice", 80000000, 10).introduce() # 이름: Alice, 연봉: 80,000,000원 # 담당 팀원: 10명
- 클래스 / 인스턴스: 클래스 = 설계도 / 인스턴스 = 실제 객체 / 클래스명(인수)로 생성
- __init__: 생성자 — 인스턴스 생성 시 자동 호출 / 인스턴스 변수 초기화
- self: 인스턴스 자기 자신 참조 / 모든 인스턴스 메서드 첫 번째 인수
- 클래스 변수 vs 인스턴스 변수: 클래스 변수는 모든 인스턴스 공유 / 인스턴스 변수는 각자 독립
- 메서드 3종: 인스턴스(self) / 클래스(@classmethod, cls) / 정적(@staticmethod, 인수 없음)
- 상속: class 자식(부모): / super()로 부모 메서드 호출 / 오버라이딩으로 재정의
- 캡슐화: 밑줄 없음(공개) / _밑줄(보호) / __밑줄(비공개·맹글링)
- property: @property·@속성.setter로 getter/setter 구현 → 유효성 검사 포함
- 매직 메서드: __str__·__repr__·__add__·__eq__·__len__ 등 — 내장 연산자·함수와 연동
- isinstance(): 인스턴스 타입 확인 / 부모 클래스 타입으로도 True
import / from·import / 표준 라이브러리 / pip / 가상환경 / __name__