[7일차-3] Python

송정근·2026년 5월 28일

객체지향 프로그래밍 4대 개념 정리

객체지향 프로그래밍(OOP, Object-Oriented Programming)은 프로그램을 객체 단위로 구성하는 방식이다. 객체는 데이터인 속성(attribute)과 동작인 메서드(method)를 함께 가진다.

객체지향 프로그래밍의 핵심 개념은 보통 다음 네 가지로 정리한다.

개념핵심 의미
캡슐화데이터와 메서드를 하나로 묶고, 외부의 직접 접근을 제한한다.
상속기존 클래스의 속성과 메서드를 물려받아 재사용하고 확장한다.
다형성같은 이름의 메서드가 객체 종류에 따라 다르게 동작한다.
추상화복잡한 내부 구현은 숨기고 핵심 기능만 외부에 제공한다.

이 네 가지 개념을 활용하면 코드의 재사용성, 확장성, 유지보수성을 높일 수 있다.


1. 캡슐화

캡슐화(encapsulation)는 객체의 데이터와 기능을 하나로 묶고, 외부에서 내부 구현에 직접 접근하지 못하도록 제한하는 개념이다.

파이썬은 Java나 C++처럼 접근 제어자를 강제하지 않는다. 대신 이름 규칙으로 접근 수준을 표현한다.

형태이름 예시의미
publicname외부에서 자유롭게 접근 가능
protected 관례_name클래스 내부 또는 자식 클래스에서 사용하자는 관례
private 네임 맹글링__name외부 직접 접근을 어렵게 만들기 위해 이름을 변경

1.1 getter와 setter로 값 보호하기

가격처럼 잘못된 값이 들어오면 안 되는 데이터는 직접 수정하게 두기보다 메서드를 통해 검증하는 것이 좋다.

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는 외부 접근을 어렵게 만드는 장치이지 보안을 위한 완전한 차단 장치는 아니다.

1.2 @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를 사용하면 메서드로 검증 로직을 유지하면서도 외부에서는 속성처럼 읽고 쓸 수 있다.


2. 상속

상속(inheritance)은 기존 클래스의 속성과 메서드를 새로운 클래스가 물려받는 구조이다. 기존 클래스를 부모 클래스, 상속받는 클래스를 자식 클래스라고 부른다.

class Parent:
    pass


class Child(Parent):
    pass

2.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}개 판매한다.")


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()을 사용할 수 있다.

2.2 자식 클래스에 생성자가 없을 때

자식 클래스에 __init__()이 없으면 부모 클래스의 생성자가 자동으로 호출된다.

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


class Apple(Fruit):
    pass


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

출력 결과:

Fruit 생성자 호출
사과

2.3 자식 클래스에 생성자를 직접 정의할 때

자식 클래스에 __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 생성자 호출
사과

3. 오버라이딩

오버라이딩(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()을 호출하면 자식 클래스의 메서드가 실행된다.


4. 다중 상속과 MRO

다중 상속은 하나의 클래스가 둘 이상의 부모 클래스를 상속받는 기능이다.

class Parent1:
    pass


class Parent2:
    pass


class Child(Parent1, Parent2):
    pass

출력 결과:

출력 결과 없음

파이썬은 다중 상속을 지원하지만, 부모 클래스에 같은 이름의 메서드가 있을 때 어떤 메서드를 먼저 찾을지 정해야 한다. 이 순서를 MRO(Method Resolution Order)라고 한다.

4.1 다중 상속 예제

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()이 실행된다.

4.2 object 클래스

파이썬의 모든 클래스는 명시적으로 상속하지 않아도 최상위 부모인 object 클래스를 상속한다.

class Fruit:
    pass


print(Fruit.mro())
print(isinstance(Fruit(), object))

출력 결과:

[<class '__main__.Fruit'>, <class 'object'>]
True

object는 모든 객체가 가져야 할 최소한의 공통 기능을 제공한다. __str__, __repr__, __eq__ 같은 기본 매직 메서드도 object로부터 시작된다.

4.3 MRO와 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()가 실행된다.

출처 및 참고 자료
본 정리는 강의 내용과 아래 자료를 참고하여 학습 목적으로 작성하였습니다.

https://ryuzyproject.tistory.com/289

profile
기록하며 성장하는 개발자

0개의 댓글