이미지 로딩 중...

객체지향 프로그래밍 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 21. · 13 Views

객체지향 프로그래밍 완벽 가이드

초급 개발자를 위한 객체지향 프로그래밍(OOP)의 핵심 개념을 쉽고 친근하게 설명합니다. 클래스, 상속, 캡슐화 등 실무에서 바로 활용할 수 있는 OOP의 모든 것을 다룹니다.


목차

  1. 클래스와 객체 - 프로그래밍의 설계도
  2. 상속 - 부모의 능력을 물려받기
  3. 캡슐화 - 소중한 정보 숨기기
  4. 다형성 - 같은 이름, 다른 동작
  5. 추상 클래스 - 설계의 규칙 만들기
  6. 생성자와 소멸자 - 객체의 탄생과 소멸
  7. 정적 메서드와 클래스 메서드 - 객체 없이 사용하기
  8. 컴포지션 - 조립해서 만들기
  9. 프로퍼티 - 똑똑한 속성 만들기
  10. 매직 메서드 - 특별한 기능 추가하기

1. 클래스와 객체 - 프로그래밍의 설계도

시작하며

여러분이 레고 블록을 가지고 집을 만든다고 상상해보세요. 똑같은 모양의 집을 여러 개 만들려면 어떻게 해야 할까요?

매번 처음부터 블록을 하나하나 조립하는 것은 정말 힘들겠죠. 프로그래밍도 마찬가지입니다.

같은 기능을 가진 코드를 매번 새로 작성하면 시간도 오래 걸리고 실수도 많이 생깁니다. 예를 들어, 게임에서 100명의 캐릭터를 만든다면 각 캐릭터의 이름, 체력, 공격력을 일일이 관리해야 합니다.

바로 이럴 때 필요한 것이 클래스와 객체입니다. 클래스는 설계도처럼 한 번만 만들어두면, 그 설계도로 똑같은 형태의 객체를 무한히 만들어낼 수 있습니다.

개요

간단히 말해서, 클래스는 붕어빵 틀이고 객체는 그 틀로 찍어낸 붕어빵입니다. 클래스를 만들어두면 비슷한 특성을 가진 데이터를 체계적으로 관리할 수 있습니다.

예를 들어, 온라인 쇼핑몰에서 수천 개의 상품을 관리할 때 각 상품의 이름, 가격, 재고를 일일이 변수로 만들면 코드가 엉망이 됩니다. 하지만 Product 클래스를 만들면 깔끔하게 정리됩니다.

기존에는 변수 100개를 만들어서 관리했다면, 이제는 클래스 1개로 객체 100개를 쉽게 만들 수 있습니다. 클래스의 핵심 특징은 첫째, 데이터와 기능을 하나로 묶을 수 있고, 둘째, 재사용이 가능하며, 셋째, 코드의 구조가 명확해진다는 점입니다.

이러한 특징들이 코드를 훨씬 읽기 쉽고 유지보수하기 좋게 만들어줍니다.

코드 예제

# 학생 클래스 정의 - 학생의 설계도
class Student:
    # 초기화 함수: 학생 객체를 만들 때 실행됩니다
    def __init__(self, name, age, grade):
        self.name = name      # 이름 저장
        self.age = age        # 나이 저장
        self.grade = grade    # 학년 저장

    # 학생 정보를 출력하는 기능
    def introduce(self):
        return f"안녕하세요! 저는 {self.grade}학년 {self.name}이고, {self.age}살입니다."

# 클래스로 학생 객체 3개 만들기
student1 = Student("김철수", 15, 2)
student2 = Student("이영희", 16, 3)
student3 = Student("박민수", 14, 1)

# 각 학생이 자기소개하기
print(student1.introduce())

설명

이것이 하는 일: 학생이라는 개념을 코드로 표현하고, 그 설계도를 바탕으로 실제 학생 3명을 만듭니다. 첫 번째로, class Student:는 학생이라는 새로운 타입을 정의합니다.

마치 새로운 단어를 사전에 등록하는 것과 같습니다. __init__ 함수는 객체가 처음 만들어질 때 자동으로 실행되어 학생의 이름, 나이, 학년을 저장합니다.

이것을 "생성자"라고 부르는데, 객체의 초기 상태를 설정하는 역할을 합니다. 그 다음으로, introduce 함수가 실행되면서 학생이 자기소개를 할 수 있게 됩니다.

self는 "나 자신"을 의미하는데, 각 학생 객체가 자기 자신의 정보를 참조할 때 사용합니다. student1이 introduce를 호출하면 student1의 정보가, student2가 호출하면 student2의 정보가 사용됩니다.

마지막으로, Student("김철수", 15, 2)를 실행하면 Student 클래스의 설계도를 바탕으로 실제 학생 객체가 메모리에 만들어집니다. 이렇게 만들어진 student1, student2, student3는 각각 독립적인 객체로, 자신만의 이름, 나이, 학년 정보를 가지고 있습니다.

여러분이 이 코드를 사용하면 학생 100명이든 1000명이든 똑같은 방식으로 쉽게 관리할 수 있습니다. 각 학생의 정보를 변경하거나 새로운 학생을 추가하는 것도 매우 간단해집니다.

또한 나중에 학생에게 새로운 기능(예: 시험 점수 계산)을 추가하고 싶을 때도 클래스만 수정하면 모든 학생 객체에 자동으로 적용됩니다.

실전 팁

💡 클래스 이름은 항상 대문자로 시작하세요(Student, Product). 이것은 전 세계 개발자들이 따르는 약속입니다.

💡 __init__의 첫 번째 매개변수는 반드시 self여야 합니다. 이것을 빼먹으면 에러가 발생해요.

💡 객체를 만들 때는 () 안에 필요한 정보를 순서대로 넣어주세요. 순서가 바뀌면 엉뚱한 값이 저장됩니다.

💡 한 클래스에서 만든 객체들은 서로 영향을 주지 않습니다. student1의 나이를 바꿔도 student2의 나이는 그대로입니다.

💡 처음에는 간단한 클래스부터 만들어보세요. 게임 캐릭터, 도서 정보, 자동차 같은 일상적인 개념으로 연습하면 이해가 빨라집니다.


2. 상속 - 부모의 능력을 물려받기

시작하며

여러분이 스마트폰 앱을 만든다고 생각해보세요. 버튼, 텍스트 입력창, 이미지 등 화면에 보이는 모든 요소는 기본적으로 "화면에 표시된다", "클릭할 수 있다" 같은 공통 기능을 가지고 있습니다.

이런 공통 기능을 매번 각 요소마다 새로 작성한다면 같은 코드를 수십 번 복사해야 합니다. 코드도 길어지고, 나중에 버그를 고칠 때도 모든 곳을 다 찾아다니며 수정해야 합니다.

바로 이럴 때 필요한 것이 상속입니다. 부모 클래스에 공통 기능을 정의하고, 자식 클래스가 그것을 물려받아 사용하면서 자신만의 특별한 기능을 추가할 수 있습니다.

개요

간단히 말해서, 상속은 부모가 자식에게 재산을 물려주듯이 부모 클래스의 기능을 자식 클래스가 물려받는 것입니다. 상속을 사용하면 코드 중복을 극적으로 줄일 수 있습니다.

