python

11. 객체 지향 프로그래밍 4대 패러다임 ①

cinnamonbrown 2026. 5. 28. 23:58

객체지향 프로그래밍(OOP) 4대 패러다임은


캡슐화, 상속, 다형성, 추상화이다.

 

  • 캡슐화 : 데이터(속성)와 기능(메서드)을 하나로 묶고 외부에서 직접 접근을 제한하여 객체를 보호하는 개념
  • 상속 : 기존 클래스의 속성과 메서드를 물려받아 재사용하고 확장하는 구조
  • 다형성 : 같은 이름의 메서드라도 객체의 종류에 따라 다르게 동작할 수 있게 하는 특징으로, 코드의 유연성을 높여 줌
  • 추상화 : 복잡한 내부 구현은 숨기고 핵심적인 기능만 외부에 제공하는 것으로, 사용자는 세부 동작을 몰라도 객체를 쉽게 사용할 수 있게 해줌

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


1. 캡슐화

캡슐화(encapsulation)

객체지향 프로그래밍에서 데이터(속성)와 이를 처리하는 메서드를 하나로 묶고, 외부에서 내부 구현에 직접 접근하지 못하도록 제한하여 객체를 보호하는 개념이다.

 

파이썬에서는 public, _protected, __private와 같은 네이밍 규칙을 통해 접근 수준을 구분하며,

필요할 경우 getter/setter 메서드나 @property를 사용해 안전하게 값을 읽고 수정할 수 있다.

 

이를 통해 객체의 상태를 임의로 변경하는 것을 방지하고, 코드의 안정성과 유지보수성을 높일 수 있다.


코드 예시

강의에서는 price 값을 캡슐화하는 예시가 나왔다.

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}')

 

👉 여기서 핵심은

self.__price

이다.

 

 

앞에 __(언더바 2개)를 붙여 private으로 보호하고 있기 때문에,
외부에서 직접 값을 수정하는 것을 제한할 수 있다.

 

fruit.price = 4000

처럼 바로 접근해서 수정하는 방법이 통하지 않는다는 뜻이다.

 

 

이처럼 캡슐화를 사용하는 이유는 객체 내부 값이 아무 값으로나 바뀌는 걸 막기 위해서이다.

 

그래서 값을 읽을 때는 getter, 수정할 때는 setter를 사용한다.

fruit.get_price()
fruit.set_price(4000)

이런 식으로 접근할 수 있다.

 

 


🔸 @property 사용

그런데 위의 getter/setter 방식은 처리해야 할 변수가 많아질 수록 코드의 양이 그만큼 더 많아질 수 있다.

 

그래서 파이썬에서는 @property 데코레이터를 사용해 함수처럼 만들었지만 변수처럼 접근하는 방식도 많이 사용한다.

 

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

    # getter → 속성처럼 접근 가능
    @property
    def price(self):
        return self.__price

    # setter → 값 할당 시 자동 호출
    @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}")  # 함수가 아니라 속성처럼 사용

 

 

처음에는 @property가 뭔가 했는데,
getter/setter를 조금 더 편하게 사용할 수 있게 해주는 문법 정도로 이해하면 될 것 같다.


2. 상속

상속(inheritance)

객체지향 프로그래밍에서 기존 클래스(부모 클래스)의 속성과 메서드를 새로운 클래스(자식 클래스)가 물려받아 재사용하고 확장하는 개념이다.

 

이를 통해 공통 기능을 중복 작성하지 않고 코드 재사용성을 높일 수 있고

 

① 자식 클래스는 부모의 기능을 그대로 사용하거나

② 필요한 부분만 추가하거나 오버라이딩(재정의)하여 동작을 변경할 수 있습니다.

 

파이썬에서는

class 자식클래스(부모클래스)

 

형태로 상속을 구현하며,

 

super()를 사용해 부모 클래스의 초기화나 메서드를 호출할 수 있다.

이러한 구조는 코드의 유지보수성과 확장성을 크게 향상시키는 핵심 개념이다.

class Parent:
    pass

class Child(Parent):
    pass

2.1 상속 구조

상속의 구조에 대해 이해하기 위해 부모 클래스 역할을 하는 Fruit 클래스를 간단히 만들어보자.

 

Fruit 클래스는 과일의 이름(name)과 수량(quantity) 정보를 저장하고,
보관(store)과 판매(sell) 기능을 가진다.

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

 

이번에는 Fruit 클래스를 상속받는 Apple 클래스를 만들어보자.

