Python & AI Tutorials Logo
Python 프로그래밍

32. 상속과 다형성으로 클래스 확장하기

30장에서는 현실 세계의 개념을 모델링하기 위해 우리만의 클래스를 만드는 방법을 배웠습니다. 데이터와 동작을 함께 묶는 BankAccount, Student 같은 클래스를 만들었습니다. 그런데 기존 클래스와 비슷하지만, 몇 가지 차이점이나 추가 기능이 있는 새 클래스를 만들어야 한다면 어떻게 해야 할까요?

상속(inheritance)은 기존 클래스를 바탕으로 새로운 클래스를 만드는 Python의 메커니즘입니다. 코드를 복사해서 붙여넣는 대신, 부모 클래스(parent class)(또는 기본 클래스(base class), 슈퍼클래스(superclass)라고도 함)의 모든 속성과 메서드를 자동으로 물려받는 서브클래스(subclass)를 만든 뒤, 필요한 것을 추가하거나 수정할 수 있습니다.

이 장에서는 상속이 관련된 클래스들의 계층 구조를 만드는 데 어떻게 도움이 되는지, 상속받은 동작을 어떻게 커스터마이즈하는지, 그리고 다형성(polymorphism)이 공통 인터페이스를 공유하는 서로 다른 클래스를 어떻게 서로 바꿔서 사용할 수 있게 해주는지를 살펴봅니다.

32.1) 기존 클래스에서 서브클래스 만들기

32.1.1) 상속의 기본 문법

상속을 사용하면 기존 클래스(부모 클래스 또는 베이스 클래스)를 기반으로 새로운 클래스(자식 클래스 또는 서브클래스)를 만들 수 있습니다. 서브클래스는 부모 클래스의 모든 메서드와 속성을 자동으로 상속받습니다.

서브클래스를 만들 때는 클래스 이름 뒤 괄호 안에 부모 클래스를 지정합니다.

python
# 부모 클래스
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
# Animal에서 상속받는 서브클래스
class Dog(Animal):
    pass  # 아직은 추가 코드가 필요 없음
 
# Dog 인스턴스 생성
buddy = Dog("Buddy")
print(buddy.speak())  # Output: Buddy makes a sound
print(buddy.name)     # Output: Buddy

Dog에 자체 코드가 전혀 없더라도(단지 pass만 있어도) Animal의 모든 것을 상속받습니다. Dog 클래스는 부모로부터 __init__ 메서드와 speak 메서드를 자동으로 갖게 됩니다.

Animal

+name

+init(name)

+speak()

Dog

32.1.2) 상속이 중요한 이유

상속은 흔한 프로그래밍 문제인 코드 중복을 해결합니다. 예를 들어 서로 다른 유형의 직원을 관리하는 시스템을 만든다고 해봅시다:

python
# 상속 없이 - 중복이 매우 많음
class FullTimeEmployee:
    def __init__(self, name, employee_id, salary):
        self.name = name
        self.employee_id = employee_id
        self.salary = salary
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"
 
class PartTimeEmployee:
    def __init__(self, name, employee_id, hourly_rate):
        self.name = name
        self.employee_id = employee_id
        self.hourly_rate = hourly_rate
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"

name, employee_id, get_info()가 중복되어 있는 것을 알 수 있습니다. 상속을 사용하면 이 중복을 제거할 수 있습니다:

python
# 상속 사용 - 공통 코드는 부모 클래스에 둠
class Employee:
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"
 
class FullTimeEmployee(Employee):
    def __init__(self, name, employee_id, salary):
        Employee.__init__(self, name, employee_id)  # 부모의 __init__ 호출
        # 참고: 32.3절에서 super()를 사용하는 더 나은 방법을 배웁니다
        self.salary = salary
 
class PartTimeEmployee(Employee):
    def __init__(self, name, employee_id, hourly_rate):
        Employee.__init__(self, name, employee_id)  # 부모의 __init__ 호출
        self.hourly_rate = hourly_rate
 
# 두 서브클래스 모두 get_info()를 상속받음
alice = FullTimeEmployee("Alice", "E001", 75000)
bob = PartTimeEmployee("Bob", "E002", 25)
 
