카테고리 없음

패스트캠퍼스 환급챌린지 41일차: 데이터엔지니어링 초격차 강의 후기

Big Byte 2025. 5. 11. 22:23

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

파이썬 🐍! 객체지향 프로그래밍(OOP) 완전 정복: 현실 세계를 코드로 옮기는 마법! 🪄

 

안녕하세요, 여러분! ✨

지난 시간에는 파이썬 함수의 깊이 있는 세계로 떠나, 함수가 단순한 코드 묶음을 넘어 일급 객체(First-Class Citizen)로서 어떻게 변수에 할당되고, 다른 함수의 인자로 전달되며, 결과값으로 반환될 수 있는지 살펴보았습니다. 🥇 또한, 함수가 정의될 때의 환경을 기억하는 클로저(Closure) 🧠와 기존 코드를 수정하지 않고도 기능을 확장하는 데코레이터(Decorator) ✨라는 강력한 기법들까지! 🛠️✍️🎁 우리는 함수를 통해 코드의 유연성과 재사용성을 한층 끌어올리는 방법을 배웠습니다. 이제 함수를 자유자재로 다루며, 더욱 정교하고 효율적인 프로그램을 만들 준비가 되셨을 겁니다.

 

예를 들어, greet_person = versatile_decorator(greet_person)처럼 데코레이터를 활용해 함수의 동작을 풍부하게 만들거나, add_5 = outer_function(5)와 같이 클로저를 통해 상태를 기억하는 함수를 만들 수 있게 되었죠. 이러한 고급 함수 기법들은 복잡한 문제도 우아하게 해결할 수 있는 길을 열어주었습니다. 🧩

 

자, 이제 우리는 프로그래밍의 또 다른 거대한 패러다임, 바로 객체지향 프로그래밍(Object-Oriented Programming, OOP)의 세계로 한 걸음 더 나아가 볼 시간입니다! 🚀

 

함수가 코드의 '동작'을 조직화하는 데 중점을 두었다면, OOP는 '데이터'와 그 데이터를 처리하는 '행동'을 하나의 '객체'로 묶어, 현실 세계의 사물이나 개념을 더욱 직관적으로 모델링할 수 있게 해줍니다.

오늘 우리가 함께 탐험할 객체지향 프로그래밍의 핵심 요소들은 다음과 같습니다:

  • 객체지향 언어 (Object-Oriented Language)의 개념: OOP가 무엇인지, 왜 중요한지! 🤔
  • 클래스 (Class)와 인스턴스 (Instance): 설계도와 실제 제품! 🏠🧱
  • 메소드 (Method): 객체가 수행할 수 있는 행동들! 🏃‍♀️🗣️
  • 생성자 (Constructor): 객체가 세상에 태어나는 순간! __init__ 🍼
  • 상속 (Inheritance): 물려받고 확장하는 능력! 👨‍👩‍👧‍👦
  • 추상 클래스 (Abstract Class): 미완의 설계도, 구현을 위한 약속! 🏛️📐
  • 매직 메소드 (Magic Methods / Special Methods): 파이썬 객체의 숨겨진 비밀! __str__, __len__ 등 ✨🎩
  • 접근 제어자 (Access Modifiers): 소중한 데이터 보호하기! 🔒 (파이썬 스타일로!)

파이썬에서 객체지향 프로그래밍의 진수를 맛보고, 더욱 구조적이고 강력한 코드를 작성할 준비, 되셨나요? 함께 OOP의 세계로 빠져봅시다! 🧐

객체지향 프로그래밍 (Object-Oriented Programming, OOP) 이란? 🌍

객체지향 프로그래밍(OOP)은 실제 세계의 사물이나 개념을 객체(object)라는 단위로 표현하고, 이러한 객체들 간의 상호작용을 통해 프로그램을 설계하고 구현하는 프로그래밍 패러다임입니다. 각 객체는 자신만의 데이터(속성, attributes)와 그 데이터를 처리하는 행동(메소드, methods)을 가집니다.

