Python 클래스 특징 이해하기

PythonBeginner
지금 연습하기

소개

본 랩 (Lab) 에서는 Python 의 주요 객체 지향 프로그래밍 (OOP) 개념에 대한 실질적인 이해를 얻게 됩니다. 캡슐화 (encapsulation) 부터 시작하여, 클래스 내부에 데이터와 메서드를 묶고 private 속성을 사용하여 데이터 접근을 제어하는 방법을 학습합니다.

다음으로, 상속 (inheritance) 을 구현하여 클래스 간의 관계를 구축하고 코드 재사용성을 높이는 방법을 배웁니다. 또한, 서로 다른 클래스의 객체들을 균일하게 취급할 수 있게 해주는 다형성 (polymorphism) 을 탐구할 것입니다. 마지막으로, super() 메서드를 사용하여 부모 클래스의 메서드를 효과적으로 호출하는 방법을 익히고, 하나의 클래스가 여러 부모 클래스로부터 상속받을 수 있는 다중 상속 (multiple inheritance) 을 실습해 볼 것입니다.

이것은 학습 및 실습에 도움이 되는 단계별 지침을 제공하는 Guided Lab 입니다. 각 단계를 완료하고 실습 경험을 쌓으려면 지침을 주의 깊게 따르십시오. 과거 데이터에 따르면 이 랩은 초급 (beginner) 수준이며 완료율은 100%입니다. 학습자들로부터 100%의 긍정적인 평가를 받았습니다.

기본 클래스를 사용하여 캡슐화 탐색하기

이 단계에서는 핵심 OOP 원칙인 **캡슐화 (encapsulation)**를 탐구합니다. 캡슐화는 데이터 (속성, attributes) 와 그 데이터를 조작하는 메서드를 단일 단위인 클래스로 묶는 것을 포함합니다. 또한 객체의 내부 상태에 대한 직접적인 접근을 제한하여 의도치 않은 데이터 변경을 방지하는 데 도움을 줍니다.

Python 에서는 속성이 "private"임을 나타내기 위해 명명 규칙을 사용합니다. 속성 앞에 단일 밑줄 (예: _name) 을 붙이면 내부 사용을 위한 것임을 나타냅니다. 이는 엄격하게 강제되지는 않지만, 개발자들이 존중하는 강력한 관례입니다.

먼저, 클래스가 어떻게 구성될 수 있는지 확인하기 위해 Dog 클래스와 Cat 클래스라는 두 개의 별도 클래스를 생성하겠습니다.

먼저, WebIDE 왼쪽의 파일 탐색기에서 animal_classes.py 파일을 찾으십시오. 파일을 열고 다음 Python 코드를 추가합니다. 이 코드는 Dog 클래스와 Cat 클래스를 정의하며, 각 클래스는 private 속성인 _name과 이 속성과 상호작용하는 메서드를 가집니다.

## File: animal_classes.py

class Dog:
    def __init__(self, name):
        ## 단일 밑줄 접두사는 "private" 속성을 나타냅니다.
        self._name = name

    ## private 속성의 값을 가져오기 위한 public 메서드.
    def get_name(self):
        return self._name

    ## private 속성의 값을 설정하기 위한 public 메서드.
    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Woof!")

class Cat:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Meow!")

## 이 블록은 스크립트가 직접 실행될 때만 실행됩니다.
if __name__ == "__main__":
    ## Dog 클래스의 인스턴스 생성
    my_dog = Dog("Buddy")
    print(f"Initial dog name: {my_dog.get_name()}")

    ## setter 메서드를 사용하여 개의 이름 변경
    my_dog.set_name("Rocky")
    print(f"New dog name: {my_dog.get_name()}")
    my_dog.say()

    print("-" * 20)

    ## Cat 클래스의 인스턴스 생성
    my_cat = Cat("Whiskers")
    print(f"Cat name: {my_cat.get_name()}")
    my_cat.say()

코드를 추가한 후 파일을 저장합니다.

이제 스크립트를 실행하여 캡슐화가 작동하는 방식을 확인해 보겠습니다. WebIDE 에서 터미널을 열고 다음 명령을 실행합니다.

python animal_classes.py

다음과 같은 출력을 보게 될 것이며, 이는 우리가 public 메서드인 get_nameset_name을 통해 private 속성인 _name과 상호작용하고 있음을 보여줍니다.

Initial dog name: Buddy
New dog name: Rocky
Rocky says: Woof!
--------------------
Cat name: Whiskers
Whiskers says: Meow!

상속 및 다형성 구현하기

이전 단계에서 Dog 클래스와 Cat 클래스가 많은 동일한 코드 (__init__, get_name, set_name) 를 공유하고 있음을 확인했을 수 있습니다. 이는 **상속 (inheritance)**을 사용하기에 완벽한 기회입니다. 상속은 새로운 클래스 (자식 또는 서브클래스) 가 기존 클래스 (부모 또는 슈퍼클래스) 의 속성과 메서드를 물려받아 코드 재사용성을 높일 수 있게 해줍니다.