예를 들어, 동물원 관리 시스템을 만들 때 강아지, 고양이, 새는 모두 "이름이 있다", "나이가 있다", "소리를 낸다" 같은 공통점이 있습니다. Animal이라는 부모 클래스를 만들고 이런 공통 기능을 넣으면, Dog, Cat, Bird 클래스는 각자의 특별한 기능만 추가하면 됩니다.

기존에는 각 동물마다 이름, 나이 변수와 기능을 따로따로 만들었다면, 이제는 한 번만 정의하고 모두가 공유할 수 있습니다. 상속의 핵심 특징은 첫째, 코드 재사용성이 높아지고, 둘째, 계층 구조로 코드를 체계적으로 관리할 수 있으며, 셋째, 공통 기능을 한 곳에서만 수정하면 모든 자식 클래스에 자동으로 적용된다는 점입니다.

이러한 특징들이 대규모 프로젝트에서 코드를 효율적으로 관리할 수 있게 해줍니다.

코드 예제

# 부모 클래스: 모든 동물의 공통 특징
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # 모든 동물이 가진 공통 기능
    def eat(self):
        return f"{self.name}이(가) 밥을 먹습니다."

# 자식 클래스: 강아지는 Animal의 모든 것을 물려받음
class Dog(Animal):
    # 강아지만의 특별한 기능 추가
    def bark(self):
        return f"{self.name}: 멍멍!"

# 자식 클래스: 고양이도 Animal을 상속
class Cat(Animal):
    def meow(self):
        return f"{self.name}: 야옹~"

# 각 동물 만들기
dog = Dog("바둑이", 3)
cat = Cat("나비", 2)

print(dog.eat())   # 부모에게 물려받은 기능
print(dog.bark())  # 강아지만의 기능
print(cat.meow())  # 고양이만의 기능

설명

이것이 하는 일: 동물의 공통 기능을 부모 클래스에 정의하고, 각 동물 종류는 그것을 상속받아 자신만의 특별한 기능을 추가합니다. 첫 번째로, class Animal:에서 모든 동물이 공통으로 가지는 이름, 나이, 먹는 기능을 정의합니다.

이것은 마치 "동물"이라는 큰 카테고리를 만드는 것과 같습니다. 이렇게 부모 클래스를 먼저 만들어두면 나중에 새로운 동물을 추가할 때 공통 부분을 다시 작성할 필요가 없습니다.

그 다음으로, class Dog(Animal):에서 괄호 안에 Animal을 넣으면 Dog가 Animal의 모든 것을 물려받습니다. 이것을 "상속받는다"고 표현합니다.

Dog 클래스에는 __init__이나 eat 함수가 없지만, 부모인 Animal에 있기 때문에 자동으로 사용할 수 있습니다. 그리고 bark 같은 강아지만의 특별한 기능을 추가로 정의할 수 있습니다.

마지막으로, dog = Dog("바둑이", 3)를 실행하면 Dog 객체가 만들어지면서 부모의 __init__이 자동으로 호출되어 이름과 나이가 저장됩니다. 이제 dog 객체는 부모에게서 물려받은 eat() 기능과 자신만의 bark() 기능을 모두 사용할 수 있습니다.

여러분이 이 코드를 사용하면 새로운 동물을 추가할 때 매우 간단해집니다. 예를 들어 Bird 클래스를 만들고 싶다면 class Bird(Animal):만 작성하고 fly() 같은 새만의 기능만 추가하면 됩니다.

또한 모든 동물의 공통 기능을 수정할 때도 Animal 클래스만 고치면 Dog, Cat 등 모든 자식 클래스에 자동으로 적용됩니다.

실전 팁

💡 상속할 때는 class 자식(부모): 형식으로 괄호 안에 부모 클래스 이름을 넣어주세요.

💡 자식 클래스는 부모의 모든 기능을 사용할 수 있지만, 부모는 자식의 기능을 사용할 수 없습니다. 일방통행입니다!

💡 같은 이름의 함수를 자식 클래스에서 다시 정의하면 부모의 함수를 덮어씁니다. 이것을 "오버라이딩"이라고 해요.

💡 여러 단계로 상속할 수 있습니다. 예: Animal → Mammal → Dog처럼 할아버지-아버지-손자 관계도 가능해요.

💡 처음에는 "is-a" 관계인지 확인하세요. "강아지는 동물이다(Dog is an Animal)"가 맞으면 상속을 사용하는 것이 적합합니다.


3. 캡슐화 - 소중한 정보 숨기기

시작하며

여러분이 ATM에서 돈을 뽑을 때를 생각해보세요. 그냥 금액을 입력하고 버튼을 누르면 돈이 나옵니다.

ATM 내부에서 어떤 복잡한 계산과 처리가 일어나는지 우리는 알 필요가 없죠. 프로그래밍도 마찬가지입니다.

은행 계좌 클래스를 만들 때 잔액을 아무나 마음대로 바꿀 수 있다면 큰 문제가 생깁니다. 누군가 실수로 또는 고의로 잔액을 1억 원으로 바꿔버릴 수 있기 때문입니다.

바로 이럴 때 필요한 것이 캡슐화입니다. 중요한 데이터는 외부에서 직접 접근하지 못하게 숨기고, 안전한 방법(함수)을 통해서만 접근할 수 있게 만듭니다.

개요

간단히 말해서, 캡슐화는 중요한 정보를 상자 안에 넣고 자물쇠를 채우는 것입니다. 열쇠(함수)를 통해서만 안전하게 접근할 수 있습니다.

캡슐화를 사용하면 데이터의 무결성을 보장할 수 있습니다. 예를 들어, 게임 캐릭터의 체력을 관리할 때 체력이 음수가 되면 안 되고, 최대 체력을 초과해서도 안 됩니다.

외부에서 직접 값을 바꿀 수 있다면 이런 규칙을 강제할 수 없지만, 캡슐화를 사용하면 체력을 바꾸는 함수 안에서 검증 로직을 넣을 수 있습니다. 기존에는 변수를 public으로 선언해서 아무나 접근할 수 있었다면, 이제는 private으로 숨기고 getter/setter 함수를 통해서만 안전하게 접근할 수 있습니다.

캡슐화의 핵심 특징은 첫째, 데이터 보호로 잘못된 값이 들어가는 것을 방지하고, 둘째, 내부 구현을 숨겨서 나중에 내부 로직을 바꿔도 외부 코드에 영향을 주지 않으며, 셋째, 코드의 유지보수성이 크게 향상된다는 점입니다. 이러한 특징들이 안정적인 프로그램을 만드는 데 필수적입니다.

코드 예제

# 은행 계좌 클래스
class BankAccount:
    def __init__(self, owner, initial_balance):
        self.owner = owner
        self.__balance = initial_balance  # __를 붙이면 private 변수

    # 잔액 확인 함수 (읽기만 가능)
    def get_balance(self):
        return f"{self.owner}님의 잔액: {self.__balance}원"

    # 입금 함수 (안전하게 값 변경)
    def deposit(self, amount):
        if amount > 0:  # 양수만 입금 가능
            self.__balance += amount
            return f"{amount}원 입금 완료!"
        return "입금액은 0보다 커야 합니다."

    # 출금 함수 (검증 후 값 변경)
    def withdraw(self, amount):
        if amount > self.__balance:
            return "잔액이 부족합니다!"
        if amount > 0:
            self.__balance -= amount
            return f"{amount}원 출금 완료!"
        return "출금액은 0보다 커야 합니다."

