Python

객체지향 프로그램 4대 패러다임

gudwns5533 2026. 5. 28. 23:39

객체지향 프로그래밍(OOP) 4대 패러다임은 캡슐화, 상속, 다형성, 추상화입니다.

캡슐화는 데이터(속성)와 기능(메서드)을 하나로 묶고 외부에서 직접 접근을 제한하여 객체를 보호하는 개념이다.

상속은 기존 클래스의 속성과 메서드를 물려받아 재사용하고 확장하는 구조를 의미한다.

다형성은 같은 이름의 메서드라도 객체의 종류에 따라 다르게 동작할 수 있게 하는 특징으로, 코드의 유연성을 높여준다.

추상화는 복잡한 내부 구현은 숨기고 핵심적인 기능만 외부에 제공하는 것으로, 사용자는 세부 동작을 몰라도 객체를 쉽게 사용할 수 있게 해준다.

이 네 가지 개념을 통해 코드의 재사용성, 확장성, 유지보수성을 크게 향상시킬 수 있다.


캡슐화

 

캡슐화(Encapsulation)는 객체지향 프로그래밍에서 데이터(속성)와 이를 처리하는 메서드를 하나로 묶고, 외부에서 내부 구현에 직접 접근하지 못하도록 제한하여 객체를 보호하는 개념이다. 파이썬에서는 public, _protected, __private와 같은 네이밍 규칙을 통해 접근 수준을 구분하며, 필요할 경우 getter / setter 메서드나 @property를 사용해 안전하게 값을 읽고 수정할 수 있다. 이를 통해 객체의 상태를 임의로 변경하는 것을 방지하고, 코드의 안정성과 유지보수성을 높일 수 있다.

class Fruit:
    def __init__(self, name, price):
        self.name = name          # public
        self.__price = price      # private (캡슐화)

    # getter
    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):                           # 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                         # 새로운 변수 생성 (실제 private 변경 아님)

print("getter로 확인:", apple.get_price())    # 여전히 3500
print("직접 접근:", apple.__price)            # 10000 (다른 변수)

# 과일: 사과
# 가격: 3000
# 사과의 원산지: 한국
# 가격 조회: 3000
# ----- 가격 수정 -----
# 수정된 가격: 3500
# ----- 잘못된 값 입력 -----
# 가격은 0보다 커야 합니다.
# ----- 외부 접근 시도 -----
# 실제 가격: 3500
# 외부에서 만든 값: 10000

 

👉 self.__price는 실제로 private이 되는 것이 아니라 내부적으로 _Fruit__price로 바뀐다.

class Fruit:
    def __init__(self, name, price):
        self.name = name
        self.__price = price  # private

    # getter → 속성처럼 접근 가능 (변수처럼 사용 가능하게 해줌)
    @property
    def price(self):
        return self.__price

    # setter → 값 할당 시 자동 호출 (getter와 이름이 일치해야함) 
    @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)  # getter 호출

print("----- 가격 수정 -----")
apple.price = 3500               # setter 호출
print("수정된 가격:", apple.price)

print("----- 잘못된 값 입력 -----")
apple.price = -1000              # 검증 실패

print("----- 외부 접근 시도 -----")
apple.__price = 10000            # 새로운 변수 생성

print("실제 가격:", apple.price)  # 여전히 3500
print("외부에서 만든 값:", apple.__price)

# 과일: 사과
# 가격: 3000
# 사과의 원산지: 한국
# 가격 조회: 3000
# ----- 가격 수정 -----
# 수정된 가격: 3500
# ----- 잘못된 값 입력 -----
# 가격은 0보다 커야 합니다.
# ----- 외부 접근 시도 -----
# 실제 가격: 3500
# 외부에서 만든 값: 10000

 

👉 @property는 getter/setter를 함수가 아닌 “속성처럼” 사용하게 해주는 Pythonic한 캡슐화 방식입니다.


상속

 

상속(inheritance)은 객체지향 프로그래밍에서 기존 클래스(부모 클래스)의 속성과 메서드를 새로운 클래스(자식 클래스)가 물려받아 재사용하고 확장하는 개념입니다. 이를 통해 공통 기능을 중복 작성하지 않고 코드 재사용성을 높일 수 있으며, 자식 클래스는 부모의 기능을 그대로 사용하거나 필요한 부분만 추가하거나 오버라이딩(재정의)하여 동작을 변경할 수 있습니다. 파이썬에서는 class 자식클래스(부모클래스) 형태로 상속을 구현하며, super()를 사용해 부모 클래스의 초기화나 메서드를 호출할 수 있습니다. 이러한 구조는 코드의 유지보수성과 확장성을 크게 향상시키는 핵심 개념입니다.

class Parent:
    pass

class Child(Parent):
    pass

 

1. 상속 구조

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}개 판매합니다')

fruit = Fruit('과일', 100)
fruit.store('창고')
fruit.sell(10)