또한, "여러 형태"를 의미하는 **다형성 (polymorphism)**을 도입할 것입니다. OOP 에서 이는 서로 다른 클래스가 동일한 메서드 호출에 대해 자신만의 고유한 방식으로 응답할 수 있는 능력을 의미합니다.

코드를 리팩토링해 보겠습니다. 공통 코드를 담을 부모 클래스 Animal을 생성하고 DogCat이 이 클래스를 상속받도록 할 것입니다. 각 클래스마다 다른 say 메서드는 다형성을 보여주는 예시가 될 것입니다.

animal_classes.py 파일을 열고 전체 내용을 다음 코드로 교체하십시오.

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

## Dog 는 Animal 을 상속받습니다
class Dog(Animal):
    ## 이는 Animal 클래스의 say() 메서드를 오버라이드 (override) 합니다
    def say(self):
        print(f"{self._name} says: Woof!")

## Cat 는 Animal 을 상속받습니다
class Cat(Animal):
    ## 이 역시 say() 메서드를 오버라이드합니다
    def say(self):
        print(f"{self._name} says: Meow!")

def make_animal_speak(animal_instance):
    animal_instance.say()

if __name__ == "__main__":
    generic_animal = Animal("Creature")
    my_dog = Dog("Buddy")
    my_cat = Cat("Whiskers")

    print("--- 각 객체에 say() 호출 ---")
    generic_animal.say()
    my_dog.say()
    my_cat.say()

    print("\n--- 다형성 시연 ---")
    make_animal_speak(generic_animal)
    make_animal_speak(my_dog)
    make_animal_speak(my_cat)

파일을 저장합니다. DogCat 클래스가 훨씬 간결해진 것을 확인하십시오. 이들은 Animal로부터 __init__, get_name, set_name 메서드를 상속받습니다. 각 클래스는 자신만의 say 메서드 버전을 제공하며, 이는 메서드 오버라이딩 (method overriding) 의 예시입니다.

이제 터미널에서 업데이트된 스크립트를 실행합니다.

python animal_classes.py

출력 결과는 다음과 같습니다.

--- 각 객체에 say() 호출 ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

--- 다형성 시연 ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

make_animal_speak 함수는 say 메서드를 가진 모든 객체를 인수로 받습니다. 비록 우리가 서로 다른 유형의 객체 (Animal, Dog, Cat) 를 전달하더라도, 각 객체가 say 동작을 자신만의 방식으로 수행하는 방법을 알기 때문에 함수는 올바르게 작동합니다. 이것이 바로 다형성의 힘입니다.

super() 메서드를 사용하여 기능 확장하기

자식 클래스가 부모 클래스의 메서드를 오버라이드 (override) 할 때, 단순히 메서드를 대체하는 것이 아니라 부모의 메서드를 확장해야 하는 경우가 있습니다. super() 함수는 자식 클래스 내에서 부모 클래스의 메서드를 호출할 수 있는 방법을 제공합니다.

이는 __init__ 메서드에서 매우 흔하게 사용됩니다. 자식 클래스는 종종 부모 클래스가 수행하는 초기화 외에도 자신만의 고유한 초기화 단계를 수행해야 합니다.

DogCat 클래스에 고유한 속성을 추가해 보겠습니다. Dogage를 가지게 되고, Catcolor를 가지게 됩니다. super()를 사용하여 부모인 Animal 클래스의 __init__ 메서드가 여전히 호출되어 _name 속성이 설정되도록 할 것입니다.

animal_classes.py 파일을 다음 코드로 교체하여 수정하십시오.

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        print(f"Animal __init__ called for {name}")
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

class Dog(Animal):
    def __init__(self, name, age):
        ## 부모의 __init__ 메서드를 호출하여 'name' 속성 처리
        super().__init__(name)
        print("Dog __init__ called")
        self.age = age

    def say(self):
        ## super() 를 사용하여 부모의 say() 메서드를 호출할 수도 있습니다.
        ## super().say()
        print(f"{self._name} says: Woof! I am {self.age} years old.")

class Cat(Animal):
    def __init__(self, name, color):
        ## 부모의 __init__ 메서드를 호출합니다
        super().__init__(name)
        print("Cat __init__ called")
        self.color = color

    def say(self):
        print(f"{self._name} says: Meow! I have {self.color} fur.")

if __name__ == "__main__":
    my_dog = Dog("Buddy", 5)
    my_dog.say()

    print("-" * 20)

    my_cat = Cat("Whiskers", "black")
    my_cat.say()

파일을 저장합니다. 이 버전에서는 Dog.__init__Cat.__init__가 먼저 super().__init__(name)을 호출합니다. 이는 Animal.__init__ 내의 코드를 실행하여 _name 속성을 설정합니다. 그 후, 자신들만의 고유한 초기화 (self.age = ageself.color = color) 를 진행합니다.

터미널에서 스크립트를 실행합니다.

python animal_classes.py

출력 결과는 __init__ 호출 체인과 확장된 say 메서드를 보여줍니다.