# 계좌 만들기
account = BankAccount("김철수", 10000)
print(account.get_balance())
print(account.deposit(5000))
print(account.withdraw(3000))
# print(account.__balance)  # 에러! 직접 접근 불가

설명

이것이 하는 일: 은행 계좌의 잔액을 외부에서 직접 수정하지 못하게 보호하고, 입금과 출금 함수를 통해서만 안전하게 변경할 수 있게 합니다. 첫 번째로, self.__balance에서 변수 이름 앞에 __(언더스코어 2개)를 붙이면 이것은 "private" 변수가 됩니다.

외부에서 account.__balance로 직접 접근하려고 하면 에러가 발생합니다. 이것은 마치 금고에 자물쇠를 채우는 것과 같습니다.

이렇게 하면 누군가 실수로 잔액을 잘못된 값으로 바꾸는 것을 원천적으로 차단할 수 있습니다. 그 다음으로, get_balance(), deposit(), withdraw() 같은 public 함수들이 실행됩니다.

이 함수들은 금고의 열쇠 역할을 합니다. deposit 함수를 보면 if amount > 0: 조건으로 음수 입금을 막고, withdraw 함수는 if amount > self.__balance: 조건으로 잔액보다 많은 돈을 출금하는 것을 막습니다.

이런 검증 로직이 데이터의 안전성을 보장합니다. 마지막으로, 사용자는 account.deposit(5000)처럼 함수를 통해서만 잔액을 변경할 수 있습니다.

함수 내부에서 모든 검증이 이루어지기 때문에 잘못된 값이 들어갈 가능성이 0%입니다. 또한 나중에 입금 시 수수료를 추가하거나 거래 내역을 로그로 남기는 기능을 추가할 때도 함수만 수정하면 되므로 매우 편리합니다.

여러분이 이 코드를 사용하면 데이터 무결성을 완벽하게 보장할 수 있습니다. 잔액이 음수가 되거나, 한도를 초과하거나, 이상한 값이 들어가는 일이 절대 발생하지 않습니다.

또한 코드를 읽는 다른 개발자들도 "아, 이 변수는 직접 건드리면 안 되겠구나"라고 바로 알 수 있어서 협업할 때도 안전합니다.

실전 팁

💡 Python에서는 변수명 앞에 __를 붙이면 private, _ 하나면 "건드리지 말아주세요"라는 약속입니다.

💡 getter는 값을 읽기만 하고, setter는 값을 변경합니다. setter에는 반드시 검증 로직을 넣으세요.

💡 모든 변수를 private으로 만들 필요는 없습니다. 진짜 보호가 필요한 중요한 데이터만 숨기세요.

💡 캡슐화를 하면 나중에 내부 구현을 바꿔도 외부 코드는 수정할 필요가 없습니다. 유지보수가 훨씬 쉬워져요.

💡 실무에서는 금액, 비밀번호, 개인정보 같은 민감한 데이터는 반드시 캡슐화해야 합니다.


4. 다형성 - 같은 이름, 다른 동작

시작하며

여러분이 리모컨의 "재생" 버튼을 누른다고 생각해보세요. TV에서는 드라마가 재생되고, CD 플레이어에서는 음악이 재생되고, DVD 플레이어에서는 영화가 재생됩니다.

같은 "재생" 버튼이지만 기기마다 다른 동작을 하죠. 프로그래밍에서도 같은 함수 이름으로 다른 동작을 하게 만들 수 있다면 매우 편리합니다.

예를 들어, 도형을 그리는 프로그램에서 원, 사각형, 삼각형 모두 draw()라는 이름으로 그릴 수 있다면 코드가 훨씬 직관적이 됩니다. 바로 이럴 때 필요한 것이 다형성입니다.

같은 인터페이스(함수 이름)를 사용하면서 각 객체의 타입에 따라 다른 동작을 하게 만들 수 있습니다.

개요

간단히 말해서, 다형성은 같은 이름의 함수가 객체에 따라 다르게 동작하는 것입니다. 마치 만능 리모컨처럼요.

다형성을 사용하면 코드의 유연성이 크게 향상됩니다. 예를 들어, 동물원에서 모든 동물을 배열에 담아 한 번에 처리할 때, 각 동물마다 speak() 함수를 호출하면 강아지는 "멍멍", 고양이는 "야옹", 새는 "짹짹" 하고 자동으로 다른 소리를 냅니다.

일일이 타입을 체크할 필요가 없습니다. 기존에는 if문으로 타입을 확인해서 각각 다른 함수를 호출했다면, 이제는 같은 함수 이름으로 자동으로 적절한 동작이 실행됩니다.

다형성의 핵심 특징은 첫째, 코드의 일관성이 높아지고, 둘째, 새로운 타입을 추가할 때 기존 코드를 수정할 필요가 없으며, 셋째, 인터페이스가 단순해져서 사용하기 쉽다는 점입니다. 이러한 특징들이 확장 가능한 프로그램을 만드는 데 핵심적인 역할을 합니다.

코드 예제

# 부모 클래스에 공통 인터페이스 정의
class Shape:
    def area(self):
        pass  # 자식 클래스에서 구현할 것

# 원 클래스
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    # 같은 이름이지만 원의 넓이 계산
    def area(self):
        return 3.14 * self.radius ** 2

# 사각형 클래스
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    # 같은 이름이지만 사각형의 넓이 계산
    def area(self):
        return self.width * self.height

# 여러 도형을 리스트에 담기
shapes = [Circle(5), Rectangle(4, 6), Circle(3)]

# 같은 함수 이름으로 다른 계산이 실행됨!
for shape in shapes:
    print(f"넓이: {shape.area()}")  # 각자의 방식으로 계산

설명

이것이 하는 일: 여러 종류의 도형을 하나의 리스트에 담아서, 같은 area() 함수로 각 도형의 넓이를 다르게 계산합니다. 첫 번째로, class Shape:에서 모든 도형이 공통으로 가져야 할 area() 함수를 정의합니다.

여기서는 실제 구현 없이 pass만 써두는데, 이것은 "자식 클래스가 반드시 이 함수를 구현해야 한다"는 약속입니다. 이렇게 하면 모든 도형이 area() 함수를 가지도록 강제할 수 있습니다.

그 다음으로, Circle과 Rectangle 클래스가 각각 area() 함수를 자신만의 방식으로 구현합니다. 원은 3.14 * 반지름²로 계산하고, 사각형은 가로 * 세로로 계산합니다.

같은 함수 이름이지만 내부 로직은 완전히 다릅니다. 이것을 "메서드 오버라이딩"이라고 합니다.

마지막으로, for shape in shapes: 반복문에서 shape.area()를 호출할 때 마법이 일어납니다. Python은 자동으로 shape의 실제 타입을 확인해서 Circle이면 Circle의 area()를, Rectangle이면 Rectangle의 area()를 호출합니다.

우리는 타입을 확인하는 if문을 전혀 쓰지 않았지만, 각각 올바른 계산이 실행됩니다. 여러분이 이 코드를 사용하면 새로운 도형을 추가할 때 기존 코드를 전혀 수정하지 않아도 됩니다.

예를 들어 Triangle 클래스를 만들어서 area() 함수만 구현하면, shapes 리스트에 추가하는 것만으로 자동으로 작동합니다. 이것이 "개방-폐쇄 원칙"이라고 불리는 중요한 설계 원칙입니다.