print(alice.get_info())  # Output: Alice (ID: E001)
print(bob.get_info())    # Output: Bob (ID: E002)

이제 공통 속성과 메서드는 Employee에 있고, 각 서브클래스는 자신만의 고유한 부분만 정의합니다.

32.1.3) 서브클래스에 새로운 메서드 추가하기

서브클래스는 부모에 없는 자신의 메서드를 추가할 수 있습니다:

python
class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
    
    def get_description(self):
        return f"{self.brand} {self.model}"
 
class Car(Vehicle):
    def __init__(self, brand, model, num_doors):
        Vehicle.__init__(self, brand, model)
        self.num_doors = num_doors
    
    def honk(self):  # Car에만 있는 새 메서드
        return "Beep beep!"
 
class Motorcycle(Vehicle):
    def __init__(self, brand, model, has_sidecar):
        Vehicle.__init__(self, brand, model)
        self.has_sidecar = has_sidecar
    
    def rev_engine(self):  # Motorcycle에만 있는 새 메서드
        return "Vroom vroom!"
 
# 각 서브클래스는 부모의 메서드와 자신의 메서드를 모두 가짐
my_car = Car("Toyota", "Camry", 4)
print(my_car.get_description())  # Output: Toyota Camry
print(my_car.honk())             # Output: Beep beep!
 
my_bike = Motorcycle("Harley", "Sportster", False)
print(my_bike.get_description())  # Output: Harley Sportster
print(my_bike.rev_engine())       # Output: Vroom vroom!

Car 클래스는 get_description()(상속)과 honk()(자체)를 모두 갖습니다. Motorcycle 클래스는 get_description()(상속)과 rev_engine()(자체)를 갖습니다.

32.1.4) 서브클래스에 새로운 속성 추가하기

서브클래스는 자신만의 인스턴스 속성도 추가할 수 있습니다. 보통 서브클래스의 __init__ 메서드에서 이를 수행합니다:

python
class BankAccount:
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        return self.balance
 
class SavingsAccount(BankAccount):
    def __init__(self, account_number, balance, interest_rate):
        BankAccount.__init__(self, account_number, balance)
        self.interest_rate = interest_rate  # 새 속성
    
    def apply_interest(self):  # 새 속성을 사용하는 새 메서드
        interest = self.balance * self.interest_rate
        self.balance += interest
        return interest
 
# SavingsAccount는 BankAccount의 모든 속성에 interest_rate까지 가짐
savings = SavingsAccount("SA001", 1000, 0.03)
savings.deposit(500)  # 상속된 메서드
print(f"Balance: ${savings.balance}")  # Output: Balance: $1500
 
interest_earned = savings.apply_interest()
print(f"Interest earned: ${interest_earned:.2f}")  # Output: Interest earned: $45.00
print(f"New balance: ${savings.balance:.2f}")      # Output: New balance: $1545.00

SavingsAccountBankAccount로부터 account_numberbalance를 받고, 여기에 자신만의 interest_rate 속성을 추가합니다.

32.1.5) 여러 단계의 상속

클래스는 다른 클래스를 상속받는 클래스에서 다시 상속받을 수 있으며, 이를 통해 상속 계층 구조를 만들 수 있습니다:

python
class LivingThing:
    def __init__(self, name):
        self.name = name
    
    def is_alive(self):
        return True
 
class Animal(LivingThing):
    def __init__(self, name, species):
        LivingThing.__init__(self, name)
        self.species = species
    
    def move(self):
        return f"{self.name} is moving"
 
class Dog(Animal):
    def __init__(self, name, breed):
        Animal.__init__(self, name, "Dog")
        self.breed = breed
    
    def bark(self):
        return f"{self.name} says: Woof!"
 
# Dog는 Animal을 상속받고, Animal은 LivingThing을 상속받음
max_dog = Dog("Max", "Golden Retriever")
 
# 세 단계의 메서드가 모두 동작함
print(max_dog.is_alive())  # Output: True (from LivingThing)
print(max_dog.move())      # Output: Max is moving (from Animal)
print(max_dog.bark())      # Output: Max says: Woof! (from Dog)
 