예를 들어, '자동차'라는 객체는 '색상', '모델명', '현재 속도'와 같은 데이터를 가질 수 있고, '시동 걸기', '가속하기', '정지하기'와 같은 행동을 할 수 있습니다. OOP를 사용하면 이렇게 현실의 개념을 코드에 더 가깝게 반영할 수 있어, 프로그램의 이해도를 높이고 유지보수를 용이하게 만듭니다.

클래스 (Class)와 인스턴스 (Instance) 🏠🧱

클래스(Class)는 객체를 만들기 위한 '설계도' 또는 '틀'이라고 생각할 수 있습니다. 클래스에는 객체가 가져야 할 속성(데이터)과 메소드(행동)가 정의되어 있습니다. 빵틀이 빵의 모양을 결정하듯, 클래스는 객체의 구조와 기능을 결정합니다.

인스턴스(Instance)는 이 클래스라는 설계도를 바탕으로 실제로 메모리에 생성된 '객체' 그 자체를 의미합니다. 하나의 클래스로부터 여러 개의 인스턴스를 만들 수 있으며, 각 인스턴스는 고유한 상태(속성값)를 가질 수 있지만, 동일한 행동(메소드)을 공유합니다.

# 'Animal'이라는 이름의 클래스를 정의합니다.
class Animal:
    # 이 부분은 객체가 생성될 때 실행될 생성자입니다 (나중에 자세히 설명!).
    def __init__(self, name, sound):
        self.name = name    # 이 인스턴스의 'name' 속성
        self.sound = sound  # 이 인스턴스의 'sound' 속성

    # 'speak'라는 이름의 메소드를 정의합니다.
    def speak(self):
        return f"{self.name} says {self.sound}!"

# 'Animal' 클래스의 인스턴스 생성
dog = Animal("Buddy", "Woof") # Buddy라는 이름의 강아지 객체
cat = Animal("Lucy", "Meow")  # Lucy라는 이름의 고양이 객체

# 각 인스턴스의 메소드 호출
print(dog.speak())  # 출력: Buddy says Woof!
print(cat.speak())  # 출력: Lucy says Meow!

# 각 인스턴스는 고유한 속성값을 가집니다.
print(f"My dog's name is {dog.name}.") # 출력: My dog's name is Buddy.
print(f"My cat's sound is {cat.sound}.") # 출력: My cat's sound is Meow.

 

위 예제에서 Animal은 클래스이고, dogcatAnimal 클래스로부터 만들어진 인스턴스(객체)입니다. dogcat은 각자 다른 namesound 값을 가지지만, speak라는 동일한 행동 양식을 공유합니다.

메소드 (Method): 객체의 행동 🚶‍♀️🗣️

메소드(Method)는 클래스 내에 정의된 함수로, 해당 클래스의 인스턴스가 수행할 수 있는 행동이나 기능을 나타냅니다. 메소드는 항상 첫 번째 인자로 self를 받는데, self는 메소드가 호출된 인스턴스 자기 자신을 가리킵니다. 이를 통해 메소드는 해당 인스턴스의 속성에 접근하고 수정할 수 있습니다.

Animal 클래스의 speak 메소드가 좋은 예입니다.

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

    def speak(self): # 'self'는 이 메소드를 호출한 인스턴스를 의미합니다.
        # self.name은 해당 인스턴스의 name 속성을 참조합니다.
        return f"{self.name} says {self.sound}!"

dog.speak()가 호출될 때, speak 메소드 안의 selfdog 인스턴스를 가리키게 되어 dog.namedog.sound에 접근할 수 있습니다.

생성자 (Constructor): 객체의 탄생 __init__ 🍼

생성자(Constructor)는 클래스로부터 인스턴스가 생성될 때 자동으로 호출되는 특별한 메소드입니다. 파이썬에서는 __init__이라는 이름의 메소드가 생성자 역할을 합니다. 생성자의 주된 목적은 인스턴스가 처음 만들어질 때 필요한 초기 설정(주로 속성값 초기화)을 하는 것입니다.