실전 팁

💡 다형성을 사용하려면 부모 클래스에 공통 함수를 먼저 정의하고, 자식 클래스에서 오버라이딩하세요.

💡 타입 체크 if문이 많이 보인다면 다형성으로 개선할 수 있는지 고민해보세요. 코드가 훨씬 깔끔해집니다.

💡 리스트에 여러 타입의 객체를 담을 때 다형성의 위력이 진짜로 나타납니다. 반복문 하나로 모든 객체를 처리할 수 있어요.

💡 Python의 "duck typing": "오리처럼 걷고 오리처럼 꽥꽥거리면 오리다." 같은 함수만 있으면 상속 없이도 다형성을 사용할 수 있습니다.

💡 실무에서는 결제 방법(카드, 계좌이체, 간편결제), 파일 저장(로컬, 클라우드, DB) 같은 곳에서 다형성을 자주 사용합니다.


5. 추상 클래스 - 설계의 규칙 만들기

시작하며

여러분이 게임 개발 팀의 리더라고 상상해보세요. 여러 개발자가 각자 다른 적 캐릭터를 만드는데, 누구는 attack() 함수를 만들고, 누구는 hit() 함수를 만들고, 누구는 공격 기능을 아예 안 만들었습니다.

이렇게 되면 나중에 모든 적 캐릭터를 관리할 때 큰 혼란이 생깁니다. 각자 다른 함수 이름을 사용하니까 통일된 방식으로 처리할 수 없기 때문입니다.

바로 이럴 때 필요한 것이 추상 클래스입니다. "모든 적 캐릭터는 반드시 attack(), defend(), move() 함수를 구현해야 한다"는 규칙을 정해놓으면, 팀원들이 이 규칙을 따라 개발하게 됩니다.

개요

간단히 말해서, 추상 클래스는 "반드시 구현해야 할 함수 목록"을 정의한 설계도입니다. 설계도만 있고 실제 구현은 자식 클래스가 합니다.

추상 클래스를 사용하면 팀 협업 시 일관성을 보장할 수 있습니다. 예를 들어, 결제 시스템을 만들 때 여러 결제 방법(신용카드, 계좌이체, 페이팔)이 있다면, Payment 추상 클래스에 process_payment(), refund() 같은 필수 함수를 정의해놓으면 모든 결제 방법이 이 함수들을 반드시 구현하게 됩니다.

기존에는 개발자들이 각자의 스타일대로 함수를 만들어서 통일성이 없었다면, 이제는 추상 클래스가 강제하는 규칙에 따라 모두가 같은 구조로 개발합니다. 추상 클래스의 핵심 특징은 첫째, 인터페이스를 강제하여 실수를 방지하고, 둘째, 코드의 구조를 명확하게 정의하며, 셋째, 대규모 프로젝트에서 팀원 간 협업을 원활하게 한다는 점입니다.

이러한 특징들이 안정적이고 확장 가능한 시스템을 만드는 데 필수적입니다.

코드 예제

from abc import ABC, abstractmethod  # 추상 클래스 만들기 위한 도구

# 추상 클래스: 모든 결제 방법이 따라야 할 규칙
class Payment(ABC):
    @abstractmethod  # 이 함수는 반드시 구현해야 함!
    def process_payment(self, amount):
        pass

    @abstractmethod
    def refund(self, amount):
        pass

# 신용카드 결제 (추상 클래스를 상속)
class CreditCard(Payment):
    def process_payment(self, amount):
        return f"신용카드로 {amount}원 결제 완료"

    def refund(self, amount):
        return f"신용카드로 {amount}원 환불 완료"

# 계좌이체 결제
class BankTransfer(Payment):
    def process_payment(self, amount):
        return f"계좌이체로 {amount}원 결제 완료"

    def refund(self, amount):
        return f"계좌이체로 {amount}원 환불 완료"

# 결제 처리
payments = [CreditCard(), BankTransfer()]
for payment in payments:
    print(payment.process_payment(10000))

설명

이것이 하는 일: 모든 결제 방법이 반드시 구현해야 할 process_payment()refund() 함수를 추상 클래스로 정의하고, 각 결제 방법이 이를 구현하게 합니다. 첫 번째로, from abc import ABC, abstractmethod로 Python의 추상 클래스 도구를 가져옵니다.

class Payment(ABC):처럼 ABC를 상속하면 이 클래스는 추상 클래스가 됩니다. @abstractmethod 데코레이터를 붙인 함수는 "필수 구현 함수"가 되어서, 이 클래스를 상속하는 모든 자식 클래스가 반드시 이 함수를 구현해야 합니다.

만약 구현하지 않으면 에러가 발생합니다. 그 다음으로, class CreditCard(Payment):에서 Payment를 상속받으면 CreditCard는 반드시 process_payment()refund()를 구현해야 합니다.

만약 둘 중 하나라도 빠뜨리면 Python이 에러를 발생시켜서 개발자에게 알려줍니다. 이렇게 하면 "깜빡하고 안 만들었다"는 실수를 원천적으로 방지할 수 있습니다.

마지막으로, payments 리스트에 여러 결제 방법을 담아서 반복문으로 처리할 때, 모든 결제 방법이 process_payment() 함수를 가지고 있다는 것이 보장됩니다. 추상 클래스가 이것을 강제했기 때문입니다.

따라서 안심하고 payment.process_payment(10000)를 호출할 수 있습니다. 여러분이 이 코드를 사용하면 새로운 결제 방법을 추가할 때 실수할 가능성이 0%입니다.

예를 들어 PayPal 클래스를 만들 때 process_payment()를 깜빡하고 안 만들면 코드를 실행하기도 전에 에러가 나서 바로 알 수 있습니다. 또한 코드를 읽는 다른 개발자도 "아, 모든 결제 방법은 이 두 함수를 가지고 있구나"라고 바로 이해할 수 있어서 협업이 훨씬 수월해집니다.

실전 팁

💡 추상 클래스는 직접 객체를 만들 수 없습니다. Payment()는 에러가 나요. 반드시 자식 클래스를 만들어서 사용하세요.

💡 @abstractmethod를 붙인 함수는 자식에서 반드시 구현해야 하지만, 안 붙인 일반 함수는 선택사항입니다.

💡 추상 클래스는 공통 로직도 포함할 수 있습니다. 필수 함수만 정의하는 것이 아니라 공통으로 쓰이는 함수도 구현해두면 좋아요.

💡 인터페이스와 비슷하지만 Python에는 인터페이스가 없어서 추상 클래스로 같은 효과를 냅니다.

💡 실무에서는 플러그인 시스템, API 클라이언트, 데이터베이스 어댑터 같은 곳에서 추상 클래스를 자주 사용합니다.


6. 생성자와 소멸자 - 객체의 탄생과 소멸

시작하며

여러분이 새로운 스마트폰을 샀을 때를 생각해보세요. 처음 켜면 초기 설정을 하죠.

언어 선택, Wi-Fi 연결, 계정 로그인 등이 자동으로 진행됩니다. 그리고 나중에 폰을 공장 초기화할 때는 모든 데이터가 삭제되고 정리됩니다.

프로그래밍에서 객체도 마찬가지입니다. 객체가 처음 만들어질 때 초기 설정이 필요하고, 사라질 때 정리 작업이 필요합니다.