# 과일를 창고에 보관합니다
# 과일를 10개 판매합니다

class Apple(Fruit):
    pass

# Fruit 클래스를 상속 받았기 때문에
# Fruit 생성자의 매개변수를 전달해야 함
apple = Apple('사과', 50)

apple.store('냉장고')
apple.sell(5)

# 사과를 냉장고에 보관합니다
# 사과를 5개 판매합니다

 

2. 생성자 호출

class Fruit:
    def __init__(self, name):
        print("Fruit 생성자 호출")
        self.name = name

class Apple(Fruit):
    pass

apple = Apple("사과")

# Fruit 생성자 호출

# 👉 자식 클래스에 __init__이 없을 때 자동 호출


class Fruit:
    def __init__(self, name):
        print("Fruit 생성자 호출")
        self.name = name

class Apple(Fruit):
    def __init__(self, name):
        print("Apple 생성자 호출")
        self.name = name

apple = Apple("사과")
print(apple.name)

# Apple 생성자 호출
# 사과

class Fruit:
    def __init__(self, name):
        print("Fruit 생성자 호출")
        self.name = name

class Apple(Fruit):
    def __init__(self):
        print("Apple 생성자 호출")

apple = Apple()
# print(apple.name)    # AttributeError: 'Apple' object has no attribute 'name'

# Apple 생성자 호출

# 👉 자식 클래스에 __init__을 직접 정의하면 부모 생성자 호출 안됨


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 생성자 호출
# 사과

# 👉  super() 함수는 현재 클래스의 부모 클래스를 참조하며, 부모 클래스의 생성자를 호출할 수 있다.

 

 

3. 오버라이딩

 

오버라이딩(overriding)은 상속 관계에서 부모 클래스에 이미 정의된 메서드를 자식 클래스에서 동일한 이름으로 다시 정의하여, 동작을 변경하는 것을 의미한다. 이를 통해 자식 클래스는 부모의 기능을 그대로 사용할 수도 있고, 필요에 따라 일부 또는 전체 동작을 자신에게 맞게 수정할 수 있다. 파이썬에서는 별도의 키워드 없이 같은 메서드 이름을 정의하면 자동으로 오버라이딩이 이루어지며, 필요할 경우 super()를 사용해 부모의 메서드를 함께 호출할 수도 있다.

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 superEat(self):
        super().eat()
        
apple = Apple('사과', 10)
apple.eat()                # 오버라이딩된 메서드
apple.store('냉장고')       # 부모 메서드
apple.wash()               # 자식 메서드
apple.superEat()           # 부모 eat 호출

# 사과를 아주 맛있게 먹습니다
# 사과를 냉장고에 보관합니다
# 사과를 깨끗이 씻습니다
# 사과를 먹습니다

fruit = Fruit('과일', 10)
fruit.eat()
fruit.store('창고')
# fruit.wash()  # AttributeError: 'Fruit' object has no attribute 'wash'
# 부모는 자식클래스에 접근할 수 없다.

# 과일를 먹습니다
# 과일를 창고에 보관합니다

 

4. 다중 상속

 

다중 상속은 클래스가 둘 이상의 부모 클래스로부터 상속을 받는 기능을 의미한다. 파이썬은 다른 많은 객체 지향 언어와 달리 다중 상속을 지원합니다. 다중 상속을 사용하면 코드의 재사용성을 향상시킬 수 있지만, 동시에 복잡성이 높아지기 때문에 주의해야 한다.

class Parent1:
    pass

class Parent2:
    pass

class Child(Parent1, Parent2):
    pass
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}시간 동안 보관합니다')

    def supersleep(self, hour):
        super().sleep(hour)    


class Storage:
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    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 class
fruit.process(2)  # Storage class
fruit.sleep(8)    # MRO에 따라 Fruit class의 sleep 실행
fruit.supersleep(5)

# 사과를 먹습니다
# 사과를 2시간 동안 가공합니다
# 사과를 8시간 동안 보관합니다
# 사과를 5시간 동안 저온 숙성합니다

 

object 클래스

 

파이썬의 object 클래스는 모든 클래스의 최상위 부모 클래스(루트 클래스)로, 사용자가 클래스를 정의할 때 명시적으로 상속하지 않아도 자동으로 상속되는 기본 클래스이다. 이 클래스는 객체가 가져야 할 최소한의 공통 기능을 제공하며, __str__, __repr__, __eq__ 같은 기본 매직 메서드의 기본 구현을 포함하고 있다. 따라서 모든 파이썬 객체는 내부적으로 object를 기반으로 동작하며, 새로운 클래스를 정의할 때도 이 기본 기능들을 물렵잗아 사용할 수 있다. 즉, object 클래스는 파이썬 객체 시스템의 가장 기초가 되는 공통 부모이다.

 

MRO

 

