지난 시간에는 객체 지향 프로그래밍의 네 기둥 중 세 번째, 상속에 대한 개념을 알아봤습니다. 'A는 B다'라는 포함 관계에 따라 상속 관계를 정의했는데요.

이번 시간에는 상속과 관련된 다양한 개념들에 대해 알아보겠습니다.

👨‍👩‍👧 오버라이딩

앞서 Cashier 클래스와 DeliveryMan 클래스가 Employee 클래스를 상속받도록 했습니다. 그런데 두 클래스 안에는 아무 내용도 정의하지 않아 Employee 클래스의 변수와 메소드만을 물려 받고 있는데요. 이렇게 하면 두 클래스를 제대로 사용할 수 없습니다. 두 클래스에 차이가 없기 때문이죠.

따라서, 물려받은 내용을 Cashier 클래스와 DeliveryMan 클래스에 맞게 각각 바꿔주어야 합니다. 이렇게 부모 클래스로부터 물려받은 내용을 자식 클래스에 맞게 변경하는 것을 오버라이딩(Overriding)이라고 합니다. 오버라이딩은 우리말로 덮어 쓴다는 뜻을 가지고 있습니다. 부모에게 물려 받은 내용을 자기 자신에게 덮어 쓴다는 것이죠.

먼저, Cashier 클래스의 이닛 메소드를 오버라이딩 해야 하는데요. Employee의 이닛 메소드를 그대로 물려받아도 될까요? Cashier 클래스에는 Employee 클래스의 name과 wage 외에도 커피 판매량을 나타내는 num_sold라는 변수가 있었습니다. 따라서, Employee의 이닛 메소드를 그대로 물려 받으면 안되겠습니다.

오버라이딩은 자식 클래스에서 물려받은 메소드와 같은 이름의 메소드를 내용만 바꿔서 정의해 주면 되는데요.

def __init__(self, name, wage, num_sold):
    self.name = name
    self.wage = wage
    self.num_sold = num_sold

이런 식으로 똑같이 이닛 메소드를 정의하고 num_sold라는 변수를 추가해주면 됩니다. 이로써 부모 클래스의 이닛 메소드와 차이점을 가지게 됩니다.

이렇게 오버라이딩을 하면 Cashier 클래스로 인스턴스를 생성할 때 Employee 클래스의 이닛 메소드가 아니라 Cashier 클래스 자신의 이닛 메소드를 실행합니다.

그런데 위 코드는 기존의 Cashier 클래스를 작성했을 때와 같은 코드이죠? 이렇게 해서는 상속의 의미가 없습니다. 기존 코드와 차이를 주기 위해서는 중복되는 부분을 생략해야 하는데요.

Employee.__init__(self, name, wage)

생략된 부분에는 위 코드를 넣어주면 됩니다. 이렇게 하면 부모 클래스를 통해 이닛 메소드를 호출할 수 있습니다. 이 코드마저도 더 간단히 만들어줄 수 있는데요.

super().__init__(name, wage)

이처럼 부모 클래스 이름 대신 super라는 함수를 사용하면 됩니다. super는 자식 클래스에서 부모 클래스의 메소드를 사용하고 싶을 때 쓰는 함수입니다. super 함수를 사용할 때에는 self 파라미터를 넘겨줄 필요가 없어 생략했습니다. 이는 상속에서 자주 사용되는 방식입니다.

이번에는 Employee의 던더 str을 Cashier 클래스에 오버라이딩 해보겠습니다. 두 메소드의 차이점은 직원이라는 명칭에 직원 유형을 붙였다는 것인데요.

def __str__(self):
    return f"{Cashier.company_name} 계산대 직원: {self.name}"

이런 식으로 Cashier 클래스가 원하는 방식으로 코드를 수정해주면 됩니다.

이번에는 변수를 오버라이딩 해보겠습니다. 변수의 오버라이딩은 자식 클래스에서 다른 값을 대입하는 것을 의미합니다.

현재 Employee 클래스의 시급 인상률은 1.02인데요. 계산대 직원들만 시급 인상률을 높여주고 싶습니다. 그럼 그냥 자식 클래스에서 똑같은 변수 이름을 사용하고 다른 값을 넣어주기만 하면 됩니다.

class Cashier(Employee):
    raise_percentage = 1.05
print(taki.raise_percentage)
1.05