# 세 단계의 속성이 모두 존재함
print(max_dog.name)     # Output: Max (from LivingThing)
print(max_dog.species)  # Output: Dog (from Animal)
print(max_dog.breed)    # Output: Golden Retriever (from Dog)

LivingThing

+name

+is_alive()

Animal

+species

+move()

Dog

+breed

+bark()

DogAnimal을 상속받고, AnimalLivingThing을 상속받습니다. 즉 Dog는 두 부모 클래스의 메서드와 속성에 모두 접근할 수 있습니다.

32.2) 서브클래스에서 메서드 오버라이딩하기

32.2.1) 메서드 오버라이딩의 의미

때로는 서브클래스가 상속받은 메서드의 동작 방식을 변경해야 합니다. 메서드 오버라이딩(method overriding)이란 부모 클래스의 메서드와 같은 이름의 메서드를 서브클래스에 정의하는 것을 의미합니다. 서브클래스 인스턴스에서 그 메서드를 호출하면, Python은 부모 버전 대신 서브클래스 버전을 사용합니다.

python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
class Dog(Animal):
    def speak(self):  # 부모의 speak 메서드를 오버라이드
        return f"{self.name} says: Woof!"
 
class Cat(Animal):
    def speak(self):  # 다른 동작으로 오버라이드
        return f"{self.name} says: Meow!"
 
# 각 클래스는 자신만의 speak() 버전을 가짐
generic_animal = Animal("Generic")
print(generic_animal.speak())  # Output: Generic makes a sound
 
buddy = Dog("Buddy")
print(buddy.speak())  # Output: Buddy says: Woof!
 
whiskers = Cat("Whiskers")
print(whiskers.speak())  # Output: Whiskers says: Meow!

buddy.speak()를 호출하면, Python은 buddyDog의 인스턴스이므로 먼저 Dog 클래스에서 speak 메서드를 찾습니다. Dog가 자체적으로 speak 메서드를 정의하고 있으므로, Python은 그 버전을 사용합니다. 만약 Dogspeak 메서드가 없다면, Python은 부모 클래스인 Animal에서 찾아서 그 버전을 사용합니다.

이러한 탐색 순서—인스턴스의 클래스에서 시작해서 부모 클래스로 이동—가 메서드 오버라이딩이 작동하는 방식이며, 서브클래스가 상속받은 동작을 커스터마이징하는 방법입니다.

32.2.2) 왜 메서드를 오버라이드할까?

메서드 오버라이딩을 사용하면 일반적인 동작을 특화된 버전으로 만들 수 있습니다. 도형 계층 구조를 생각해 봅시다:

python
class Shape:
    def __init__(self, name):
        self.name = name
    
    def area(self):
        return 0  # 기본 구현
    
    def describe(self):
        return f"{self.name} with area {self.area()}"
 
class Rectangle(Shape):
    def __init__(self, width, height):
        Shape.__init__(self, "Rectangle")
        self.width = width
        self.height = height
    
    def area(self):  # 직사각형 전용 계산으로 오버라이드
        return self.width * self.height
 
class Circle(Shape):
    def __init__(self, radius):
        Shape.__init__(self, "Circle")
        self.radius = radius
    
    def area(self):  # 원 전용 계산으로 오버라이드
        return 3.14159 * self.radius ** 2
 
# 각 도형은 area를 다르게 계산함
rect = Rectangle(5, 3)
print(rect.describe())  # Output: Rectangle with area 15
 
circle = Circle(4)
print(circle.describe())  # Output: Circle with area 50.26544

describe() 메서드는 두 서브클래스 모두에게 상속되며, 각 서브클래스가 자체 area() 구현을 제공하기 때문에 올바르게 작동합니다.

rect.describe()를 호출하면 상속받은 describe() 메서드가 실행되지만, selfRectangle 인스턴스를 가리킵니다. 따라서 describe()self.area()를 호출하면, Python은 먼저 Rectangle 클래스에서 area()를 찾고 오버라이드된 버전을 실행합니다.

32.2.3) __init__ 오버라이딩과 부모 초기화 호출하기