Animal __init__ called for Buddy
Dog __init__ called
Buddy says: Woof! I am 5 years old.
--------------------
Animal __init__ called for Whiskers
Cat __init__ called
Whiskers says: Meow! I have black fur.

다중 상속 실습하기

Python 에서는 하나의 클래스가 둘 이상의 부모 클래스로부터 상속받는 것이 가능합니다. 이를 **다중 상속 (multiple inheritance)**이라고 합니다. 이는 서로 다른 소스의 기능들을 혼합하는 강력한 도구가 될 수 있지만, 특히 메서드 이름이 동일할 때 Python 이 어느 부모의 메서드를 사용할지 결정하는 방식 때문에 복잡성을 야기하기도 합니다.

이러한 검색 순서를 **메서드 결정 순서 (Method Resolution Order, MRO)**라고 합니다. Python 은 일관되고 예측 가능한 MRO 를 결정하기 위해 C3 선형화 (C3 linearization) 라는 알고리즘을 사용합니다.

새로운 예제를 통해 이를 탐구해 보겠습니다. 파일 탐색기에서 multiple_inheritance.py 파일을 열고 다음 코드를 추가하십시오.

## File: multiple_inheritance.py

class ParentA:
    def speak(self):
        print("Speaking from ParentA")

    def common_method(self):
        print("ParentA's common method")

class ParentB:
    def speak(self):
        print("Speaking from ParentB")

    def common_method(self):
        print("ParentB's common method")

## Child 는 A 를 상속받고, 그 다음 B 를 상속받습니다
class Child_AB(ParentA, ParentB):
    pass

## Child 는 B 를 상속받고, 그 다음 A 를 상속받습니다
class Child_BA(ParentB, ParentA):
    def common_method(self):
        print("Child_BA's own common method")

if __name__ == "__main__":
    child1 = Child_AB()
    child2 = Child_BA()

    print("--- Child_AB (ParentA, ParentB) 조사 ---")
    child1.speak()
    child1.common_method()
    ## .mro() 메서드는 메서드 결정 순서를 보여줍니다
    print("MRO for Child_AB:", [c.__name__ for c in Child_AB.mro()])

    print("\n--- Child_BA (ParentB, ParentA) 조사 ---")
    child2.speak()
    child2.common_method()
    print("MRO for Child_BA:", [c.__name__ for c in Child_BA.mro()])

파일을 저장합니다. 여기서 Child_ABParentA를 상속받고 그 다음 ParentB를 상속받습니다. Child_BA는 역순으로 상속받습니다. 메서드가 호출되면 Python 은 MRO 에 지정된 순서대로 해당 메서드를 검색합니다.

터미널에서 스크립트를 실행합니다.

python multiple_inheritance.py

다음과 같은 출력을 보게 될 것입니다.

--- Investigating Child_AB (ParentA, ParentB) ---
Speaking from ParentA
ParentA's common method
MRO for Child_AB: ['Child_AB', 'ParentA', 'ParentB', 'object']

--- Investigating Child_BA (ParentB, ParentA) ---
Speaking from ParentB
Child_BA's own common method
MRO for Child_BA: ['Child_BA', 'ParentB', 'ParentA', 'object']

출력 결과에서 다음 사항을 관찰할 수 있습니다.

  • child1.speak()Child_AB의 MRO 에서 ParentA가 먼저 오기 때문에 ParentA의 메서드를 호출합니다.
  • child2.speak()Child_BA의 MRO 에서 ParentB가 먼저 오기 때문에 ParentB의 메서드를 호출합니다.
  • child2.common_method()Child_BA에 직접 정의된 버전을 호출합니다. Python 이 부모 클래스를 확인하기 전에 여기서 먼저 찾기 때문입니다.

다중 상속 시나리오에서 동작을 예측하려면 MRO 를 이해하는 것이 매우 중요합니다.

요약

본 실습 (lab) 을 통해 Python 객체 지향 프로그래밍 (OOP) 의 네 가지 기본 개념에 대한 실습 경험을 쌓았습니다.

먼저 **캡슐화 (encapsulation)**부터 시작하여, 관례적으로 private 속성을 사용하여 클래스 데이터를 보호하고 접근을 위해 public 메서드를 제공하는 방법을 배웠습니다. 그런 다음 **상속 (inheritance)**을 사용하여 코드를 리팩토링하고, 부모 Animal 클래스를 생성하여 DogCat 서브클래스에서 코드 중복을 줄였습니다.

상속을 구현하는 과정에서 동일한 say() 메서드 호출에 DogCat 객체가 다르게 반응하는 것을 보며 **다형성 (polymorphism)**이 실제로 작동하는 것을 확인했습니다. 또한, 부모 클래스의 기능을 호출하고 확장하기 위해 super() 메서드를 사용하는 방법을 배웠으며, 이는 특히 __init__ 메서드 내에서 유용했습니다. 마지막으로 **다중 상속 (multiple inheritance)**과 메서드 결정 순서 (MRO) 가 어떤 부모 메서드가 호출될지 결정하는 데 얼마나 중요한지 살펴보았습니다.