class Car:
    # 생성자 메소드
    def __init__(self, brand, model, color="검정색"): # color는 기본값을 가집니다.
        print(f"{brand} {model} ({color}) 객체가 생성됩니다!")
        self.brand = brand      # 인스턴스 변수 brand 초기화
        self.model = model      # 인스턴스 변수 model 초기화
        self.color = color      # 인스턴스 변수 color 초기화
        self.is_running = False # 자동차의 초기 상태 (시동 꺼짐)

    def start_engine(self):
        if not self.is_running:
            self.is_running = True
            print(f"{self.brand} {self.model}의 시동이 걸렸습니다. 부릉부릉~")
        else:
            print("이미 시동이 걸려있습니다.")

# Car 클래스의 인스턴스 생성
my_car = Car("현대", "쏘나타", "흰색") # 출력: 현대 쏘나타 (흰색) 객체가 생성됩니다!
your_car = Car("기아", "K5")         # 출력: 기아 K5 (검정색) 객체가 생성됩니다! (color 기본값 사용)

print(my_car.brand)       # 출력: 현대
print(your_car.color)     # 출력: 검정색
my_car.start_engine()     # 출력: 현대 쏘나타의 시동이 걸렸습니다. 부릉부릉~

Car("현대", "쏘나타", "흰색") 코드가 실행되면, Car 클래스의 __init__ 메소드가 자동으로 호출되어 my_car 인스턴스의 brand, model, color, is_running 속성들이 초기화됩니다.

상속 (Inheritance): 코드 재사용의 마법 👨‍👩‍👧‍👦

상속(Inheritance)은 객체지향 프로그래밍의 중요한 특징 중 하나로, 기존 클래스(부모 클래스, 슈퍼 클래스)의 속성과 메소드를 물려받아 새로운 클래스(자식 클래스, 서브 클래스)를 정의할 수 있게 해줍니다. 이를 통해 코드의 중복을 줄이고, 계층적인 관계를 표현하며, 프로그램의 유지보수성을 높일 수 있습니다.

# 부모 클래스 (Animal)
class Animal:
    def __init__(self, name):
        self.name = name
        print(f"{self.name} (으)로 Animal 객체 생성")

    def speak(self):
        raise NotImplementedError("Subclass must implement abstract method") # 자식 클래스에서 구현하도록 강제

# 자식 클래스 (Dog) - Animal을 상속
class Dog(Animal): # 괄호 안에 부모 클래스를 명시
    def __init__(self, name, breed):
        super().__init__(name) # 부모 클래스의 __init__ 호출 (Animal의 name 초기화)
        self.breed = breed
        print(f"{self.name} (은)는 {self.breed} 품종의 Dog 객체로 생성")

    # 부모 클래스의 speak 메소드를 오버라이딩 (재정의)
    def speak(self):
        return f"{self.name} says Woof!"

# 자식 클래스 (Cat) - Animal을 상속
class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name) # 부모 클래스의 __init__ 호출
        self.color = color
        print(f"{self.name} (은)는 {self.color}색 Cat 객체로 생성")

    def speak(self):
        return f"{self.name} says Meow!"

    def purr(self): # Cat 클래스만의 고유한 메소드
        return f"{self.name} is purring... Rrrrr..."

my_dog = Dog("레오", "골든 리트리버")
# 출력:
# 레오 (으)로 Animal 객체 생성
# 레오 (은)는 골든 리트리버 품종의 Dog 객체로 생성

my_cat = Cat("나비", "삼색이")
# 출력:
# 나비 (으)로 Animal 객체 생성
# 나비 (은)는 삼색이색 Cat 객체로 생성

print(my_dog.speak())  # 출력: 레오 says Woof! (Dog 클래스의 speak)
print(my_cat.speak())  # 출력: 나비 says Meow! (Cat 클래스의 speak)
print(my_cat.purr())   # 출력: 나비 is purring... Rrrrr...

 

DogCat 클래스는 Animal 클래스로부터 name 속성과 __init__ 메소드의 일부 기능을 상속받았습니다. super().__init__(name)은 부모 클래스의 생성자를 호출하는 방법입니다. 또한, 각 자식 클래스는 speak 메소드를 자신에게 맞게 오버라이딩(재정의) 했으며, Cat 클래스는 purr라는 자신만의 메소드를 추가했습니다.