__init__를 오버라이드할 때는 보통 부모의 __init__를 호출하여 부모의 초기화가 수행되도록 해야 합니다:

python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def introduce(self):
        return f"I'm {self.name}, {self.age} years old"
 
class Student(Person):
    def __init__(self, name, age, student_id, major):
        # name과 age를 설정하기 위해 부모의 __init__ 호출
        Person.__init__(self, name, age)
        # 그 다음 학생 전용 속성 설정
        self.student_id = student_id
        self.major = major
    
alice = Student("Alice", 20, "S12345", "Computer Science")
print(alice.name)        # Output: Alice
print(alice.student_id)  # Output: S12345

Student.__init__이 먼저 Person.__init__(self, name, age)를 호출하여 부모 클래스의 속성들을 초기화한 후, 자신만의 속성을 추가하는 방식을 주목하세요.

32.3) super()로 부모 동작에 접근하기

32.3.1) super()가 하는 일

앞선 섹션에서는 부모 메서드를 ParentClass.method(self, ...) 형태로 명시적으로 호출했습니다. Python은 더 깔끔한 방법으로 super() 함수를 제공합니다. super()는 부모 클래스를 명시적으로 이름 붙이지 않고도 부모 클래스의 메서드를 호출할 수 있게 해주는 임시 객체를 반환합니다.

python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # Animal.__init__(self, name)보다 깔끔함
        self.breed = breed
    
    def speak(self):
        parent_sound = super().speak()  # 부모의 speak() 호출
        return f"{parent_sound} - specifically, Woof!"
 
buddy = Dog("Buddy", "Labrador")
print(buddy.speak())
# Output: Buddy makes a sound - specifically, Woof!

super()를 사용하면 여러 가지 장점이 있습니다:

  • 부모 클래스를 명시적으로 이름 붙일 필요가 없습니다
  • 다중 상속에서 올바르게 동작합니다(나중에 다룹니다)
  • 부모 클래스를 변경하더라도 코드를 유지보수하기가 더 쉽습니다

32.3.2) __init__에서 super() 사용하기

super()의 가장 흔한 사용은 부모의 __init__를 호출하는 것입니다:

python
class Employee:
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
        self.is_active = True
    
    def deactivate(self):
        self.is_active = False
 
class Manager(Employee):
    def __init__(self, name, employee_id, department):
        super().__init__(name, employee_id)  # 부모 속성 초기화
        self.department = department
        self.team = []  # Manager 전용 속성
    
    def add_team_member(self, employee):
        self.team.append(employee)
 
# Manager는 Employee의 모든 속성과 자신의 속성을 함께 가짐
sarah = Manager("Sarah", "M001", "Engineering")
print(sarah.name)        # Output: Sarah
print(sarah.is_active)   # Output: True
print(sarah.department)  # Output: Engineering

super().__init__(name, employee_id)를 호출함으로써 Manager 클래스는 Employee의 초기화 로직 전체가 실행되도록 보장하며, 여기에는 is_activeTrue로 설정하는 작업도 포함됩니다.

32.3.3) super()로 부모 메서드 확장하기

super()를 사용하면 부모의 메서드를 완전히 대체하는 대신 확장할 수 있습니다:

python
class BankAccount:
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.balance = balance
        self.transaction_count = 0
    
    def deposit(self, amount):
        self.balance += amount
        self.transaction_count += 1
        return self.balance
 
class CheckingAccount(BankAccount):
    def __init__(self, account_number, balance, overdraft_limit):
        super().__init__(account_number, balance)
        self.overdraft_limit = overdraft_limit
    
    def deposit(self, amount):
        was_overdrawn = self.balance < 0
        
        # 기본 로직 처리를 위해 부모의 deposit 호출
        new_balance = super().deposit(amount)
        
        # 당좌 계좌 전용 동작 추가
        if was_overdrawn and new_balance >= 0:
            print("Account is no longer overdrawn")
        return new_balance
 
checking = CheckingAccount("C001", -50, 100)
checking.deposit(75)
# Output: Account is no longer overdrawn
print(f"Balance: ${checking.balance}")  # Output: Balance: $25
print(f"Transactions: {checking.transaction_count}")  # Output: Transactions: 1