👨‍👩‍👧 mro

앞서 상속에서 자주 사용되는 메소드로 mro를 배웠습니다. 이 메소드는 클래스가 상속받는 부모 클래스들이 순서대로 담긴 리스트를 리턴하는데요.

오버라이딩은 자식 클래스에서 물려받은 메소드를 같은 이름의 메소드의 내용을 바꿔 정의합니다. 이렇게 하면 자식 클래스에는 물려 받은 메소드와 오버라이딩 한 메소드가 있는데 두 메소드 중 오버라이딩한 메소드가 출력됩니다. 왜 그런 걸까요?

이유는 바로 메소드를 호출하면 Python이 mro에 나와 있는 순서대로 메소드를 탐색하기 때문입니다. 다시 말해, mro에서 앞에 나오는 자식 클래스의 메소드가 뒤에 나오는 부모 클래스의 같은 이름의 메소드보다 먼저 발견되어 호출되기 때문입니다.

다시 한번, Cashier의 mro 메소드를 호출하겠습니다.

print(Cashier.mro())
[<class '__main__.Cashier'>, <class '__main__.Employee'>, <class 'object'>]

위에 나온 순서가 바로 메소드를 찾는 순서가 됩니다.

만약 Cashier 클래스에 없는 raise_wage 메소드를 호출하면 어떻게 될까요? 먼저 Python은 리스트 순서에 따라 Cashier 클래스에서 raise_wage를 찾습니다. raise_wage는 Cashier에 없기 때문에 다음 순서인 Employee 클래스에서 찾기 시작합니다.

그렇게 Employee 클래스에서 raise_wage를 찾게 되면 탐색을 멈추고 Employee 클래스의 raise_wage 메소드를 호출합니다.

mro는 Method Resolution Order의 약어입니다. 우리말로 하면 메소드 검색 순서를 의미합니다.

오버라이딩과 mro는 밀접한 관련을 맺고 있다고 할 수 있는데요. 메소드 검색 방향은 자식에서 부모 순입니다. 이 때문에 메소드 오버라이딩이 가능한 것이죠. 부모 클래스와 자식 클래스에 같은 이름의 메소드가 있더라도 자식 클래스의 메소드가 먼저 검색되어 실행됩니다.

👨‍👩‍👧 상속 - 기능 추가하기

기존 Cashier 클래스에는 coffee_price라는 클래스 변수와 take_order라는 메소드가 있었습니다.

이제 이 변수와 메소드를 추가해서 완성된 전체 Cashier 코드를 봅시다.

class Cashier(Employee):
    """계산대 직원 클래스"""
    raise_percentage = 1.05
    coffee_price = 3000
    
    def __init__(self, name, wage, num_sold=0):
        super().__init__(name, wage)
        self.num_sold = num_sold
        
    def take_order(self, money_received):
        """주문과 돈을 받고 거스름돈을 리턴한다"""
        if Cashier.coffee_price > money_received:
            print("돈이 충분하지 않습니다.")
            return money_received
        else:
            self.num_sold += 1
            change = money_received - Cashier.coffee_price
            return change
        
    def __str__(self):
        return f"{Cashier.company_name} 계산대 직원: {self.name}"

이렇게 중복되지 않았던 Cashier 클래스만의 내용은 상속 받기 전과 똑같이 정의해주면 됩니다.

완성된 DeliveryMan의 전체 코드도 볼까요?

class DeliveryMan(Employee):
    def __init__(self, name, wage, on_standby):
        super().__init__(name, wage)
        self.on_standby = on_standby
    
    def deliver(self, address):
        """배달원이 대기 중이면 주어진 주소로 배달을 보내고 아니면 메시지를 출력한다"""
        if self.on_standby:
            print(address + "로 배달갑니다!")
            self.on_standby = False
        else:
            print("이미 배달 갔습니다!")

    def back(self):
        """배달원을 복귀 처리한다"""
        self.on_standby = True

    def __str__(self):
        return DeliveryMan.company_name + " 배달원: " + self.name

👨‍👩‍👧 상속 정리

이 챕터를 시작하며 우리는 Cashier 클래스와 DeliveryMan 클래스를 각각 정의했습니다. 그런데 이 두 클래스에는 겹치는 공통 부분이 있었죠? 이 공통 부분을 가지고 부모 클래스인 Employee 클래스를 만들었습니다.