추상 클래스 (Abstract Class)와 추상 메소드 (Abstract Method) 🏛️📐

 

추상 클래스(Abstract Class)는 미완성된 설계도와 같습니다. 자체적으로 인스턴스를 만들 수 없으며, 다른 클래스가 상속받아 특정 메소드들을 반드시 구현하도록 강제하는 역할을 합니다. 이는 여러 서브 클래스들이 공통된 인터페이스를 가지도록 보장하는 데 유용합니다.

추상 메소드(Abstract Method)는 추상 클래스 내에 선언만 되어 있고 실제 구현 내용은 없는 메소드입니다. 추상 클래스를 상속받는 자식 클래스는 반드시 이 추상 메소드를 구현(오버라이딩)해야 합니다.

파이썬에서는 abc (Abstract Base Classes) 모듈을 사용하여 추상 클래스와 추상 메소드를 만듭니다.

from abc import ABC, abstractmethod

class Shape(ABC): # ABC를 상속받아 추상 클래스로 만듦
    def __init__(self, name):
        self.name = name

    @abstractmethod # 이 데코레이터를 사용해 추상 메소드로 지정
    def area(self):
        pass # 구현 내용은 없음

    def describe(self):
        return f"This is a shape named {self.name}."

# Shape 추상 클래스를 상속받는 Circle 클래스
class Circle(Shape):
    def __init__(self, name, radius):
        super().__init__(name)
        self.radius = radius

    # 추상 메소드인 area를 반드시 구현해야 함
    def area(self):
        return 3.14159 * self.radius * self.radius

# Shape 추상 클래스를 상속받는 Rectangle 클래스
class Rectangle(Shape):
    def __init__(self, name, width, height):
        super().__init__(name)
        self.width = width
        self.height = height

    # 추상 메소드인 area를 반드시 구현해야 함
    def area(self):
        return self.width * self.height

# s = Shape("AbstractShape") # 오류! 추상 클래스는 인스턴스화할 수 없음 (TypeError)

circle = Circle("MyCircle", 5)
rectangle = Rectangle("MyRectangle", 4, 6)

print(circle.describe())       # 출력: This is a shape named MyCircle.
print(f"Circle Area: {circle.area()}") # 출력: Circle Area: 78.53975

print(rectangle.describe())    # 출력: This is a shape named MyRectangle.
print(f"Rectangle Area: {rectangle.area()}") # 출력: Rectangle Area: 24

Shape 클래스는 area라는 추상 메소드를 가지고 있기 때문에, Shape를 상속하는 CircleRectangle은 반드시 area 메소드를 자신만의 방식으로 구현해야 합니다. 만약 구현하지 않으면 TypeError가 발생합니다.

매직 메소드 (Magic Methods / Special Methods): 파이썬 객체의 숨겨진 힘 ✨🎩

매직 메소드(Magic Methods) 또는 특수 메소드(Special Methods)는 파이썬에서 특별한 의미를 가지는, 더블 언더스코어(__)로 시작하고 끝나는 이름의 메소드들입니다. (그래서 "던더 메소드(dunder methods)"라고도 불립니다.) 이 메소드들은 직접 호출하기보다는, 파이썬의 내장 함수나 연산자를 사용할 때 간접적으로 호출됩니다. 매직 메소드를 클래스에 구현함으로써, 우리가 만든 객체가 파이썬의 기본 문법과 자연스럽게 통합되도록 할 수 있습니다.

대표적인 매직 메소드들은 다음과 같습니다:

  • __init__(self, ...): 생성자, 객체 초기화.
  • __str__(self): str(object) 또는 print(object) 호출 시 객체의 "공식적이지 않은" 문자열 표현을 반환. 사용자가 보기 편한 형태로 만듭니다.
  • __repr__(self): repr(object) 호출 시 객체의 "공식적인" 문자열 표현을 반환. 주로 개발자가 디버깅하거나, 객체를 명확히 식별할 수 있는 형태(가능하면 객체를 다시 생성할 수 있는 코드 형태)로 만듭니다. __str__이 정의되지 않으면 print__repr__을 사용합니다.
  • __len__(self): len(object) 호출 시 객체의 길이를 반환.
  • __add__(self, other): object1 + object2 연산 시 호출.
  • __eq__(self, other): object1 == object2 비교 연산 시 호출.