CheckingAccount.deposit()super().deposit(amount)를 호출해 기본 입금 로직(잔액과 거래 횟수 업데이트)을 처리한 뒤, 마이너스 상태(오버드래프트) 여부에 대한 자체 체크를 추가합니다.

32.3.4) super()를 쓸 때 vs 부모를 직접 호출할 때

대부분의 경우 super()를 사용하세요:

python
class Vehicle:
    def __init__(self, brand):
        self.brand = brand
 
class Car(Vehicle):
    def __init__(self, brand, model):
        super().__init__(brand)  # 권장
        self.model = model

다중 상속 시나리오에서 특정 부모를 호출해야 하는 경우(나중에 다룹니다) 또는 어떤 부모를 호출하는지 명확히 하고 싶을 때는 부모를 직접 호출할 수 있습니다:

python
class Car(Vehicle):
    def __init__(self, brand, model):
        Vehicle.__init__(self, brand)  # 명시적이지만 유연성이 떨어짐
        self.model = model

단일 상속(부모 1개)에서는 super()가 거의 항상 더 나은 선택입니다.

32.3.5) 다른 메서드에서의 super()

super()__init__뿐 아니라 어떤 메서드에도 사용할 수 있습니다:

python
class TextProcessor:
    def process(self, text):
        # 기본 처리: 공백 제거
        return text.strip()
 
class UppercaseProcessor(TextProcessor):
    def process(self, text):
        # 먼저 부모의 처리 수행
        processed = super().process(text)
        # 그 다음 대문자 변환 추가
        return processed.upper()
 
class PrefixProcessor(UppercaseProcessor):
    def __init__(self, prefix):
        self.prefix = prefix
    
    def process(self, text):
        # 먼저 부모의 처리 수행(부모도 자신의 부모를 호출함)
        processed = super().process(text)
        # 그 다음 접두사 추가
        return f"{self.prefix}: {processed}"
 
processor = PrefixProcessor("ALERT")
result = processor.process("  system error  ")
print(result)  # Output: ALERT: SYSTEM ERROR

32.4) 다형성: 호환되는 클래스들과 함께 작업하기

32.4.1) 다형성이란 무엇인가?

다형성(Polymorphism)(그리스어: "many forms")은 서로 다른 클래스의 객체들이 같은 메서드를 제공할 때, 이들을 동일한 방식으로 다룰 수 있는 능력을 말합니다.

Python에서는 여러 클래스가 같은 이름의 메서드를 가지고 있으면, 객체가 어떤 클래스에 속하는지 몰라도 그 메서드를 호출할 수 있습니다.

python
class Dog:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Woof!"
 
class Cat:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Meow!"
 
class Bird:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Tweet!"
 
# speak() 메서드가 있는 어떤 객체와도 동작하는 함수
def make_animal_speak(animal):
    print(animal.speak())
 
# 서로 다른 클래스에 대해 동작함
buddy = Dog("Buddy")
whiskers = Cat("Whiskers")
tweety = Bird("Tweety")
 
make_animal_speak(buddy)     # Output: Buddy says: Woof!
make_animal_speak(whiskers)  # Output: Whiskers says: Meow!
make_animal_speak(tweety)    # Output: Tweety says: Tweet!

make_animal_speak() 함수는 animal 매개변수가 어떤 클래스인지 신경 쓰지 않습니다. 그 객체에 speak() 메서드만 있으면 됩니다. 이것이 다형성의 실제 동작입니다.

32.4.2) 상속과 함께 쓰는 다형성

다형성은 특히 서브클래스가 부모 메서드를 오버라이드하는 상속과 함께 사용할 때 강력합니다:

python
class PaymentMethod:
    def process_payment(self, amount):
        return f"Processing ${amount:.2f}"
 
class CreditCard(PaymentMethod):
    def __init__(self, card_number):
        self.card_number = card_number
    
    def process_payment(self, amount):
        return f"Charging ${amount:.2f} to credit card ending in {self.card_number[-4:]}"
 