다음으로 Employee로부터 모든 메소드와 변수를 물려 받고 Cashier 클래스와 DeliveryMan 클래스에 맞게 코드를 변경하여 오버라이딩 했습니다.

마지막으로 부모 클래스와 중복되지 않는 Cashier 클래스와 DeliveryMan 고유의 변수와 메소드를 추가했습니다.

위 과정이 바로 상속을 적용하는 방법입니다. 그럼 상속의 이점은 무엇일까요? 상속은 Cashier 클래스와 DeliveryMan 클래스와 같은 종류의 새로운 클래스를 만들 때 효율적입니다.

예를 들어, 요리사를 위한 Cook 클래스를 만들었다고 합시다. 이 Cook은 Employee 클래스로부터 상속 받기 때문에 직원이라면 누구나 가질 수 있는 부분을 매번 코드로 적을 필요 없이 사용할 수 있습니다. Cook 클래스 고유의 특성은 오버라이딩 하거나 새롭게 추가하면 되구요.

정리하자면, 상속을 적용하면 더 적은 코드로 새로운 클래스를 만들 수 있다는 이점이 있습니다. 중복을 줄일 수 있으니 매우 효율적이죠?

👨‍👩‍👧 다중 상속

Python에서는 하나의 자식 클래스여러 부모 클래스로부터 상속 받는 것이 가능합니다. 이와 같은 경우를 다중 상속이라고 합니다.

를 하나 들어볼까요? 타키탸키는 엔지니어이지만 부업으로 게임 유튜버를 하고 있습니다. 이때, 타키탸키라는 사람을 표현하려면 다중 상속을 하면 됩니다. 다시 말해, 개발자 클래스와 유튜버 클래스를 둘 다 상속 받는 자식 클래스를 만들면 된다는 뜻입니다.

먼저, 엔지니어 클래스와 유튜버 클래스부터 봅시다.

class Engineer:
    def __init__(self, work_language):
        self.work_language = work_language
        
    def program(self):
        print(f"{self.work_language}(으)로 프로그래밍합니다.")
        
class Youtuber:
    def __init__(self, game_level):
        self.game_level = game_level
        
    def play_game(self):
        print("{self.game_level} 등급에서 게임을 합니다.")

이제, 자식 클래스 EngineerYoutuber 클래스를 보겠습니다. 이 클래스는 위 두 클래스를 상속 받아야 하는데요. 방법은 매우 간단합니다.

class EngineerYoutuber(Engineer, Youtuber):

이렇게 괄호 안에 부모 클래스의 이름을 나열하기만 하면 됩니다. 그럼 해당 클래스는 두 부모 클래스의 변수와 메소드를 모두 상속받게 됩니다.

EngineerYoutuber 클래스의 이닛 메소드를 정의해볼까요?

def __init__(self, work_language, game_level)

두 부모 클래스로부터 상속을 받기 때문에 각 부모 클래스의 파라미터가 모두 필요합니다.

이제 오버라이딩을 하면 되는데요. super 함수를 이용하면 되겠죠? 그런데 문제가 생겼습니다. 이 super 함수지칭하는 것이 Engineer일까요? 아니면 Youtuber일까요?

다중 상속의 문제점은 super 함수를 썼을 때 어느 부모 클래스를 지칭하는지를 알 수 없다는 것입니다. 따라서, 다중 상속을 할 때에는 두 클래스의 이름을 직접 사용해야 합니다.

Engineer.__init__(self, work_language)
Youtuber.__init__(self, game_level)

super를 사용하지 않기 때문에 self를 생략해서는 안됩니다. 이렇게 하면 오버라이딩까지 마쳤습니다.

이제 다중 상속이 잘 되었는지 확인해보겠습니다.

taki = EngineerYoutuber("파이썬", "다이아")

taki.program()
taki.play_game
파이썬(으)로 프로그래밍합니다.
다이아 등급에서 게임을 합니다.

EngineerYoutuber가 Engineer 클래스와 Youtuber 클래스를 다중 상속했기 때문에 각자의 메소드가 잘 실행됩니다.

👨‍👩‍👧 다중 상속의 위험성

이번에는 앞서 작성했던 Cashier 클래스와 DeliveryMan 클래스를 다중 상속해보겠습니다. 카운터도 보고 배달도 맡는 CashierDeliveryMan 클래스를 작성해봅시다.