예를 들어, 데이터베이스 연결 객체는 생성될 때 DB에 연결하고, 소멸될 때 연결을 끊어야 합니다. 바로 이럴 때 필요한 것이 생성자와 소멸자입니다.

객체의 생명주기를 관리하여 자원을 효율적으로 사용할 수 있게 해줍니다.

개요

간단히 말해서, 생성자는 객체가 태어날 때 자동으로 실행되는 함수이고, 소멸자는 객체가 사라질 때 자동으로 실행되는 함수입니다. 생성자를 사용하면 객체를 만들 때 필요한 초기화 작업을 자동으로 처리할 수 있습니다.

예를 들어, 파일 객체를 만들 때 생성자에서 파일을 열고, 게임 캐릭터를 만들 때 생성자에서 기본 스탯을 설정하고, 네트워크 클라이언트를 만들 때 생성자에서 서버에 연결합니다. 이렇게 하면 객체를 만드는 즉시 사용 가능한 상태가 됩니다.

기존에는 객체를 만든 후 따로 초기화 함수를 호출해야 했다면, 이제는 객체를 만들기만 하면 자동으로 모든 준비가 완료됩니다. 생성자와 소멸자의 핵심 특징은 첫째, 자동으로 실행되어 개발자가 깜빡할 일이 없고, 둘째, 자원 관리를 체계적으로 할 수 있으며, 셋째, 메모리 누수나 자원 고갈을 방지한다는 점입니다.

이러한 특징들이 안정적인 프로그램을 만드는 데 매우 중요합니다.

코드 예제

# 파일 관리 클래스
class FileManager:
    # 생성자: 객체가 만들어질 때 자동 실행
    def __init__(self, filename):
        self.filename = filename
        self.file = open(filename, 'w')  # 파일 열기
        print(f"📁 {filename} 파일을 열었습니다.")

    # 파일에 데이터 쓰기
    def write_data(self, data):
        self.file.write(data + '\n')
        print(f"✍️ '{data}' 를 파일에 썼습니다.")

    # 소멸자: 객체가 사라질 때 자동 실행
    def __del__(self):
        if hasattr(self, 'file'):
            self.file.close()  # 파일 닫기
            print(f"🔒 {self.filename} 파일을 닫았습니다.")

# 파일 매니저 사용
file_manager = FileManager("test.txt")  # 생성자 실행
file_manager.write_data("안녕하세요")
file_manager.write_data("OOP는 재미있어요")
# 프로그램 종료 시 자동으로 소멸자 실행됨

설명

이것이 하는 일: 파일 관리 객체가 생성될 때 자동으로 파일을 열고, 객체가 소멸될 때 자동으로 파일을 닫아서 자원을 안전하게 관리합니다. 첫 번째로, __init__ 생성자가 실행되면서 open(filename, 'w')로 파일을 엽니다.

이것은 객체를 만드는 순간 자동으로 실행되기 때문에 개발자가 따로 파일을 열 필요가 없습니다. FileManager("test.txt")라고 쓰는 순간 파일이 열려서 바로 사용 가능한 상태가 됩니다.

생성자에는 보통 초기값 설정, 자원 할당, 연결 설정 같은 준비 작업을 넣습니다. 그 다음으로, write_data() 함수로 데이터를 파일에 씁니다.

이것은 일반 메서드로, 필요할 때마다 호출할 수 있습니다. 파일이 이미 생성자에서 열렸기 때문에 바로 쓸 수 있습니다.

마지막으로, 프로그램이 종료되거나 객체를 더 이상 사용하지 않을 때 __del__ 소멸자가 자동으로 실행됩니다. self.file.close()로 파일을 닫아서 자원을 해제합니다.

만약 소멸자가 없다면 파일이 계속 열려 있어서 다른 프로그램이 그 파일을 사용하지 못하거나, 메모리 누수가 발생할 수 있습니다. 여러분이 이 코드를 사용하면 파일을 열고 닫는 것을 깜빡할 일이 없습니다.

모든 것이 자동으로 관리되기 때문입니다. 데이터베이스 연결, 네트워크 소켓, 메모리 할당 같은 자원을 다룰 때도 생성자/소멸자 패턴을 사용하면 안전하게 관리할 수 있습니다.

특히 에러가 발생해도 소멸자는 실행되기 때문에 자원이 제대로 해제됩니다.

실전 팁

💡 Python에서는 생성자는 __init__, 소멸자는 __del__입니다. 언더스코어 2개씩 붙는 것을 잊지 마세요.

💡 소멸자는 언제 실행될지 정확히 알 수 없습니다. 중요한 작업은 명시적으로 close() 같은 함수를 만들어 호출하는 게 안전해요.

💡 with 문을 사용하면 더 안전합니다: with FileManager("test.txt") as fm: 형식으로 자동으로 정리됩니다.

💡 생성자에서 에러가 나면 객체가 만들어지지 않습니다. 생성자는 반드시 성공해야 하므로 예외 처리를 신중히 하세요.

💡 실무에서는 DB 연결, API 클라이언트, 하드웨어 제어 같은 곳에서 생성자/소멸자를 반드시 사용합니다.


7. 정적 메서드와 클래스 메서드 - 객체 없이 사용하기

시작하며

여러분이 계산기 앱을 만든다고 생각해보세요. 덧셈, 뺄셈, 곱셈 같은 기능은 특정 계산기 객체에 속할 필요가 없습니다.

그냥 "2 + 3"을 계산하면 되지, 굳이 계산기 객체를 만들 필요는 없죠. 프로그래밍에서도 모든 함수가 객체에 속해야 하는 것은 아닙니다.

예를 들어, 날짜 형식을 변환하거나, 문자열을 검증하거나, 수학 계산을 하는 유틸리티 함수들은 객체 없이 바로 사용할 수 있으면 더 편리합니다. 바로 이럴 때 필요한 것이 정적 메서드와 클래스 메서드입니다.

객체를 만들지 않고도 클래스에 속한 함수를 바로 호출할 수 있습니다.

개요

간단히 말해서, 정적 메서드는 객체 없이 클래스 이름으로 바로 호출할 수 있는 함수이고, 클래스 메서드는 클래스 자체를 다루는 함수입니다. 정적 메서드를 사용하면 관련된 함수들을 클래스로 그룹화할 수 있습니다.

예를 들어, StringUtils 클래스에 문자열 관련 유틸리티 함수를 모아두면 코드 구조가 명확해집니다. 객체를 만들 필요 없이 StringUtils.reverse("hello")처럼 바로 사용할 수 있어서 편리합니다.

기존에는 그냥 일반 함수로 만들거나 객체를 억지로 만들어서 사용했다면, 이제는 클래스의 네임스페이스를 활용하여 체계적으로 관리할 수 있습니다. 정적 메서드와 클래스 메서드의 핵심 특징은 첫째, 객체 생성 없이 바로 사용할 수 있고, 둘째, 관련 함수를 논리적으로 그룹화할 수 있으며, 셋째, 메모리를 절약할 수 있다는 점입니다.

이러한 특징들이 유틸리티 함수나 팩토리 패턴을 구현할 때 매우 유용합니다.

코드 예제

# 수학 유틸리티 클래스
class MathUtils:
    # 정적 메서드: 객체 없이 바로 사용 가능
    @staticmethod
    def add(a, b):
        return a + b

    @staticmethod
    def is_even(number):
        return number % 2 == 0

