객체지향 프로그래밍(OOP, Object-Oriented Programming)은 프로그램을 객체 단위로 구성하는 방식이다. 객체는 데이터인 속성(attribute)과 동작인 메서드(method)를 함께 가진다.
객체지향 프로그래밍의 핵심 개념은 보통 다음 네 가지로 정리한다.
| 개념 | 핵심 의미 |
|---|---|
| 캡슐화 | 데이터와 메서드를 하나로 묶고, 외부의 직접 접근을 제한한다. |
| 상속 | 기존 클래스의 속성과 메서드를 물려받아 재사용하고 확장한다. |
| 다형성 | 같은 이름의 메서드가 객체 종류에 따라 다르게 동작한다. |
| 추상화 | 복잡한 내부 구현은 숨기고 핵심 기능만 외부에 제공한다. |
이 네 가지 개념을 활용하면 코드의 재사용성, 확장성, 유지보수성을 높일 수 있다.
캡슐화(encapsulation)는 객체의 데이터와 기능을 하나로 묶고, 외부에서 내부 구현에 직접 접근하지 못하도록 제한하는 개념이다.
파이썬은 Java나 C++처럼 접근 제어자를 강제하지 않는다. 대신 이름 규칙으로 접근 수준을 표현한다.
| 형태 | 이름 예시 | 의미 |
|---|---|---|
| public | name | 외부에서 자유롭게 접근 가능 |
| protected 관례 | _name | 클래스 내부 또는 자식 클래스에서 사용하자는 관례 |
| private 네임 맹글링 | __name | 외부 직접 접근을 어렵게 만들기 위해 이름을 변경 |
가격처럼 잘못된 값이 들어오면 안 되는 데이터는 직접 수정하게 두기보다 메서드를 통해 검증하는 것이 좋다.
class Fruit:
def __init__(self, name, price):
self.name = name # public
self.__price = price # private처럼 사용
def get_price(self):
return self.__price
def set_price(self, price):
if price > 0:
self.__price = price
else:
print("가격은 0보다 커야 한다.")
def print_info(self):
print(f"과일: {self.name}")
print(f"가격: {self.__price}")
class Apple(Fruit):
def __init__(self, name, price, origin):
super().__init__(name, price)
self.origin = origin
def print_origin(self):
print(f"{self.name}의 원산지: {self.origin}")
apple = Apple("사과", 3000, "한국")
apple.print_info()
apple.print_origin()
print("가격 조회:", apple.get_price())
print("----- 가격 수정 -----")
apple.set_price(3500)
print("수정된 가격:", apple.get_price())
print("----- 잘못된 값 입력 -----")
apple.set_price(-1000)
print("----- 외부 접근 시도 -----")
apple.__price = 10000
print("getter로 확인:", apple.get_price())
print("직접 접근:", apple.__price)
출력 결과:
과일: 사과
가격: 3000
사과의 원산지: 한국
가격 조회: 3000
----- 가격 수정 -----
수정된 가격: 3500
----- 잘못된 값 입력 -----
가격은 0보다 커야 한다.
----- 외부 접근 시도 -----
getter로 확인: 3500
직접 접근: 10000
self.__price는 실제로 완전한 private이 되는 것이 아니다. 파이썬 내부에서 _Fruit__price라는 이름으로 바뀐다. 이를 네임 맹글링(name mangling)이라고 한다.
class Fruit:
def __init__(self, price):
self.__price = price
fruit = Fruit(3000)
print(fruit.__dict__)
print(fruit._Fruit__price)
출력 결과:
{'_Fruit__price': 3000}
3000
즉, __price는 외부 접근을 어렵게 만드는 장치이지 보안을 위한 완전한 차단 장치는 아니다.
@property로 Pythonic하게 캡슐화하기파이썬에서는 getter/setter 메서드를 직접 호출하기보다 @property를 사용해 속성처럼 접근하는 방식을 자주 사용한다.
class Fruit:
def __init__(self, name, price):
self.name = name
self.__price = price
@property
def price(self):
return self.__price
@price.setter
def price(self, value):
if value > 0:
self.__price = value
else:
print("가격은 0보다 커야 한다.")
def print_info(self):
print(f"과일: {self.name}")
print(f"가격: {self.price}")
class Apple(Fruit):
def __init__(self, name, price, origin):
super().__init__(name, price)
self.origin = origin
def print_origin(self):
print(f"{self.name}의 원산지: {self.origin}")
apple = Apple("사과", 3000, "한국")
apple.print_info()
apple.print_origin()
print("가격 조회:", apple.price)
print("----- 가격 수정 -----")
apple.price = 3500
print("수정된 가격:", apple.price)
print("----- 잘못된 값 입력 -----")
apple.price = -1000
print("----- 외부 접근 시도 -----")
apple.__price = 10000
print("실제 가격:", apple.price)
print("외부에서 만든 값:", apple.__price)
출력 결과:
과일: 사과
가격: 3000
사과의 원산지: 한국
가격 조회: 3000
----- 가격 수정 -----
수정된 가격: 3500
----- 잘못된 값 입력 -----
가격은 0보다 커야 한다.
----- 외부 접근 시도 -----
실제 가격: 3500
외부에서 만든 값: 10000
@property를 사용하면 메서드로 검증 로직을 유지하면서도 외부에서는 속성처럼 읽고 쓸 수 있다.
상속(inheritance)은 기존 클래스의 속성과 메서드를 새로운 클래스가 물려받는 구조이다. 기존 클래스를 부모 클래스, 상속받는 클래스를 자식 클래스라고 부른다.
class Parent:
pass
class Child(Parent):
pass
자식 클래스는 부모 클래스의 생성자와 메서드를 사용할 수 있다.
class Fruit:
def __init__(self, name, quantity):
self.name = name
self.quantity = quantity
def store(self, place):
print(f"{self.name}를 {place}에 보관한다.")
def sell(self, amount):
print(f"{self.name}를 {amount}개 판매한다.")
class Apple(Fruit):
pass
fruit = Fruit("배", 100)
fruit.store("창고")
fruit.sell(10)
apple = Apple("사과", 50)
apple.store("냉장고")
apple.sell(5)
출력 결과:
배를 창고에 보관한다.
배를 10개 판매한다.
사과를 냉장고에 보관한다.
사과를 5개 판매한다.
Apple 클래스에는 아무 메서드도 직접 정의하지 않았지만 Fruit를 상속했기 때문에 store()와 sell()을 사용할 수 있다.
자식 클래스에 __init__()이 없으면 부모 클래스의 생성자가 자동으로 호출된다.
class Fruit:
def __init__(self, name):
print("Fruit 생성자 호출")
self.name = name
class Apple(Fruit):
pass
apple = Apple("사과")
print(apple.name)
출력 결과:
Fruit 생성자 호출
사과
자식 클래스에 __init__()을 직접 정의하면 부모 생성자는 자동으로 호출되지 않는다.
class Fruit:
def __init__(self, name):
print("Fruit 생성자 호출")
self.name = name
class Apple(Fruit):
def __init__(self, name):
print("Apple 생성자 호출")
apple = Apple("사과")
# print(apple.name) # AttributeError: 'Apple' object has no attribute 'name'
출력 결과:
Apple 생성자 호출
부모 클래스의 초기화 로직이 필요하다면 super()로 부모 생성자를 명시적으로 호출해야 한다.
class Fruit:
def __init__(self, name):
print("Fruit 생성자 호출")
self.name = name
class Apple(Fruit):
def __init__(self, name):
super().__init__(name)
print("Apple 생성자 호출")
apple = Apple("사과")
print(apple.name)
출력 결과:
Fruit 생성자 호출
Apple 생성자 호출
사과
오버라이딩(overriding)은 부모 클래스에 정의된 메서드를 자식 클래스에서 같은 이름으로 다시 정의하는 것이다. 자식 클래스는 부모의 기본 동작을 자신에게 맞게 변경할 수 있다.
class Fruit:
def __init__(self, name, quantity):
self.name = name
self.quantity = quantity
def eat(self):
print(f"{self.name}를 먹는다.")
def store(self, place):
print(f"{self.name}를 {place}에 보관한다.")
class Apple(Fruit):
def wash(self):
print(f"{self.name}를 깨끗이 씻는다.")
def eat(self):
print(f"{self.name}를 아주 맛있게 먹는다.")
def super_eat(self):
super().eat()
apple = Apple("사과", 10)
apple.eat()
apple.store("냉장고")
apple.wash()
print("----- 부모 메서드 호출 -----")
apple.super_eat()
fruit = Fruit("배", 20)
fruit.eat()
fruit.store("창고")
출력 결과:
사과를 아주 맛있게 먹는다.
사과를 냉장고에 보관한다.
사과를 깨끗이 씻는다.
----- 부모 메서드 호출 -----
사과를 먹는다.
배를 먹는다.
배를 창고에 보관한다.
Apple.eat()은 Fruit.eat()을 오버라이딩한다. 따라서 apple.eat()을 호출하면 자식 클래스의 메서드가 실행된다.
다중 상속은 하나의 클래스가 둘 이상의 부모 클래스를 상속받는 기능이다.
class Parent1:
pass
class Parent2:
pass
class Child(Parent1, Parent2):
pass
출력 결과:
출력 결과 없음
파이썬은 다중 상속을 지원하지만, 부모 클래스에 같은 이름의 메서드가 있을 때 어떤 메서드를 먼저 찾을지 정해야 한다. 이 순서를 MRO(Method Resolution Order)라고 한다.
class Fruit:
def __init__(self, name, quantity):
self.name = name
self.quantity = quantity
def eat(self):
print(f"{self.name}를 먹는다.")
def sleep(self, hour):
print(f"{self.name}를 {hour}시간 동안 보관한다.")
class Storage:
def process(self, hour):
print(f"{self.name}를 {hour}시간 동안 가공한다.")
def sleep(self, hour):
print(f"{self.name}를 {hour}시간 동안 저온 숙성한다.")
class ProcessedFruit(Fruit, Storage):
pass
fruit = ProcessedFruit("사과", 10)
fruit.eat()
fruit.process(2)
fruit.sleep(8)
print(ProcessedFruit.mro())
출력 결과:
사과를 먹는다.
사과를 2시간 동안 가공한다.
사과를 8시간 동안 보관한다.
[<class '__main__.ProcessedFruit'>, <class '__main__.Fruit'>, <class '__main__.Storage'>, <class 'object'>]
ProcessedFruit(Fruit, Storage)처럼 작성했기 때문에 sleep()을 찾을 때 Fruit를 먼저 확인한다. 따라서 Storage.sleep()이 아니라 Fruit.sleep()이 실행된다.
object 클래스파이썬의 모든 클래스는 명시적으로 상속하지 않아도 최상위 부모인 object 클래스를 상속한다.
class Fruit:
pass
print(Fruit.mro())
print(isinstance(Fruit(), object))
출력 결과:
[<class '__main__.Fruit'>, <class 'object'>]
True
object는 모든 객체가 가져야 할 최소한의 공통 기능을 제공한다. __str__, __repr__, __eq__ 같은 기본 매직 메서드도 object로부터 시작된다.
super()super()는 단순히 “부모 클래스 하나”를 의미하는 것이 아니라, MRO 순서상 다음 클래스를 찾아가는 도구이다.
class Base:
def hello(self):
print("Base")
class Clean(Base):
def hello(self):
print("Clean")
super().hello()
class Pack(Base):
def hello(self):
print("Pack")
super().hello()
class Product(Clean, Pack):
def hello(self):
print("Product")
super().hello()
p = Product()
p.hello()
print(Product.mro())
출력 결과:
Product
Clean
Pack
Base
[<class '__main__.Product'>, <class '__main__.Clean'>, <class '__main__.Pack'>, <class '__main__.Base'>, <class 'object'>]
Product의 MRO는 Product -> Clean -> Pack -> Base -> object 순서이다. 각 클래스에서 super().hello()를 호출하면 이 순서대로 다음 클래스의 hello()가 실행된다.
출처 및 참고 자료
본 정리는 강의 내용과 아래 자료를 참고하여 학습 목적으로 작성하였습니다.