class Apple(Fruit):
  pass

apple = Apple('사과', 50)

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

### 실행 결과

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

 

👉Apple 클래스 안에는 아무 코드가 없지만(pass)


👉Fruit 클래스를 상속받았기 때문에

 

store()sell() 메서드를 그대로 사용할 수 있다.

 

 

여기서 한 가지 유의해야 할 부분은 객체 생성 시 매개변수이다.

apple = Apple('사과', 50)

 

Apple 클래스 안에는 __init__()이 없어서, 처음 보면 아무 값도 안 넣어도 될 것 같다.

 

하지만,

AppleFruit를 상속받고 있기 때문에
부모 클래스의 생성자(__init__)가 자동으로 호출된다.

 

 

즉, Fruit 생성자에서 필요한 매개변수인

name, quantity

를 그대로 전달해야 한다.

 

 

그래서 아래처럼 만들면 에러가 발생한다.

apple = Apple()

부모 생성자에서 필요한 값이 전달되지 않았기 때문이다.


2.2 생성자 호출

상속 관계에서는 생성자(__init__)가 어떻게 호출되는 지 알아보자.


1️⃣ 자식 클래스에 __init__ 이 없을 때 : 부모 생성자 자동 호출

자식 클래스에서 생성자를 따로 정의하지 않으면, 부모 클래스의 생성자가 자동으로 호출된다.

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

class Apple(Fruit):
    pass

apple = Apple("사과")

# Fruit 생성자 호출

실행 결과

Fruit 생성자 호출

 

👉 Apple 클래스에는 __init__이 없기 때문에 객체를 생성하면 부모 클래스인 Fruit의 생성자가 자동으로 실행된다.

 


2️⃣ 자식 클래스에 __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) # AttributeError: 'Apple' object has no attribute 'name'```

 

실행 결과

Apple 생성자 호출



👉 이 경우에는 Apple 클래스의 생성자만 실행된다.

 

그래서, 생성자 안에서 초기화하던 코드(self.name = name)이 실행되지 않는다.

그 결과, apple.name을 출력하면 에러가 생기는 것이다.


3️⃣ super()함수 이용

그렇다면 자식 클래스에서 생성자를 만들면서도 부모 생성자를 함께 사용하고 싶을 때는 어떻게 할까?

 

 

이때 사용하는 것이 super()이다.

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

class Apple(Fruit):
    def __init__(self,name):
        super().__init__(name) # super() : 부모를 호출하는 함수 
print('Apple 생성자 호출')

apple=Apple('사과')

print(apple.name)```

### 실행 결과

```python
Fruit 생성자 호출
Apple 생성자 호출
사과

👉

super().__init__(name)

은 부모 클래스의 생성자를 호출하는 코드이다.


이 코드를 통해 Fruit에서 하던 초기화 작업(self.name = name)을 그대로 가져오면서
자식 클래스의 생성자도 함께 사용할 수 있다.

표로 정리

상황  부모 생성자 호출 여부 특징
자식에 __init__ ❌ O 부모 생성자 자동 호출
자식에 __init__ 직접 정의 X 부모 생성자 자동 호출 안 됨
super().__init__() 사용 O 부모 생성자와 자식 생성자 함께 사용 가능

2.3 오버라이딩

오버라이딩(overriding)은 상속 관계에서 부모 클래스에 이미 정의된 메서드를 자식 클래스에서 동일한 이름으로 다시 정의하여, 동작을 변경하는 것을 의미한다.

 

이를 통해 자식 클래스는 부모의 기본 기능을 그대로 사용할 수도 있고, 필요에 따라 일부 또는 전체 동작을 자신에게 맞게 수정할 수 있다.

 

파이썬에서는 별도의 키워드 없이 같은 메서드 이름을 정의하면 자동으로 오버라이딩이 이루어지며,

 

필요할 경우 super()를 사용해 부모의 메서드를 함께 호출할 수도 있다.


예시

아래 코드는 Fruit 클래스를 상속받은 Apple 클래스에서 eat() 메서드를 오버라이딩한 예시이다.

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}를 깨끗이 씻습니다')

    # Overriding
    def eat(self):
        print(f'{self.name}를 아주 맛있게 먹습니다')

    def superEat(self):
        super().eat() #  print(f'{self.name}를 먹습니다')


apple=Apple('사과',10) # apple 의 init이 없다 부모 생성자를 이용한다!

apple.eat() # 사과를 아주 맛있게 먹습니다.

apple.store('냉장고') # 사과를 냉장고에 보관합니다