class CashierDeliveryMan(Cashier, DeliveryMan):

이제 이닛 메소드를 정의할 건데요. 두 부모 클래스는 name과 wage 파라미터를 공통으로 가지고 있고 Cashier는 num_sold를, DeliveryMan은 on_stanby 고유 파라미터를 각각 가지고 있습니다.

def __init__(self, name, wage, num_sold=0, on_stanby):
    Employee.__init__(self, name, wage)
    self.num_sold = num_sold
    self.on_stanby = on_stanby

이런 식으로 공통 부분은 두 클래스의 부모 클래스의 이름을 가져다 직접 쓰면 되고 고유 변수는 각각 따로 선언하면 됩니다.

이제 잘 실행되는지 확인해 보겠습니다.

cashier_and_delivery_man = CashierDeliveryMan("타키탸키", 10000, 10, True)

cashier_and_delivery_man.take_order(2000)

cashier_and_delivery_man.deliver_to("포켓몬 트레이닝 센터 101호")
cashier_and_delivery_man.deliver_to("포켓몬 트레이닝 센터 102호")
cashier_and_delivery_man.back()
돈이 충분하지 않습니다. 돈을 다시 계산해서 주세요!
포켓몬 트레이닝 센터 101호로 배달 갑니다!
이미 배달 갔습니다!

실행이 잘 되는 걸 보니 다중 상속이 잘 된 것 같습니다.

그런데 한 가지 문제가 있습니다.

print(cashier_and_delivery_man)
스타커피 계산대 직원: 타키탸키

CashierDeliveryMan 클래스에는 던더 str 메소드가 없습니다. 따라서, 위 문구는 부모 클래스로부터 받아온 것인데요. 출력 내용을 보면 Cashier 클래스로부터 던더 str 메소드를 받아온 것 같습니다. 그렇다면 두 가지 클래스 중 왜 Cashier 클래스에서 받아온 걸까요?

답은 앞서 배운 mro 메소드에 있습니다.

print(CashierDeliveryMan.mro())
[<class '__main__.CashierDeliveryMan'>, <class '__main__.Cashier'>, <class '__main__.DeliveryMan'>, <class '__main__.Employee'>, <class '__main__.object>]

위 결과를 보면 Cashier 클래스가 DeliveryMan보다 앞에 위치하고 있습니다. 순서상으로 Cashier 클래스가 우선이라는 것이죠. 그런데 이 순서는 상속 관계의 선언에 따라 달라집니다.

class CashierDeliveryMan(DeliveryMan, Cashier):

이런 식으로 상속 순서를 바꾸면 이번에는 DeliveryMan이 Cashier보다 앞에 위치하게 됩니다. 이렇게 다중 상속을 하게 되면 상속 받는 순서에 따라 mro가 바뀝니다. 하지만 이런 식으로 코딩을 하는 동안 순서를 상기해야 하는 것은 조금 번거로워 보입니다.

다른 객체 지향 언어인 JAVA는 이러한 문제를 방지하기 위해 다중 상속을 지원하지 않습니다. 다시 말해, 오직 하나의 부모 클래스만 상속이 가능하다는 것이죠.

다중 상속의 이러한 문제를 해결하는 방법이 있는데요. 바로 자식 클래스에서 해당 메소드를 오버라이딩 하는 것입니다. 그럼 부모 클래스를 생각할 필요 없이 무조건 우선 순위에 있는 자식 클래스의 메소드가 실행될 테니까요.

정리하자면, 다중 상속의 번거로움을 피하기 위해서는

  1. 부모 클래스끼리 같은 이름의 메소드를 갖지 않도록 하거나,
  2. 같은 이름의 메소드는 자식 클래스에서 오버라이딩

하면 됩니다. 이렇게 해야 어느 부모 클래스의 메소드를 호출하는 것인지 혼돈이 생기는 문제를 방지할 수 있습니니다.


이번 시간에는 상속과 관련해서 오버라이딩, mro, 다중 상속 등의 여러 개념을 배웠습니다.

다음 시간에는 마지막 기둥, 다형성에 대해 함께 알아보겠습니다.

* 이 자료는 CODEIT의 '객체 지향 프로그래밍' 강의를 기반으로 작성되었습니다.
profile
There's Only One Thing To Do: Learn All We Can

0개의 댓글