MRO(Method Resolution Order)는 파이썬에서 다중 상속을 사용할 때, 메서드나 속성을 찾는 순서를 정의하는 규칙이다. MRO는 특히 여러 부모 클래스를 상속받는 경우에 어떤 부모 클래스에서 메서드를 먼저 찾을지를 결정하며, 이를 통해 클래스 간의 메서드 충동을 해결할 수 있다.

 

class Base:
    def hello(self):
        print("Base")
        
class Clean(Base):
    def hello(self):
        print("Clean")
        super().hello()
        print("Clean 끝")

class Pack(Base):
    def hello(self):
        print("Pack")
        super().hello()
        print("Pack 끝")

class Product(Clean, Pack):
    def hello(self):
        print("Product")
        super().hello()
        print("Product 끝")
        
p = Product()
p.hello()
print(Product.mro())


# Product
# Clean
# Pack
# Base
# Pack 끝
# Clean 끝
# Product 끝
# [<class '__main__.Product'>, <class '__main__.Clean'>, <class '__main__.Pack'>, <class '__main__.Base'>, <class 'object'>]

# MRO에 따라 Product > Clean > Pack > Base 순으로 정의된다

다형성

 

다형성(polymorphism)은 같은 이름의 메서드나 함수를 호출하더라도, 객체의 종류에 따라 서로 다른 방식으로 동작하는 특성을 의미한다. 파이썬은 정적 타입 언어처럼 명시적인 인터페이스 구현을 요구하지 않고, 동일한 메서드를 가지고 있으면 타입과 관계없이 사용할 수 있는 덕 타이핑(duck typing)을 기반으로 다형성을 자연스럽게 지원한다. 예를 들어 여러 객체가 pay()나 speak() 같은 동일한 메서드를 가지고 있으면, 하나의 함수에서 객체의 타입을 구분하지 않고도 공통된 방식으로 처리할 수 있다.이러한 특징은 코드의 유연성과 확장성을 높여주며, 새로운 기능을 추가할 때 기존 코드를 수정하지 않아도 되는 장점을 제공한다.

👉 덕 타이핑(duck typing) : 객체의 실제 클래스가 무엇인지 중요하지 않고, 필요한 메서드나 기능을 가지고 있으면 같은 방식으로 사용하는 것

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)    # 어떤 결제 수단이든 그냥 .pay()를 호출
    
card = CardPayment()
cash = CashPayment()
kakao = KakaoPay()

process_payment(card, 10000)
process_payment(cash, 5000)
process_payment(kakao, 2000)

# 카드로 10000원 결제합니다
# 현금으로 5000원 결제합니다
# 카카오페이로 2000원 결제합니다

 

👉 payment라는 매개변수로 어떤 객체가 들어올지 미리 따지지 않고, 카드/현금/카카오페이 상관없이 .pay(amount)라는 동일한 명령(메서드)을 내린다. 호출하는 방식은 똑같이만 어떤 객체가 들어가느냐에 따라 실제 실행되는 결과가 달라지는 것이 바로 다형성이다.

 

💡 추후에 새로운 기능이 추가되어도 결제를 처리하는 process_payment함수는 고칠 필요가 없다. 새로운 클래스를 만들고 그 안에 .pay() 메서드만 정의해주면 되기 때문에 확장성면에서 큰 장점이 있다.


추상화

 

추상화(abstraction)는 복잡한 내부 구현은 숨기고, 외부에서는 필요한 핵심 기능만 간단하게 사용할 수 있도록 만든 개념이다. 사용자는 객체가 내부적으로 어떻게 동작하는지 몰라도, 제공된 메서드만으로 기능을 사용할 수 있으며, 이를 통해 코드의 복잡도를 줄이고 이해와 사용을 쉽게 한다. 파이썬에서는 일반 클래스 설계뿐 아니라 abc 모듈의 ABC와 @abstractmethod를 사용해 추상 클래스를 정의하고, 반드시 구현해야 하는 메서드를 강제함으로써 일관된 구조를 유지할 수 있다. 즉, 추상화는 "어떻게 동작하는 지" 보다 "무엇을 할 수 있는지"에 집중하게 만드는 설계 방식이다.

from abc import ABC, abstractmethod  # abc모듈에서 import

# 추상 클래스
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원 결제합니다

 

👉 추후에 결제수단을 추가하기 위해서는 @abstractmethod로 강제되어있는 메서드를 무조건 오버라이딩 해야한다. 그렇지않으면 에러가 나서 프로그램이 실행되지 않도록 막는다.

 

💡 결제방식이 어떻게 동작하는지를 몰라도 주어진 메서드를 통해 오버라이딩을 하면 결제가 진행되겠구나를 알 수 있다.

'Python' 카테고리의 다른 글

매직 메서드  (0) 2026.05.29
예외처리  (0) 2026.05.29
객체지향 프로그래밍  (0) 2026.05.28
사용자 정의 함수 part2  (0) 2026.05.28
사용자 정의 함수 part1  (0) 2026.05.28