apple.wash() # 사과를 깨끗이 씻습니다

print('---- 부모 메서드 호출 -----')

apple.superEat()

fruit=Fruit('과일',20)

fruit.eat() # 과일를 먹습니다

fruit.store('창고') # 과일를 창고에 보관합니다

실행 결과

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

----- 부모 메서드 호출 -----

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

👉 메서드별 동작 차이

메서드 정의된 위치 Apple 객체 호출 시 동작 특징
eat() Fruit, Apple 둘 다 Apple.eat() 실행 자식 클래스에서 오버라이딩
store() Fruit Fruit.store() 실행 오버라이딩하지 않아 부모 메서드 그대로 사용
wash() Apple Apple.wash() 실행 자식 클래스에서 새로 추가한 메서드
superEat() Apple Fruit.eat() 실행 super()를 사용해 부모 메서드 직접 호출

✅ 정리하자면,

  • eat() - 오버라이딩되어 자식클래스의 메서드가 실행됨
  • store()- 재정의하지 않았기 때문에 부모 클래스 메서드를 그대로 사용

+) super()를 사용하면, 오버라이딩했더라도 부모 메서드를 직접 호출할 수 있다. 


2.4 다중 상속

다중 상속은 클래스가 둘 이상의 부모 클래스로부터 상속을 받는 기능을 의미한다

파이썬은 다른 많은 객체 지향 언어와 달리 다중 상속을 지원한다.

class Parent1:
    pass

class Parent2:
    pass

class Child(Parent1, Parent2):
    pass

 

 

👉 Child 클래스는 Parent1Parent2를 동시에 상속받는다.
👉 다중 상속을 사용하면 여러 클래스의 기능을 조합해서 사용할 수 있다는 장점이 있다.

👉 하지만 부모 클래스들 안에 같은 이름의 메서드가 존재할 경우, 어떤 부모의 메서드를 먼저 사용할 것인지가 헷갈릴 수 있다.
이때 파이썬은 MRO(Method Resolution Order) 라는 규칙에 따라 메서드를 찾는다.

예시

아래 코드는 Fruit 클래스와 Storage 클래스를 만들고,

ProcessedFruit 클래스가 이 두 클래스를 동시에 상속받는 예시이다.

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 __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

 

👉 ProcessedFruitFruitStorage를 동시에 상속받는다.

 

그래서 Fruit에 있는 eat() 메서드도 사용할 수 있고, Storage에 있는 process() 메서드도 사용할 수 있다.

 

👉 다만 중요한 부분은 sleep() 메서드이다.

 

Fruit에도 sleep() 메서드가 있고, Storage에도 sleep() 메서드가 있다.

 

이렇게 부모 클래스들에 같은 이름의 메서드가 있을 때, 파이썬이 아무거나 실행하는 것이 아니라,

 

상속 순서에 따라 먼저 찾은 메서드를 실행한다.

class ProcessedFruit(Fruit, Storage):
pass

위 코드에서는 Fruit가 먼저 적혀 있기 때문에


ProcessedFruit에서 sleep()을 호출하면 Fruitsleep() 메서드가 먼저 실행된다.

 

즉, sleep() 메서드는 Storage에도 존재하지만

 

상속 순서상 Fruit가 먼저이기 때문에 Fruit.sleep()이 실행된 것이다.

실행결과


🔸 object 클래스

파이썬의 object 클래스모든 클래스의 최상위 부모 클래스(루트 클래스)이다.

 

사용자가 클래스를 정의할 때 명시적으로 상속하지 않아도 자동으로 상속되는 기본 클래스이다.

 

이 클래스는 객체가 가져야 할 최소한의 공통 기능을 제공하며,

__str__, __repr__, __eq__ 같은 기본 매직 메서드의 기본 구현을 포함하고 있다.

 

따라서 모든 파이썬 객체는 내부적으로 object를 기반으로 동작하며, 새로운 클래스를 정의할 때도 이 기본 기능들을 물려받아 사용할 수 있다.

 

즉, object 클래스는 파이썬 객체 시스템의 가장 기초가 되는 공통 부모이다.

 

'python' 카테고리의 다른 글

자료구조 - 스택(Stack)  (0) 2026.05.30
자료구조 - 배열(Array)  (0) 2026.05.29
10. 객체지향 프로그래밍  (0) 2026.05.28
9. 사용자 정의 함수 ②  (0) 2026.05.28
9. 사용자 정의 함수 ①  (0) 2026.05.27