class PayPal(PaymentMethod):
    def __init__(self, email):
        self.email = email
    
    def process_payment(self, amount):
        return f"Sending ${amount:.2f} via PayPal to {self.email}"
 
class BankTransfer(PaymentMethod):
    def __init__(self, account_number):
        self.account_number = account_number
    
    def process_payment(self, amount):
        return f"Transferring ${amount:.2f} to account {self.account_number}"
 
# 어떤 PaymentMethod와도 동작하는 함수
def complete_purchase(payment_method, amount):
    print(payment_method.process_payment(amount))
    print("Purchase complete!")
 
# 모든 결제 수단이 같은 함수로 동작함
credit = CreditCard("1234567890123456")
paypal = PayPal("user@example.com")
bank = BankTransfer("9876543210")
 
complete_purchase(credit, 99.99)
# Output: Charging $99.99 to credit card ending in 3456
# Output: Purchase complete!
 
complete_purchase(paypal, 49.50)
# Output: Sending $49.50 via PayPal to user@example.com
# Output: Purchase complete!
 
complete_purchase(bank, 199.00)
# Output: Transferring $199.00 to account 9876543210
# Output: Purchase complete!

complete_purchase() 함수는 어떤 PaymentMethod 서브클래스와도 동작합니다. 각 서브클래스는 process_payment()의 자신만의 구현을 제공하지만, 함수는 어떤 구체 클래스와 작업하는지 알 필요가 없습니다.

32.4.3) 덕 타이핑: “오리처럼 걷고…”

Python의 다형성은 클래스들이 상속으로 연결되어 있을 필요가 없습니다. 이를 덕 타이핑(duck typing)이라고 하는데, "오리처럼 걷고 오리처럼 소리를 낸다면, 그것은 오리다"라는 말에서 유래했습니다. 즉, Python은 객체가 어떤 클래스인지보다 어떤 메서드를 가지고 있는지를 중요하게 생각합니다. 필요한 메서드만 있다면, 클래스 계층 구조와 상관없이 그 객체를 사용할 수 있습니다.

python
class FileWriter:
    def __init__(self, filename):
        self.filename = filename
    
    def write(self, data):
        print(f"Writing to {self.filename}: {data}")
 
class DatabaseWriter:
    def __init__(self, table_name):
        self.table_name = table_name
    
    def write(self, data):
        print(f"Inserting into {self.table_name}: {data}")
 
class ConsoleWriter:
    def write(self, data):
        print(f"Console output: {data}")
 
# write() 메서드가 있는 어떤 객체와도 동작하는 함수
def save_data(writer, data):
    writer.write(data)
 
# 서로 관련이 없어도 세 클래스 모두 동작함
file_writer = FileWriter("data.txt")
db_writer = DatabaseWriter("users")
console_writer = ConsoleWriter()
 
save_data(file_writer, "User data")
# Output: Writing to data.txt: User data
 
save_data(db_writer, "User data")
# Output: Inserting into users: User data
 
save_data(console_writer, "User data")
# Output: Console output: User data

이 클래스들 중 어느 것도 공통 부모를 상속받지 않지만, 모두 write() 메서드를 갖고 있기 때문에 save_data()와 함께 동작합니다. 이것이 덕 타이핑이며, 이 함수는 클래스가 아니라 인터페이스(사용 가능한 메서드)에만 관심이 있습니다.

32.4.4) 실용 예제: 플러그인 시스템

다형성은 유연하고 확장 가능한 시스템을 가능하게 합니다. 다음은 데이터 처리를 위한 간단한 플러그인 시스템입니다:

python
class DataProcessor:
    def process(self, data):
        return data  # 기본 구현은 아무것도 하지 않음
 
class UppercaseProcessor(DataProcessor):
    def process(self, data):
        return data.upper()
 
class ReverseProcessor(DataProcessor):
    def process(self, data):
        return data[::-1]
 
class RemoveSpacesProcessor(DataProcessor):
    def process(self, data):
        return data.replace(" ", "")
 
class DataPipeline:
    def __init__(self):
        self.processors = []
    
    def add_processor(self, processor):
        self.processors.append(processor)
    
    def run(self, data):
        result = data
        for processor in self.processors:
            result = processor.process(result)  # 다형적 호출
        return result
 