class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages

    def __str__(self): # print() 함수나 str() 내장 함수에 사용될 문자열
        return f"『{self.title}』 by {self.author} ({self.pages} pages)"

    def __repr__(self): # repr() 함수나 개발자용 표현에 사용될 문자열
        return f"Book(title='{self.title}', author='{self.author}', pages={self.pages})"

    def __len__(self): # len() 함수에 사용될 값
        return self.pages

    def __eq__(self, other): # '==' 연산자 오버로딩
        if isinstance(other, Book): # 같은 Book 타입인지 확인
            return (self.title == other.title and
                    self.author == other.author and
                    self.pages == other.pages)
        return False


book1 = Book("The Little Prince", "Antoine de Saint-Exupéry", 100)
book2 = Book("The Little Prince", "Antoine de Saint-Exupéry", 100)
book3 = Book("Demian", "Hermann Hesse", 250)

print(str(book1))  # __str__ 호출: 『The Little Prince』 by Antoine de Saint-Exupéry (100 pages)
print(book1)       # __str__ 호출 (print는 내부적으로 str을 사용)
print(repr(book1)) # __repr__ 호출: Book(title='The Little Prince', author='Antoine de Saint-Exupéry', pages=100)
print(f"Length of book1: {len(book1)}") # __len__ 호출: Length of book1: 100

print(f"book1 == book2: {book1 == book2}") # __eq__ 호출: book1 == book2: True
print(f"book1 == book3: {book1 == book3}") # __eq__ 호출: book1 == book3: False

매직 메소드를 잘 활용하면 우리가 만든 커스텀 객체도 파이썬의 기본 타입처럼 자연스럽게 다룰 수 있게 됩니다!

접근 제어자 (Access Modifiers): 정보 은닉의 기초 🔒 (파이썬 스타일)

접근 제어자(Access Modifier)는 클래스의 속성이나 메소드에 대한 외부로부터의 접근 수준을 제어하는 키워드입니다. (예: public, protected, private) 이를 통해 정보 은닉(information hiding)을 구현하여 객체의 내부 상태를 보호하고, 의도치 않은 변경을 막으며, 클래스의 인터페이스를 명확히 할 수 있습니다.

하지만, 파이썬은 "We are all consenting adults here." (우리는 모두 동의한 성인이다)라는 철학을 가지고 있어, C++이나 Java처럼 엄격한 의미의 접근 제어자를 제공하지는 않습니다. 대신, 이름 규칙을 통해 접근 수준에 대한 힌트를 줍니다.

  1. Public: 기본적으로 모든 속성과 메소드는 public입니다. 어디서든 접근 가능합니다.
    class MyClass:
        def __init__(self):
            self.public_var = "I am public"
    obj = MyClass()
    print(obj.public_var) # 접근 가능
  2. Protected (관례적): 속성/메소드 이름 앞에 밑줄 하나(_)를 붙입니다. (_protected_var)
    • "이것은 내부적으로 사용되지만, 정말 필요하다면 외부에서 접근해도 괜찮아. 하지만 조심해서 써!"라는 의미입니다.
    • 클래스 외부나 상속받은 자식 클래스에서 접근은 가능하지만, 직접 접근하는 것은 권장되지 않습니다.
    class MyClass:
        def __init__(self):
            self._protected_var = "I am protected (by convention)"
    obj = MyClass()
    print(obj._protected_var) # 기술적으로는 접근 가능하지만, 사용에 주의해야 함
  3. Private (이름 장식, Name Mangling): 속성/메소드 이름 앞에 밑줄 두 개(__)를 붙입니다. (__private_var)
    • "이것은 이 클래스 내부에서만 사용되어야 해!"라는 강력한 힌트입니다.
    • 파이썬은 이 이름을 _ClassName__private_var 형태로 변경하여 (이를 이름 장식(name mangling) 이라 함) 외부에서 일반적인 방법으로는 접근하기 어렵게 만듭니다.
    • 하지만 여전히 obj._ClassName__private_var와 같이 변경된 이름으로 접근은 가능합니다. 엄격한 의미의 private은 아닙니다.
    class MyClass:
        def __init__(self):
            self.__private_var = "I am 'private' (name mangled)"
    
        def get_private_var(self): # getter 메소드를 통해 간접적으로 접근
            return self.__private_var
    
    obj = MyClass()
    # print(obj.__private_var) # AttributeError: 'MyClass' object has no attribute '__private_var'
    print(obj.get_private_var()) # getter를 통해 접근: I am 'private' (name mangled)
    print(obj._MyClass__private_var) # 이름 장식된 형태로 접근 가능: I am 'private' (name mangled)