# 날짜 관리 클래스
class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    # 클래스 메서드: 다른 방식으로 객체 생성
    @classmethod
    def from_string(cls, date_string):
        # "2024-03-15" 형식을 파싱
        year, month, day = date_string.split('-')
        return cls(int(year), int(month), int(day))

# 정적 메서드 사용 (객체 없이)
print(MathUtils.add(5, 3))        # 8
print(MathUtils.is_even(10))      # True

# 클래스 메서드로 객체 생성
date = Date.from_string("2024-03-15")
print(f"{date.year}{date.month}{date.day}일")

설명

이것이 하는 일: 정적 메서드로 유틸리티 함수를 제공하고, 클래스 메서드로 다양한 형식의 입력으로 객체를 생성할 수 있게 합니다. 첫 번째로, @staticmethod 데코레이터를 붙인 함수는 정적 메서드가 됩니다.

이것은 selfcls 매개변수가 필요 없고, 그냥 일반 함수처럼 작동합니다. MathUtils.add(5, 3)처럼 클래스 이름으로 바로 호출할 수 있습니다.

객체를 만들지 않아도 되므로 메모리도 절약되고 코드도 간결합니다. 수학 계산, 문자열 처리, 파일 경로 조작 같은 순수 함수에 적합합니다.

그 다음으로, @classmethod 데코레이터를 붙인 함수는 첫 번째 매개변수로 cls(클래스 자체)를 받습니다. from_string 메서드를 보면 문자열을 파싱해서 cls(int(year), int(month), int(day))로 객체를 생성합니다.

이것은 "팩토리 메서드"라고 불리는 패턴으로, 다양한 형식의 입력으로 객체를 만들 수 있게 해줍니다. 일반 생성자는 숫자 3개를 받지만, from_string은 문자열을 받아서 자동으로 파싱합니다.

마지막으로, 이런 메서드들은 객체의 상태를 사용하지 않는 독립적인 기능에 사용됩니다. add(5, 3)은 어떤 객체의 데이터도 필요하지 않고 순수하게 계산만 합니다.

반면 일반 메서드는 self를 통해 객체의 데이터를 사용합니다. 여러분이 이 코드를 사용하면 관련된 함수들을 체계적으로 관리할 수 있습니다.

MathUtils에 수학 함수를 모아두고, StringUtils에 문자열 함수를 모아두면 어디에 어떤 함수가 있는지 찾기 쉽습니다. 또한 클래스 메서드를 사용하면 객체를 만드는 다양한 방법을 제공할 수 있어서 사용자 편의성이 크게 향상됩니다.

실전 팁

💡 정적 메서드는 self를 안 쓰고, 클래스 메서드는 cls를 씁니다. 헷갈리지 않도록 주의하세요.

💡 객체의 데이터를 사용하지 않는다면 정적 메서드, 클래스 자체를 다뤄야 한다면 클래스 메서드를 선택하세요.

💡 클래스 메서드는 상속받은 자식 클래스에서도 잘 작동합니다. cls가 실제 호출된 클래스를 가리키기 때문이에요.

💡 Python의 내장 타입도 클래스 메서드를 많이 씁니다: dict.fromkeys(), str.maketrans() 등이 대표적입니다.

💡 실무에서는 설정 관리, 싱글톤 패턴, 팩토리 패턴 같은 곳에서 정적/클래스 메서드를 자주 사용합니다.


8. 컴포지션 - 조립해서 만들기

시작하며

여러분이 레고로 우주선을 만든다고 생각해보세요. 엔진, 날개, 조종석을 각각 만든 다음 조립하면 완성됩니다.

각 부품은 독립적으로 존재하고, 나중에 날개를 다른 것으로 교체할 수도 있습니다. 프로그래밍에서도 복잡한 객체를 작은 부품 객체들을 조합해서 만들 수 있습니다.

예를 들어, 자동차 클래스를 만들 때 엔진, 바퀴, 핸들을 각각 별도 객체로 만들고 자동차 안에 포함시키는 방식입니다. 바로 이럴 때 필요한 것이 컴포지션입니다.

상속 대신 "has-a" 관계로 객체를 구성하면 더 유연하고 재사용 가능한 코드를 만들 수 있습니다.

개요

간단히 말해서, 컴포지션은 큰 객체 안에 작은 객체들을 넣어서 기능을 조합하는 방식입니다. 레고 블록을 조립하듯이요.

컴포지션을 사용하면 코드의 유연성이 크게 향상됩니다. 예를 들어, 게임에서 무기 시스템을 만들 때 Weapon 클래스를 별도로 만들고 캐릭터가 이것을 "가지도록" 하면, 게임 중에 무기를 바꾸거나 업그레이드하기가 매우 쉽습니다.

상속으로 만들면 이런 동적 변경이 어렵습니다. 기존에는 모든 것을 상속으로 해결하려다가 클래스 계층이 너무 복잡해졌다면, 이제는 컴포지션으로 필요한 부품만 조합해서 깔끔하게 만들 수 있습니다.

컴포지션의 핵심 특징은 첫째, 부품을 교체하거나 수정하기 쉽고, 둘째, 다중 상속의 복잡함 없이 여러 기능을 조합할 수 있으며, 셋째, 테스트와 디버깅이 훨씬 쉽다는 점입니다. 이러한 특징들이 "상속보다 컴포지션을 선호하라"는 유명한 디자인 원칙의 근거가 됩니다.

코드 예제

# 부품 클래스들
class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        return f"{self.horsepower}마력 엔진 시동!"

class Wheel:
    def __init__(self, size):
        self.size = size

    def rotate(self):
        return f"{self.size}인치 바퀴 회전 중"

# 컴포지션: 자동차는 엔진과 바퀴를 "가진다"
class Car:
    def __init__(self, engine_hp, wheel_size):
        self.engine = Engine(engine_hp)  # 엔진 객체 포함
        self.wheels = [Wheel(wheel_size) for _ in range(4)]  # 바퀴 4개

    def drive(self):
        engine_msg = self.engine.start()
        wheel_msg = self.wheels[0].rotate()
        return f"🚗 {engine_msg}\n   {wheel_msg}"

    # 엔진 교체 가능!
    def upgrade_engine(self, new_hp):
        self.engine = Engine(new_hp)
        return f"엔진을 {new_hp}마력으로 업그레이드!"

# 자동차 만들고 사용
my_car = Car(engine_hp=200, wheel_size=18)
print(my_car.drive())
print(my_car.upgrade_engine(300))
print(my_car.drive())

설명

이것이 하는 일: 자동차 객체가 엔진과 바퀴 객체를 내부에 포함하여, 각 부품을 독립적으로 관리하고 교체할 수 있게 합니다. 첫 번째로, Engine과 Wheel 클래스를 별도로 정의합니다.

이들은 완전히 독립적인 객체로, 다른 곳에서도 재사용할 수 있습니다. 예를 들어 Engine은 배, 비행기, 오토바이에서도 사용할 수 있습니다.

각 클래스는 자신의 역할에만 집중하므로 코드가 명확하고 이해하기 쉽습니다. 그 다음으로, Car 클래스의 __init__에서 self.engine = Engine(engine_hp)로 엔진 객체를 생성해서 포함시킵니다.

이것이 컴포지션의 핵심입니다. Car는 Engine을 "상속"하는 것이 아니라 "포함"합니다.