# 처리 파이프라인 구성
pipeline = DataPipeline()
pipeline.add_processor(UppercaseProcessor())
pipeline.add_processor(RemoveSpacesProcessor())
pipeline.add_processor(ReverseProcessor())
 
# 파이프라인을 통해 데이터 처리
input_data = "Hello World"
output = pipeline.run(input_data)
print(f"Input:  {input_data}")   # Output: Input:  Hello World
print(f"Output: {output}")        # Output: Output: DLROWOLLEH

DataPipeline은 어떤 프로세서가 들어 있는지 구체적으로 알 필요가 없습니다. 그저 각각에 대해 process()를 호출하기만 합니다. 파이프라인 코드를 바꾸지 않고도 새로운 프로세서 타입을 쉽게 추가할 수 있습니다.

32.5) 타입과 클래스 관계 확인하기 (isinstance, issubclass)

32.5.1) isinstance()로 인스턴스 타입 확인하기

때로는 객체가 특정 클래스의 인스턴스인지 확인해야 합니다. isinstance() 함수가 이를 수행합니다:

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
class Cat(Animal):
    pass
 
buddy = Dog()
whiskers = Cat()
 
# 객체가 클래스의 인스턴스인지 확인
print(isinstance(buddy, Dog))     # Output: True
print(isinstance(buddy, Animal))  # Output: True (Dog inherits from Animal)
print(isinstance(buddy, Cat))     # Output: False
 
print(isinstance(whiskers, Cat))    # Output: True
print(isinstance(whiskers, Animal)) # Output: True
print(isinstance(whiskers, Dog))    # Output: False

buddyDog 인스턴스임에도 isinstance(buddy, Animal)True를 반환하는 점에 주목하세요. DogAnimal을 상속받기 때문에 Dog 인스턴스는 Animal 인스턴스로도 간주됩니다.

32.5.2) isinstance()가 상속을 반영하는 이유

isinstance() 함수는 전체 상속 체인을 확인합니다:

python
class Vehicle:
    pass
 
class Car(Vehicle):
    pass
 
class ElectricCar(Car):
    pass
 
tesla = ElectricCar()
 
# 모든 상속 단계 확인
print(isinstance(tesla, ElectricCar))  # Output: True
print(isinstance(tesla, Car))          # Output: True
print(isinstance(tesla, Vehicle))      # Output: True
print(isinstance(tesla, str))          # Output: False

is a

is a

is a

tesla 인스턴스

ElectricCar

Car

Vehicle

tesla 객체는 ElectricCar의 인스턴스이지만, 상속 때문에 CarVehicle의 인스턴스이기도 합니다.

32.5.3) 여러 타입을 한 번에 확인하기

튜플을 전달하면 여러 클래스 중 하나의 인스턴스인지 확인할 수 있습니다:

python
class Dog:
    pass
 
class Cat:
    pass
 
class Bird:
    pass
 
def is_pet(animal):
    return isinstance(animal, (Dog, Cat, Bird))
 
buddy = Dog()
whiskers = Cat()
tweety = Bird()
rock = "just a rock"
 
print(is_pet(buddy))     # Output: True
print(is_pet(whiskers))  # Output: True
print(is_pet(tweety))    # Output: True
print(is_pet(rock))      # Output: False

이는 isinstance(animal, Dog) or isinstance(animal, Cat) or isinstance(animal, Bird)를 작성하는 것보다 더 간결합니다.

32.5.4) issubclass()로 클래스 관계 확인하기

issubclass() 함수는 한 클래스가 다른 클래스의 서브클래스인지 확인합니다:

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
class Cat(Animal):
    pass
 
class Poodle(Dog):
    pass
 
# 클래스 관계 확인
print(issubclass(Dog, Animal))    # Output: True
print(issubclass(Cat, Animal))    # Output: True
print(issubclass(Poodle, Dog))    # Output: True
print(issubclass(Poodle, Animal)) # Output: True (indirect inheritance)
print(issubclass(Dog, Cat))       # Output: False
 