파이썬에서는 주로 public 인터페이스를 명확히 제공하고, 내부 구현에 해당하는 부분은 ___를 사용하여 개발자에게 힌트를 주는 방식을 사용합니다.

정리하며 📝

오늘은 객체지향 프로그래밍(OOP)의 세계로 첫발을 내디뎠습니다. 🚀 OOP는 현실 세계를 모델링하고, 코드의 재사용성과 유지보수성을 높이는 강력한 패러다임입니다.

  • 객체지향 언어 (OOP) 🌍: 데이터와 행동을 '객체' 단위로 묶어 생각하는 방식.
  • 클래스 (Class)와 인스턴스 (Instance) 🏠🧱: 객체를 만들기 위한 설계도(클래스)와 그 설계도로 만들어진 실체(인스턴스).
  • 메소드 (Method) 🏃‍♀️🗣️: 클래스 내에 정의되어 인스턴스가 수행하는 행동, self를 통해 인스턴스 자신에게 접근.
  • 생성자 (Constructor) __init__ 🍼: 인스턴스가 생성될 때 속성을 초기화하는 특별한 메소드.
  • 상속 (Inheritance) 👨‍👩‍👧‍👦: 부모 클래스의 특징을 물려받아 코드를 재사용하고 확장. super()로 부모 클래스 접근.
  • 추상 클래스 (Abstract Class) 🏛️📐: 인스턴스화될 수 없으며, 자식 클래스에게 특정 메소드의 구현을 강제하는 뼈대. (abc 모듈)
  • 매직 메소드 (Magic Methods) ✨🎩: __str__, __len__ 등 더블 언더스코어로 감싸진 특수 메소드로, 파이썬 내장 기능과 객체를 통합.
  • 접근 제어자 (Access Modifiers) 🔒: _protected (관례), __private (이름 장식) 등 파이썬 스타일의 정보 은닉 기법.

지난 시간 함수를 통해 코드의 동작을 구조화했다면, 오늘은 OOP를 통해 데이터와 그 데이터를 다루는 동작을 하나로 묶어 더욱 체계적으로 프로그램을 구성하는 방법을 배웠습니다. 클래스를 설계하고, 객체를 생성하며, 상속을 통해 기능을 확장하는 과정은 마치 레고 블록으로 복잡한 구조물을 만드는 것과 같습니다. 처음에는 다소 생소하고 복잡하게 느껴질 수 있지만, 익숙해질수록 코드의 가독성과 재사용성이 크게 향상되는 것을 경험하실 수 있을 겁니다.

 

이러한 OOP의 개념들은 여러분이 앞으로 만들게 될 더 크고 복잡한 애플리케이션의 뼈대가 되어줄 것입니다! 🌟

 

 

오늘도 정말 수고 많으셨습니다! 💪 객체지향의 세계는 깊고 넓지만, 그만큼 강력한 도구를 여러분께 선사할 거예요. 다음 시간에는 또 어떤 흥미로운 파이썬의 기능들이 우리를 기다리고 있을지 기대해주세 요! 그때까지 즐거운 코딩 여정 이어가시길 바랍니다! 👋

https://abit.ly/lisbva