"자동차는 엔진이다(is-a)"가 아니라 "자동차는 엔진을 가진다(has-a)" 관계입니다. 바퀴도 마찬가지로 4개의 Wheel 객체를 리스트로 가지고 있습니다.

마지막으로, upgrade_engine 메서드를 보면 컴포지션의 장점이 명확히 드러납니다. self.engine = Engine(new_hp)로 엔진 객체를 새것으로 교체할 수 있습니다.

상속으로 만들었다면 이런 동적 교체가 불가능합니다. 또한 각 부품이 독립적이므로 엔진만 테스트하거나, 바퀴만 수정하는 것이 매우 쉽습니다.

여러분이 이 코드를 사용하면 시스템을 모듈화된 부품으로 나누어 관리할 수 있습니다. 새로운 타입의 엔진을 추가하거나, 전기 엔진으로 교체하거나, 특수한 바퀴를 장착하는 것이 모두 간단합니다.

또한 부품을 다른 프로젝트에서도 재사용할 수 있어서 개발 효율이 크게 향상됩니다. 복잡한 상속 계층 없이도 강력한 객체를 만들 수 있습니다.

실전 팁

💡 "is-a" 관계면 상속, "has-a" 관계면 컴포지션을 사용하세요. "자동차는 엔진을 가진다"는 has-a입니다.

💡 상속은 한 부모만 가질 수 있지만(Python 제외), 컴포지션은 여러 객체를 마음껏 포함할 수 있어요.

💡 부품 객체를 외부에서 주입받으면 더 유연합니다: Car(engine=my_engine) 형식으로 의존성 주입이 가능해요.

💡 실무에서는 대부분의 경우 상속보다 컴포지션이 더 좋은 선택입니다. 더 유연하고 테스트하기 쉬워요.

💡 게임 개발, UI 컴포넌트, 플러그인 시스템 같은 곳에서 컴포지션 패턴을 자주 사용합니다.


9. 프로퍼티 - 똑똑한 속성 만들기

시작하며

여러분이 온라인 쇼핑몰에서 상품 가격을 설정한다고 생각해보세요. 가격이 음수가 되면 안 되고, 너무 큰 값도 막아야 합니다.

그리고 가격이 바뀔 때마다 할인가도 자동으로 다시 계산되어야 하죠. 프로그래밍에서 단순히 변수에 값을 저장하면 이런 검증이나 자동 계산을 할 수 없습니다.

함수를 만들면 되지만 product.price = 10000 대신 product.set_price(10000)처럼 써야 해서 불편합니다. 바로 이럴 때 필요한 것이 프로퍼티입니다.

겉으로는 일반 변수처럼 보이지만 내부에서는 함수가 실행되어 검증, 계산, 로깅 같은 작업을 자동으로 수행합니다.

개요

간단히 말해서, 프로퍼티는 함수처럼 동작하지만 변수처럼 사용할 수 있는 똑똑한 속성입니다. 프로퍼티를 사용하면 데이터 접근을 제어하면서도 코드를 깔끔하게 유지할 수 있습니다.

예를 들어, 사용자의 이메일을 저장할 때 프로퍼티를 사용하면 값을 설정할 때마다 자동으로 유효성 검사를 하고, 값을 읽을 때마다 자동으로 소문자로 변환해서 반환할 수 있습니다. 사용자는 그냥 user.email = "Test@Example.COM"처럼 쓰기만 하면 됩니다.

기존에는 getter/setter 함수를 명시적으로 호출해야 했다면, 이제는 일반 변수처럼 사용하면서도 내부에서 복잡한 로직을 실행할 수 있습니다. 프로퍼티의 핵심 특징은 첫째, 코드가 직관적이고 읽기 쉬우며, 둘째, 나중에 일반 변수를 프로퍼티로 바꿔도 외부 코드를 수정할 필요가 없고, 셋째, 값의 유효성을 자동으로 검증할 수 있다는 점입니다.

이러한 특징들이 API 설계를 우아하게 만들어줍니다.

코드 예제

# 상품 클래스
class Product:
    def __init__(self, name, price):
        self.name = name
        self._price = price  # 실제 데이터는 _로 시작

    # price를 읽을 때 자동 실행
    @property
    def price(self):
        return self._price

    # price를 설정할 때 자동 실행
    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("가격은 0보다 커야 합니다!")
        if value > 1000000:
            raise ValueError("가격이 너무 높습니다!")
        self._price = value
        print(f"✅ 가격이 {value}원으로 변경되었습니다.")

    # 계산된 프로퍼티: 할인가 자동 계산
    @property
    def discounted_price(self):
        return self._price * 0.9  # 10% 할인

# 프로퍼티 사용 (일반 변수처럼!)
product = Product("노트북", 1000000)
print(f"원가: {product.price}원")
print(f"할인가: {product.discounted_price}원")

product.price = 800000  # setter 자동 실행
# product.price = -1000  # 에러 발생!

설명

이것이 하는 일: 가격을 설정할 때 자동으로 유효성을 검증하고, 할인가를 읽을 때 자동으로 계산하여 반환합니다. 첫 번째로, _price에 실제 데이터를 저장합니다.

언더스코어 하나는 "이 변수는 내부용이니 직접 건드리지 마세요"라는 약속입니다. 그리고 @property 데코레이터를 붙인 price() 함수를 만들면, 이것이 getter가 됩니다.

사용자가 product.price라고 쓰면 자동으로 이 함수가 실행되어 _price 값을 반환합니다. 여기서 추가 로직을 넣을 수도 있습니다(예: 로깅, 형식 변환 등).

그 다음으로, @price.setter 데코레이터를 붙인 함수가 setter가 됩니다. 사용자가 product.price = 800000처럼 값을 설정하면 이 함수가 자동으로 실행됩니다.

함수 안에서 if value < 0: 같은 검증 로직이 실행되어 잘못된 값이 들어가는 것을 막습니다. 검증을 통과하면 self._price = value로 실제 값을 저장합니다.

이 모든 과정이 자동으로 일어납니다. 마지막으로, discounted_price는 "계산된 프로퍼티"입니다.

setter가 없고 getter만 있어서 읽기 전용입니다. product.discounted_price라고 쓰면 그때마다 _price * 0.9을 계산해서 반환합니다.

실제로 값을 저장하지 않고 필요할 때마다 계산하므로 메모리도 절약되고 항상 최신 값을 보장합니다. 여러분이 이 코드를 사용하면 데이터 무결성을 보장하면서도 코드가 매우 깔끔해집니다.

product.price = -1000처럼 잘못된 값을 넣으려고 하면 즉시 에러가 발생해서 버그를 조기에 발견할 수 있습니다. 또한 나중에 가격 변경 로그를 남기거나, 변경 알림을 보내는 기능을 추가할 때도 setter 함수만 수정하면 되므로 매우 편리합니다.

실전 팁

💡 프로퍼티 이름과 내부 변수 이름을 구분하세요: price (프로퍼티), _price (실제 데이터).

💡 계산된 값은 프로퍼티로 만들면 좋습니다. 나이를 저장하지 말고 생년월일을 저장한 후 나이를 계산하는 프로퍼티를 만드세요.

💡 setter 없이 getter만 만들면 읽기 전용 프로퍼티가 됩니다. 외부에서 값을 바꿀 수 없어요.

💡 프로퍼티에서 복잡한 계산을 하면 성능이 느려질 수 있습니다. 단순한 작업만 하세요.