# 클래스는 자기 자신에 대해서도 서브클래스로 간주됨
print(issubclass(Dog, Dog))       # Output: True

issubclass()는 인스턴스가 아니라 클래스에 대해 동작한다는 점에 유의하세요. 인스턴스에는 isinstance(), 클래스에는 issubclass()를 사용합니다.

32.5.5) 타입 체크의 실용적 활용 사례

타입 체크는 서로 다른 타입에 대해 다른 동작이 필요할 때 유용합니다:

python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
 
class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus
 
class Contractor:
    def __init__(self, name, hourly_rate, hours):
        self.name = name
        self.hourly_rate = hourly_rate
        self.hours = hours
 
def calculate_payment(worker):
    # 상속 관계에서 isinstance()를 사용할 때는 서브클래스를 부모 클래스보다 먼저 체크해야 합니다
    if isinstance(worker, Manager):  # Manager를 먼저 체크
        return worker.salary + worker.bonus
    elif isinstance(worker, Employee):  # 그 다음 Employee(부모 클래스)를 체크
        return worker.salary
    elif isinstance(worker, Contractor):
        return worker.hourly_rate * worker.hours
    else:
        return 0
 
alice = Employee("Alice", 50000)
bob = Manager("Bob", 70000, 10000)
charlie = Contractor("Charlie", 50, 160)
 
print(f"Alice's payment: ${calculate_payment(alice)}")    # Output: Alice's payment: $50000
print(f"Bob's payment: ${calculate_payment(bob)}")        # Output: Bob's payment: $80000
print(f"Charlie's payment: ${calculate_payment(charlie)}")# Output: Charlie's payment: $8000

하지만 많은 경우, 타입 체크보다는 다형성(각 클래스가 공통 메서드를 구현하도록 하는 방식)이 더 좋습니다:

python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def calculate_payment(self):
        return self.salary
 
class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus
    
    def calculate_payment(self):
        return self.salary + self.bonus
 
class Contractor:
    def __init__(self, name, hourly_rate, hours):
        self.name = name
        self.hourly_rate = hourly_rate
        self.hours = hours
    
    def calculate_payment(self):
        return self.hourly_rate * self.hours
 
# 타입 체크 불필요 - 다형성이 처리합니다
workers = [
    Employee("Alice", 50000),
    Manager("Bob", 70000, 10000),
    Contractor("Charlie", 50, 160)
]
 
for worker in workers:
    payment = worker.calculate_payment()  # 다형적 호출
    print(f"{worker.name}'s payment: ${payment}")

Output:

Alice's payment: $50000
Bob's payment: $80000
Charlie's payment: $8000

이러한 다형적 접근 방식은 더 유연하고 새로운 worker 타입을 추가하기 쉽습니다. 새로운 worker 클래스를 추가할 때 호출 코드를 수정할 필요가 없습니다—단지 calculate_payment() 메서드만 구현하면 됩니다.


상속과 다형성은 코드를 조직하고 유연하고 확장 가능한 시스템을 만드는 강력한 도구입니다. 서브클래스를 만들면 기존 코드를 재사용하면서 동작을 추가하거나 수정할 수 있습니다. 메서드를 오버라이드하면 서브클래스가 동작하는 방식을 커스터마이즈할 수 있습니다. 그리고 다형성을 사용하면 공통 인터페이스를 통해 여러 다른 클래스에서 동작하는 코드를 작성할 수 있습니다.

핵심은 이러한 기능을 신중하게 사용하는 것입니다:

  • 진짜 “is-a” 관계가 있을 때 상속을 사용하세요(DogAnimal입니다)
  • 클래스가 하는 일을 완전히 바꾸기 위해서가 아니라, 동작을 특화하기 위해 메서드를 오버라이드하세요
  • 부모 동작을 완전히 대체하기보다 확장하려면 super()를 사용하세요
  • 가능하면 타입 체크보다 다형성(공통 메서드)을 선호하세요

더 큰 프로그램을 만들수록, 이러한 객체 지향 기법은 더 이해하기 쉽고 유지보수하기 쉬우며 확장하기 쉬운 코드를 만드는 데 도움이 될 것입니다.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai