객체지향 프로그래밍(OOP, Object-Oriented Programming)은 프로그램을 객체 단위로 구성하는 방식이다. 객체는 데이터인 속성(attribute)과 동작인 메서드(method)를 함께 가진다.
객체지향 프로그래밍의 핵심 개념은 보통 다음 네 가지로 정리한다.
| 개념 | 핵심 의미 |
|---|---|
| 캡슐화 | 데이터와 메서드를 하나로 묶고, 외부의 직접 접근을 제한한다. |
| 상속 | 기존 클래스의 속성과 메서드를 물려받아 재사용하고 확장한다. |
| 다형성 | 같은 이름의 메서드가 객체 종류에 따라 다르게 동작한다. |
| 추상화 | 복잡한 내부 구현은 숨기고 핵심 기능만 외부에 제공한다. |
이 네 가지 개념을 활용하면 코드의 재사용성, 확장성, 유지보수성을 높일 수 있다.
다형성(polymorphism) 은 같은 이름의 메서드라도 객체의 종류에 따라 다르게 동작할 수 있는 특성이다.
파이썬은 정적 타입 언어처럼 명시적인 인터페이스 구현을 강제하지 않아도, 객체가 필요한 메서드를 가지고 있으면 사용할 수 있다. 이를 덕 타이핑(duck typing)이라고 한다.
"오리처럼 걷고 오리처럼 소리 내면 오리로 볼 수 있다" 는 사고방식에서 나온 표현이다. 파이썬에서는 객체의 실제 타입보다 "필요한 동작을 할 수 있는가" 가 중요하다.
class Fruit:
def describe(self):
print("과일이다.")
class Apple(Fruit):
def describe(self):
print("사과는 아삭하다.")
class Banana(Fruit):
def describe(self):
print("바나나는 달콤하다.")
fruits = [Apple(), Banana(), Fruit()]
for fruit in fruits:
fruit.describe()
출력 결과 :
사과는 아삭하다.
바나나는 달콤하다.
과일이다.
반복문에서는 모두 fruit.describe() 라는 같은 코드를 호출한다. 하지만 실제 객체가 Apple 인지, Banana 인지, Fruit 인지에 따라 실행 결과가 달라진다.
다형성은 반드시 상속이 있어야만 가능한 것은 아니다. 서로 다른 클래스라도 같은 이름의 메서드를 가지고 있으면 공통된 방식으로 처리할 수 있다.
class CardPayment:
def pay(self, amount):
print(f"카드로 {amount}원 결제한다.")
class CashPayment:
def pay(self, amount):
print(f"현금으로 {amount}원 결제한다.")
class KakaoPay:
def pay(self, amount):
print(f"카카오페이로 {amount}원 결제한다.")
def process_payment(payment, amount):
payment.pay(amount)
card = CardPayment()
cash = CashPayment()
kakao = KakaoPay()
process_payment(card, 10000)
process_payment(cash, 5000)
process_payment(kakao, 2000)
출력 결과 :
카드로 10000원 결제한다.
현금으로 5000원 결제한다.
카카오페이로 2000원 결제한다.
process_payment() 함수는 전달받은 객체가 어떤 클래스인지 검사하지 않는다. 그 객체에 pay() 메서드가 있다고 믿고 호출한다. 이처럼 같은 인터페이스를 가진 객체를 동일한 방식으로 다루는 것이 다형성의 핵심이다.
새로운 결제 수단을 추가할 때도 기존 process_payment() 함수를 수정할 필요가 없다.
class NaverPay:
def pay(self, amount):
print(f"네이버페이로 {amount}원 결제한다.")
naver = NaverPay()
process_payment(naver, 3000)
출력 결과 :
네이버페이로 3000원 결제한다.
이 구조는 확장에 유리하다. 결제 수단이 늘어나도 각 클래스가 pay() 메서드만 제공하면 공통 처리 함수는 그대로 사용할 수 있다.
추상화(abstraction) 는 복잡한 내부 구현은 숨기고, 외부에는 꼭 필요한 기능만 제공하는 개념이다.
사용자는 객체가 내부적으로 어떻게 동작하는지 몰라도, 제공된 메서드만으로 기능을 사용할 수 있다. 추상화는 "어떻게 동작하는가"보다 "무엇을 할 수 있는가" 에 집중하게 만드는 설계 방식이다.
파이썬에서는 일반 클래스 설계만으로도 추상화를 표현할 수 있고, 더 명확한 규칙이 필요하면 abc 모듈의 ABC, abstractmethod 를 사용해 추상 클래스를 만들 수 있다.
from abc import ABC, abstractmethod
class Fruit(ABC):
@abstractmethod
def eat(self):
pass
class Apple(Fruit):
def eat(self):
print("사과를 먹는다.")
class Banana(Fruit):
def eat(self):
print("바나나를 먹는다.")
apple = Apple()
banana = Banana()
apple.eat()
banana.eat()
# fruit = Fruit() # TypeError: 추상 클래스는 직접 인스턴스화할 수 없음
출력 결과 :
사과를 먹는다.
바나나를 먹는다.
추상 클래스는 "자식 클래스가 반드시 구현해야 하는 메서드" 를 정해준다. 위 예제에서 Fruit 를 상속받는 클래스는 eat() 메서드를 반드시 구현해야한다.
결제 시스템을 만든다고 가정해보자. 카드 결제, 현금 결제, 카카오페이는 내부 처리 방식이 서로 다르다. 하지만 외부에서 볼 때는 모두 "금액을 결제한다." 는 공통 기능을 제공한다.
이때 Payment 라는 추상 클래스를 만들고, 모든 결제 클래스가 pay()를 반드시 구현하도록 강제할 수 있다.
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount):
pass
class CardPayment(Payment):
def pay(self, amount):
print(f"카드로 {amount}원 결제한다.")
class CashPayment(Payment):
def pay(self, amount):
print(f"현금으로 {amount}원 결제한다.")
class KakaoPay(Payment):
def pay(self, amount):
print(f"카카오페이로 {amount}원 결제한다.")
def process_payment(payment: Payment, amount):
payment.pay(amount)
card = CardPayment()
cash = CashPayment()
kakao = KakaoPay()
process_payment(card, 10000)
process_payment(cash, 5000)
process_payment(kakao, 2000)
출력 결과 :
카드로 10000원을 결제한다.
현금으로 5000원 결제한다.
카카오페이로 2000원 결제한다.
process_payment() 함수는 결제 객채의 내부 구현을 알 필요가 없다. 단지 Payment계열 객체가 pay() 메서드를 제공한다는 규치만 알고 있으면 된다.
추상 클래스를 상속받았는데 추상 메서드를 구현하지 않으면 해당 클래스는 인스턴스를 만들 수 없다.
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount):
pass
class BankTransfer(Payment):
pass
# bank = BankTransfer()
# TypeError: Can't instantiate abstract class BankTransfer with abstract method pay
이처럼 추상화는 자식 클래스가 지켜야 할 공통 규칙을 명확히 만든다.
다형성과 추상화는 함께 쓰일 때 강력하다.
| 개념 | 결제 예제에서의 의미 |
|---|---|
| 추상화 | 모든 결제 수단은 pay()를 제공해야 한다는 규칙을 만든다. |
| 다형성 | process_payment()가 카드, 현금, 카카오페이를 같은 방식으로 처리한다. |
추상화가 공통 인터페이스를 설계하고, 다형성이 그 인터페이스를 통해 여러 객체를 유연하게 실행하게 만든다.
객체지향 프로그래밍의 네 가지 핵심 개념은 서로 연결되어 있다.
| 개념 | 역할 |
|---|---|
| 캡슐화 | 객체의 내부 상태를 보호하고 안전한 사용 방법을 제공한다. |
| 상속 | 공통 코드를 재사용하고 자식 클래스에서 확장한다. |
| 다형성 | 같은 인터페이스로 다양한 객체를 다룰 수 있게 한다. |
| 추상화 | 핵심 기능만 드러내고 세부 구현을 숨긴다. |
캡슐화는 객체를 안전하게 만들고, 상속은 코드를 재사용하게 해준다. 오버라이딩과 다형성은 같은 호출이 객체에 따라 다른 동작을 하게 만들며, 추상화는 클래스가 지켜야 할 공통 규칙을 정의한다. 이 개념들을 함께 이해하면 더 유연하고 유지보수하기 쉬운 코드를 작성할 수 있다.
본 정리는 강의 내용과 아래 자료를 참고하여 학습 목적으로 작성하였습니다.