💡 실무에서는 설정값 검증, 형식 변환, 캐싱, 지연 로딩 같은 곳에서 프로퍼티를 자주 사용합니다.


10. 매직 메서드 - 특별한 기능 추가하기

시작하며

여러분이 두 숫자를 더할 때 5 + 3이라고 쓰죠. 만약 우리가 만든 클래스도 + 연산자로 더할 수 있다면 얼마나 편리할까요?

예를 들어, 두 벡터 객체를 vector1 + vector2로 더하거나, 쇼핑 카트 객체들을 cart1 + cart2로 합칠 수 있다면요. Python에서는 이런 특별한 기능을 클래스에 추가할 수 있습니다.

+, -, * 같은 연산자를 우리 클래스에서 사용할 수 있게 만들거나, print()로 객체를 예쁘게 출력하거나, len()으로 길이를 구할 수 있게 만들 수 있습니다. 바로 이럴 때 필요한 것이 매직 메서드입니다.

__add__, __str__, __len__ 같은 특별한 이름의 함수를 정의하면 Python이 자동으로 인식해서 특별한 기능을 제공합니다.

개요

간단히 말해서, 매직 메서드는 __로 시작하고 끝나는 특별한 함수로, Python의 내장 기능과 연동되어 작동합니다. 매직 메서드를 사용하면 우리가 만든 클래스가 Python의 내장 타입처럼 자연스럽게 동작하게 만들 수 있습니다.

예를 들어, 복소수 클래스를 만들 때 __add__를 정의하면 + 연산자로 두 복소수를 더할 수 있고, __str__을 정의하면 print()로 예쁘게 출력할 수 있습니다. 사용자 경험이 크게 향상됩니다.

기존에는 cart1.merge(cart2) 같은 함수를 만들어야 했다면, 이제는 cart1 + cart2처럼 직관적으로 사용할 수 있습니다. 매직 메서드의 핵심 특징은 첫째, 코드가 Pythonic하고 읽기 쉬우며, 둘째, 내장 함수와 연산자를 우리 클래스에서 사용할 수 있고, 셋째, 객체를 더 자연스럽게 다룰 수 있다는 점입니다.

이러한 특징들이 전문적이고 우아한 API를 만드는 데 필수적입니다.

코드 예제

# 벡터 클래스
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # + 연산자 지원: vector1 + vector2
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    # * 연산자 지원: vector * 3
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    # print() 할 때 예쁘게 출력
    def __str__(self):
        return f"Vector({self.x}, {self.y})"

    # len() 함수 지원: 벡터의 크기
    def __len__(self):
        return int((self.x**2 + self.y**2) ** 0.5)

    # == 연산자 지원: 두 벡터가 같은지 비교
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

# 매직 메서드 사용
v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(v1 + v2)        # Vector(4, 6)
print(v1 * 2)         # Vector(6, 8)
print(len(v1))        # 5
print(v1 == v2)       # False

설명

이것이 하는 일: 벡터 클래스에 +, *, == 연산자와 print(), len() 함수를 사용할 수 있게 만듭니다. 첫 번째로, __add__ 매직 메서드를 정의하면 + 연산자를 사용할 수 있습니다.

v1 + v2를 쓰면 Python이 자동으로 v1.__add__(v2)를 호출합니다. 이 함수에서 새로운 Vector 객체를 만들어 반환하면 됩니다.

각 성분을 더해서 Vector(self.x + other.x, self.y + other.y)로 결과 벡터를 만듭니다. 이렇게 하면 수학 공식처럼 자연스럽게 코드를 작성할 수 있습니다.

그 다음으로, __str__ 메서드는 객체를 문자열로 변환할 때 사용됩니다. print(v1)을 하면 Python이 자동으로 v1.__str__()을 호출해서 그 결과를 출력합니다.

이것을 정의하지 않으면 <__main__.Vector object at 0x...> 같은 의미 없는 메시지가 나오지만, 정의하면 Vector(3, 4) 같은 읽기 쉬운 형식으로 출력됩니다. 마지막으로, __len__, __mul__, __eq__ 같은 다른 매직 메서드들도 각각 len(), *, == 기능을 제공합니다.

Python에는 50개 이상의 매직 메서드가 있어서 거의 모든 연산자와 내장 함수를 우리 클래스에서 사용할 수 있습니다. 예를 들어 __getitem__을 정의하면 vector[0]처럼 인덱싱도 가능합니다.

여러분이 이 코드를 사용하면 클래스가 Python의 일급 시민처럼 동작합니다. 숫자나 문자열처럼 자연스럽게 연산하고, 비교하고, 출력할 수 있습니다.

특히 수학, 데이터 분석, 게임 개발 같은 분야에서 매직 메서드를 활용하면 코드가 수식처럼 읽혀서 이해하기 매우 쉬워집니다.

실전 팁

💡 자주 쓰는 매직 메서드: __init__, __str__, __repr__, __len__, __add__, __eq__, __getitem__

💡 __str__은 사용자용 출력, __repr__은 개발자용 디버깅 정보입니다. 둘 다 정의하는 게 좋아요.

💡 연산자 오버로딩은 직관적인 경우에만 사용하세요. +로 뺄셈하면 안 돼요! 혼란을 줍니다.

💡 비교 연산자를 전부 정의하려면 __eq__, __lt__, __le__, __gt__, __ge__가 필요합니다. 많으니까 @total_ordering 데코레이터를 쓰면 편해요.

💡 실무에서는 커스텀 컨테이너, 수학 라이브러리, ORM 같은 곳에서 매직 메서드를 적극 활용합니다.


#Python#OOP#Class#Inheritance#Encapsulation#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

데이터 증강과 정규화 완벽 가이드

머신러닝 모델의 성능을 극대화하는 핵심 기법인 데이터 증강과 정규화에 대해 알아봅니다. 실무에서 바로 활용할 수 있는 다양한 기법과 실전 예제를 통해 과적합을 방지하고 모델 성능을 향상시키는 방법을 배웁니다.

ResNet과 Skip Connection 완벽 가이드

딥러닝 모델이 깊어질수록 성능이 떨어지는 문제를 해결한 혁신적인 기법, ResNet과 Skip Connection을 초급자도 이해할 수 있도록 쉽게 설명합니다. 실제 구현 코드와 함께 배워보세요.

CNN 아키텍처 완벽 가이드 LeNet AlexNet VGGNet

컴퓨터 비전의 기초가 되는 세 가지 핵심 CNN 아키텍처를 배웁니다. 손글씨 인식부터 이미지 분류까지, 딥러닝의 발전 과정을 따라가며 각 모델의 구조와 특징을 실습 코드와 함께 이해합니다.

CNN 기초 Convolution과 Pooling 완벽 가이드

CNN의 핵심인 Convolution과 Pooling을 초급자도 쉽게 이해할 수 있도록 설명합니다. 이미지 인식의 원리부터 실제 코드 구현까지, 실무에서 바로 활용 가능한 내용을 담았습니다.

TensorFlow와 Keras 완벽 입문 가이드

머신러닝과 딥러닝의 세계로 들어가는 첫걸음! TensorFlow와 Keras 프레임워크를 처음 접하는 분들을 위한 친절한 가이드입니다. 실무에서 바로 활용할 수 있는 핵심 개념과 예제를 통해 AI 모델 개발의 기초를 탄탄히 다져보세요.