Python 클래스(Class) 완벽 가이드: 객체지향 프로그래밍 마스터하기

“파이썬 클래스가 어려워요. 언제, 왜 사용해야 하나요?”

파이썬은 대표적인 객체지향 프로그래밍(OOP) 언어입니다. 클래스는 연관된 데이터와 기능을 하나로 묶어 코드를 구조화하고 재사용성을 높이는 강력한 도구입니다. 이 글에서는 클래스의 기초부터 고급 기법까지 실전 예제와 함께 완벽하게 알려드립니다.

클래스란 무엇인가?

클래스의 개념

클래스는 서로 연관 있는 변수(데이터)와 함수(기능)를 하나로 묶어 이름을 붙인 것입니다.

현실 세계의 개념을 코드로 표현할 때 유용합니다.

# 예시: 자동차를 클래스로 표현
class Car:
    """자동차 클래스"""

    def __init__(self, brand, model):
        self.brand = brand    # 브랜드 (데이터)
        self.model = model    # 모델명 (데이터)
        self.speed = 0        # 현재 속도 (데이터)

    def accelerate(self):     # 가속 (기능)
        self.speed += 10
        print(f"현재 속도: {self.speed}km/h")

    def brake(self):          # 브레이크 (기능)
        self.speed = 0
        print("정지했습니다")

# 클래스 사용
my_car = Car("현대", "소나타")
my_car.accelerate()  # 출력: 현재 속도: 10km/h
my_car.accelerate()  # 출력: 현재 속도: 20km/h
my_car.brake()       # 출력: 정지했습니다

클래스 vs 함수

언제 클래스를 사용하고, 언제 함수를 사용할까요?

# 함수 방식 (단순한 경우)
def calculate_area(width, height):
    return width * height

area = calculate_area(10, 5)

# 클래스 방식 (복잡한 경우)
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

    def resize(self, width, height):
        self.width = width
        self.height = height

rect = Rectangle(10, 5)
print(rect.area())       # 50
print(rect.perimeter())  # 30
rect.resize(20, 10)
print(rect.area())       # 200

클래스를 사용해야 하는 경우

  • 관련된 데이터와 기능이 여러 개일 때
  • 상태(state)를 유지해야 할 때
  • 같은 구조의 객체를 여러 개 만들 때
  • 코드를 재사용하고 확장해야 할 때

클래스 정의하기

기본 문법

class ClassName:
    """클래스 설명 (docstring)"""
    pass  # 내용이 없을 때

클래스 이름 규칙

  • 첫 글자는 대문자로 시작 (PascalCase)
  • 여러 단어는 첫 글자를 대문자로 (예: MyClass, UserAccount)
  • 언더스코어는 사용하지 않음 (my_class ❌, MyClass ✅)
# 올바른 클래스 이름
class Person:
    pass

class BankAccount:
    pass

class HTTPRequest:
    pass

# 잘못된 클래스 이름 (동작은 하지만 권장하지 않음)
class person:        # 소문자 시작
    pass

class bank_account:  # 언더스코어 사용
    pass

인스턴스 생성

클래스로부터 **인스턴스(객체)**를 생성합니다.

class Dog:
    pass

# 인스턴스 생성
dog1 = Dog()
dog2 = Dog()
dog3 = Dog()

# 각 인스턴스는 독립적
print(dog1)  # <__main__.Dog object at 0x...>
print(dog2)  # <__main__.Dog object at 0x...> (다른 주소)
print(dog1 == dog2)  # False (서로 다른 객체)

생성자: init() 메서드

init()이란?

__init__()은 **생성자(Constructor)**로, 인스턴스가 생성될 때 자동으로 호출됩니다.

초기값 설정, 필요한 준비 작업 등을 수행합니다.

class Person:
    def __init__(self, name, age):
        """생성자: 인스턴스 초기화"""
        print(f"{name}님의 인스턴스가 생성되었습니다")
        self.name = name
        self.age = age

# 인스턴스 생성 시 자동으로 __init__ 호출
person1 = Person("김철수", 25)
# 출력: 김철수님의 인스턴스가 생성되었습니다

print(person1.name)  # 김철수
print(person1.age)   # 25

self의 의미

self인스턴스 자기 자신을 가리키는 참조입니다.

class Counter:
    def __init__(self):
        self.count = 0  # self.count는 이 인스턴스의 count 변수

    def increment(self):
        self.count += 1  # 자기 자신의 count를 증가

    def get_count(self):
        return self.count  # 자기 자신의 count를 반환

counter1 = Counter()
counter2 = Counter()

counter1.increment()
counter1.increment()

print(counter1.get_count())  # 2
print(counter2.get_count())  # 0 (독립적)

왜 self를 명시해야 할까?

Python의 설계 철학인 “명시적이 암시적보다 낫다(Explicit is better than implicit)”를 따릅니다.

class Example:
    def method(self):
        # self를 통해 명시적으로 인스턴스 변수임을 표시
        self.value = 10

        # 지역 변수
        local_value = 20

        print(self.value)    # 인스턴스 변수: 10
        print(local_value)   # 지역 변수: 20

기본값 설정

매개변수에 기본값을 지정할 수 있습니다.

class Student:
    def __init__(self, name, grade=1, school="미정"):
        self.name = name
        self.grade = grade
        self.school = school

# 다양한 방식으로 생성 가능
student1 = Student("김철수")
student2 = Student("이영희", 2)
student3 = Student("박민수", 3, "서울고등학교")

print(student1.grade)   # 1 (기본값)
print(student2.grade)   # 2
print(student3.school)  # 서울고등학교

인스턴스 변수 vs 클래스 변수

인스턴스 변수

각 인스턴스가 독립적으로 가지는 변수입니다.

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner      # 인스턴스 변수
        self.balance = balance  # 인스턴스 변수

    def deposit(self, amount):
        self.balance += amount

account1 = BankAccount("김철수", 10000)
account2 = BankAccount("이영희", 50000)

account1.deposit(5000)

print(account1.balance)  # 15000
print(account2.balance)  # 50000 (독립적)

클래스 변수

모든 인스턴스가 공유하는 변수입니다.

class Minsu:
    # 클래스 변수 (모든 인스턴스가 공유)
    version = "v4"
    count = 0  # 생성된 인스턴스 개수

    def __init__(self, name="Minsu"):
        # 인스턴스 변수 (각 인스턴스마다 독립적)
        self.name = name
        Minsu.count += 1  # 클래스 변수 증가

# 인스턴스 생성
voice1 = Minsu("김철수")
voice2 = Minsu("이영희")
voice3 = Minsu("박민수")

# 클래스 변수는 모든 인스턴스가 공유
print(voice1.version)  # v4
print(voice2.version)  # v4
print(voice3.version)  # v4

# 클래스 변수 변경 시 모든 인스턴스에 영향
Minsu.version = "v5"
print(voice1.version)  # v5
print(voice2.version)  # v5
print(voice3.version)  # v5

# 생성된 인스턴스 개수 확인
print(Minsu.count)  # 3

클래스 변수 vs 인스턴스 변수 비교

class Car:
    # 클래스 변수
    wheels = 4        # 모든 자동차는 바퀴 4개
    total_cars = 0    # 생성된 자동차 총 개수

    def __init__(self, brand, color):
        # 인스턴스 변수
        self.brand = brand    # 각 자동차의 브랜드
        self.color = color    # 각 자동차의 색상
        Car.total_cars += 1

car1 = Car("현대", "흰색")
car2 = Car("기아", "검정")

# 클래스 변수 접근
print(Car.wheels)        # 4
print(car1.wheels)       # 4 (인스턴스를 통해서도 접근 가능)
print(Car.total_cars)    # 2

# 인스턴스 변수 접근
print(car1.brand)        # 현대
print(car2.brand)        # 기아
# print(Car.brand)       # ❌ AttributeError (클래스는 인스턴스 변수에 접근 불가)

메서드(Method)

클래스 내부의 함수를 메서드라고 합니다.

인스턴스 메서드

가장 일반적인 메서드로, 첫 번째 매개변수로 self를 받습니다.

class Minsu:
    version = "v4"

    def __init__(self, name="Minsu"):
        self.name = name

    def song(self, title="hello"):
        """노래 부르기 메서드"""
        print(f"{self.name} sing the {title}")

    def medley(self):
        """여러 곡 이어 부르기"""
        self.song()                 # 기본값으로 호출
        self.song("second song")
        self.song("third song")

# 사용 예시
voice_minsu = Minsu("김철수")
voice_minsu.song()                    # 김철수 sing the hello
voice_minsu.song("hello world")       # 김철수 sing the hello world
voice_minsu.medley()
# 출력:
# 김철수 sing the hello
# 김철수 sing the second song
# 김철수 sing the third song

메서드 체이닝

메서드가 self를 반환하면 메서드를 연속으로 호출할 수 있습니다.

class Calculator:
    def __init__(self):
        self.value = 0

    def add(self, num):
        self.value += num
        return self  # 자기 자신 반환

    def subtract(self, num):
        self.value -= num
        return self

    def multiply(self, num):
        self.value *= num
        return self

    def result(self):
        return self.value

# 메서드 체이닝
calc = Calculator()
result = calc.add(10).multiply(2).subtract(5).result()
print(result)  # 15

# 위 코드는 다음과 같음
# calc.add(10)      # value = 10
# calc.multiply(2)  # value = 20
# calc.subtract(5)  # value = 15
# calc.result()     # 15

Getter와 Setter

class Person:
    def __init__(self, name, age):
        self._name = name  # _ 접두사: 내부에서만 사용 권장
        self._age = age

    # Getter
    def get_name(self):
        return self._name

    def get_age(self):
        return self._age

    # Setter
    def set_age(self, age):
        if age < 0:
            print("나이는 0보다 작을 수 없습니다")
            return
        self._age = age

person = Person("김철수", 25)
print(person.get_name())  # 김철수
print(person.get_age())   # 25

person.set_age(26)
print(person.get_age())   # 26

person.set_age(-5)        # 나이는 0보다 작을 수 없습니다
print(person.get_age())   # 26 (변경되지 않음)

상속(Inheritance)

상속의 개념

기존 클래스의 기능을 물려받아 확장할 수 있습니다.

# 부모 클래스 (슈퍼 클래스, 기반 클래스)
class Player:
    version = "v4"

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

    def song(self, title="default song"):
        print(f"{self.name} is playing {title}")

# 자식 클래스 (서브 클래스, 파생 클래스)
class Tottenham(Player):
    def __init__(self, uniform="class uniform"):
        self.uniform = uniform
        # 부모 클래스의 __init__ 호출
        super().__init__("tottenham")

    def shoot(self):
        print("Shooting!")

# 사용 예시
team = Tottenham()
print(team.uniform)   # class uniform
print(team.version)   # v4 (부모 클래스의 클래스 변수)
print(team.name)      # tottenham (부모 클래스의 인스턴스 변수)
team.shoot()          # Shooting! (자식 클래스의 메서드)
team.song("hello world")  # tottenham is playing hello world (부모 클래스의 메서드)

super() 이해하기

super()부모 클래스에 접근할 수 있게 해줍니다.

class Animal:
    def __init__(self, name):
        self.name = name
        print(f"Animal 생성: {name}")

    def speak(self):
        print("동물이 소리를 냅니다")

class Dog(Animal):
    def __init__(self, name, breed):
        # 부모 클래스의 __init__ 호출
        super().__init__(name)
        self.breed = breed
        print(f"Dog 생성: {breed}")

    def speak(self):
        # 부모 클래스의 메서드 호출
        super().speak()
        print("멍멍!")

# 인스턴스 생성
dog = Dog("바둑이", "진돗개")
# 출력:
# Animal 생성: 바둑이
# Dog 생성: 진돗개

print(dog.name)    # 바둑이
print(dog.breed)   # 진돗개

dog.speak()
# 출력:
# 동물이 소리를 냅니다
# 멍멍!

메서드 오버라이딩

자식 클래스에서 부모 클래스의 메서드를 재정의할 수 있습니다.

class Shape:
    def __init__(self, name):
        self.name = name

    def area(self):
        return 0

    def describe(self):
        print(f"이것은 {self.name}입니다")

class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__("직사각형")
        self.width = width
        self.height = height

    # 메서드 오버라이딩
    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        super().__init__("원")
        self.radius = radius

    # 메서드 오버라이딩
    def area(self):
        return 3.14 * self.radius ** 2

# 사용
rect = Rectangle(10, 5)
circle = Circle(7)

rect.describe()           # 이것은 직사각형입니다
print(rect.area())        # 50

circle.describe()         # 이것은 원입니다
print(circle.area())      # 153.86

다중 상속

Python은 여러 클래스를 동시에 상속받을 수 있습니다.

class Flyable:
    def fly(self):
        print("날고 있습니다")

class Swimmable:
    def swim(self):
        print("수영하고 있습니다")

class Duck(Flyable, Swimmable):
    def __init__(self, name):
        self.name = name

    def quack(self):
        print("꽥꽥!")

# 오리는 날 수도 있고 수영도 할 수 있음
duck = Duck("도널드")
duck.fly()    # 날고 있습니다
duck.swim()   # 수영하고 있습니다
duck.quack()  # 꽥꽥!

실전 예제

예제 1: 은행 계좌 시스템

class BankAccount:
    """은행 계좌 클래스"""
    bank_name = "파이썬은행"  # 클래스 변수

    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        self.transactions = []  # 거래 내역

    def deposit(self, amount):
        """입금"""
        if amount <= 0:
            print("입금액은 0보다 커야 합니다")
            return

        self.balance += amount
        self.transactions.append(f"입금: +{amount:,}원")
        print(f"{amount:,}원이 입금되었습니다. 잔액: {self.balance:,}원")

    def withdraw(self, amount):
        """출금"""
        if amount <= 0:
            print("출금액은 0보다 커야 합니다")
            return

        if amount > self.balance:
            print("잔액이 부족합니다")
            return

        self.balance -= amount
        self.transactions.append(f"출금: -{amount:,}원")
        print(f"{amount:,}원이 출금되었습니다. 잔액: {self.balance:,}원")

    def show_balance(self):
        """잔액 조회"""
        print(f"{self.owner}님의 잔액: {self.balance:,}원")

    def show_transactions(self):
        """거래 내역 조회"""
        print(f"\\\\n=== {self.owner}님의 거래 내역 ===")
        for transaction in self.transactions:
            print(transaction)
        print(f"현재 잔액: {self.balance:,}원\\\\n")

# 사용 예시
account = BankAccount("김철수", 100000)
account.deposit(50000)
account.withdraw(30000)
account.deposit(20000)
account.show_balance()
account.show_transactions()

예제 2: 학생 관리 시스템

class Student:
    """학생 클래스"""
    school_name = "파이썬고등학교"

    def __init__(self, name, student_id, grade):
        self.name = name
        self.student_id = student_id
        self.grade = grade
        self.scores = {}  # 과목별 점수

    def add_score(self, subject, score):
        """성적 추가"""
        if score < 0 or score > 100:
            print("점수는 0~100 사이여야 합니다")
            return
        self.scores[subject] = score
        print(f"{subject}: {score}점 입력 완료")

    def get_average(self):
        """평균 점수 계산"""
        if not self.scores:
            return 0
        return sum(self.scores.values()) / len(self.scores)

    def show_report(self):
        """성적표 출력"""
        print(f"\\\\n=== {self.name} 학생 성적표 ===")
        print(f"학번: {self.student_id}")
        print(f"학년: {self.grade}학년")
        print("\\\\n과목별 성적:")

        for subject, score in self.scores.items():
            print(f"  {subject}: {score}점")

        avg = self.get_average()
        print(f"\\\\n평균: {avg:.1f}점")

        # 등급 판정
        if avg >= 90:
            grade = "A"
        elif avg >= 80:
            grade = "B"
        elif avg >= 70:
            grade = "C"
        elif avg >= 60:
            grade = "D"
        else:
            grade = "F"

        print(f"등급: {grade}\\\\n")

# 사용 예시
student = Student("김철수", "2025001", 2)
student.add_score("국어", 85)
student.add_score("영어", 90)
student.add_score("수학", 88)
student.add_score("과학", 92)
student.show_report()

예제 3: 게임 캐릭터 시스템

class Character:
    """게임 캐릭터 기본 클래스"""
    def __init__(self, name, hp, attack):
        self.name = name
        self.hp = hp
        self.max_hp = hp
        self.attack = attack
        self.level = 1

    def take_damage(self, damage):
        """피해 받기"""
        self.hp -= damage
        if self.hp < 0:
            self.hp = 0
        print(f"{self.name}이(가) {damage} 피해를 입었습니다. (HP: {self.hp}/{self.max_hp})")

        if self.hp == 0:
            print(f"{self.name}이(가) 쓰러졌습니다!")

    def heal(self, amount):
        """회복"""
        self.hp += amount
        if self.hp > self.max_hp:
            self.hp = self.max_hp
        print(f"{self.name}이(가) {amount} 회복했습니다. (HP: {self.hp}/{self.max_hp})")

    def attack_enemy(self, enemy):
        """적 공격"""
        print(f"{self.name}이(가) {enemy.name}을(를) 공격합니다!")
        enemy.take_damage(self.attack)

class Warrior(Character):
    """전사 클래스"""
    def __init__(self, name):
        super().__init__(name, hp=150, attack=25)
        self.defense = 10

    def special_attack(self, enemy):
        """특수 공격: 강타"""
        damage = self.attack * 2
        print(f"{self.name}의 강타!")
        enemy.take_damage(damage)

class Mage(Character):
    """마법사 클래스"""
    def __init__(self, name):
        super().__init__(name, hp=80, attack=35)
        self.mana = 100

    def special_attack(self, enemy):
        """특수 공격: 파이어볼"""
        if self.mana < 30:
            print("마나가 부족합니다!")
            return

        self.mana -= 30
        damage = self.attack * 1.5
        print(f"{self.name}의 파이어볼! (마나: {self.mana})")
        enemy.take_damage(int(damage))

# 사용 예시
warrior = Warrior("전사왕")
mage = Mage("대마법사")

print("=== 전투 시작 ===")
warrior.attack_enemy(mage)
mage.special_attack(warrior)
warrior.special_attack(mage)

특수 메서드 (매직 메서드)

__로 시작하고 끝나는 특수한 메서드들입니다.

str()과 repr()

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __str__(self):
        """사용자 친화적인 문자열 (print 시 출력)"""
        return f"{self.name} ({self.price:,}원)"

    def __repr__(self):
        """개발자용 문자열 (디버깅용)"""
        return f"Product(name='{self.name}', price={self.price})"

product = Product("노트북", 1500000)

print(product)        # 노트북 (1,500,000원) (__str__)
print(repr(product))  # Product(name='노트북', price=1500000) (__repr__)

len(), getitem()

class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []

    def add_song(self, song):
        self.songs.append(song)

    def __len__(self):
        """len() 함수 지원"""
        return len(self.songs)

    def __getitem__(self, index):
        """인덱싱 지원"""
        return self.songs[index]

playlist = Playlist("내 플레이리스트")
playlist.add_song("노래1")
playlist.add_song("노래2")
playlist.add_song("노래3")

print(len(playlist))       # 3
print(playlist[0])         # 노래1
print(playlist[1])         # 노래2

# for 루프도 가능
for song in playlist:
    print(song)

연산자 오버로딩

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        """+ 연산자"""
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        """- 연산자"""
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        """* 연산자 (스칼라 곱)"""
        return Vector(self.x * scalar, self.y * scalar)

    def __str__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(5, 7)

v3 = v1 + v2
print(v3)  # Vector(7, 10)

v4 = v2 - v1
print(v4)  # Vector(3, 4)

v5 = v1 * 3
print(v5)  # Vector(6, 9)

고급 기능

클래스 메서드 (@classmethod)

클래스 자체를 첫 번째 인자로 받는 메서드입니다.

class Person:
    population = 0  # 클래스 변수

    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.population += 1

    @classmethod
    def get_population(cls):
        """클래스 메서드: 전체 인구 수 반환"""
        return cls.population

    @classmethod
    def from_birth_year(cls, name, birth_year):
        """대체 생성자: 출생년도로 인스턴스 생성"""
        from datetime import datetime
        age = datetime.now().year - birth_year
        return cls(name, age)

# 일반 생성자
person1 = Person("김철수", 25)

# 클래스 메서드로 생성
person2 = Person.from_birth_year("이영희", 1995)

# 클래스 메서드 호출
print(Person.get_population())  # 2
print(person1.get_population())  # 2 (인스턴스로도 호출 가능)

정적 메서드 (@staticmethod)

클래스나 인스턴스와 무관한 독립적인 메서드입니다.

class MathUtil:
    """수학 유틸리티 클래스"""

    @staticmethod
    def is_even(num):
        """짝수 판별"""
        return num % 2 == 0

    @staticmethod
    def is_prime(num):
        """소수 판별"""
        if num < 2:
            return False
        for i in range(2, int(num ** 0.5) + 1):
            if num % i == 0:
                return False
        return True

    @staticmethod
    def factorial(n):
        """팩토리얼 계산"""
        if n <= 1:
            return 1
        return n * MathUtil.factorial(n - 1)

# 인스턴스 생성 없이 바로 사용
print(MathUtil.is_even(10))      # True
print(MathUtil.is_prime(17))     # True
print(MathUtil.factorial(5))     # 120

프로퍼티 (@property)

메서드를 속성처럼 사용할 수 있게 해줍니다.

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        """반지름 getter"""
        return self._radius

    @radius.setter
    def radius(self, value):
        """반지름 setter"""
        if value < 0:
            raise ValueError("반지름은 0보다 커야 합니다")
        self._radius = value

    @property
    def area(self):
        """넓이 (읽기 전용)"""
        return 3.14 * self._radius ** 2

    @property
    def circumference(self):
        """둘레 (읽기 전용)"""
        return 2 * 3.14 * self._radius

# 사용 예시
circle = Circle(5)

# 속성처럼 접근
print(circle.radius)          # 5
print(circle.area)            # 78.5
print(circle.circumference)   # 31.4

# setter를 통한 값 변경
circle.radius = 10
print(circle.area)            # 314.0

# circle.radius = -5  # ValueError 발생

Private 변수 (캡슐화)

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.__balance = balance  # Private 변수 (__)

    def get_balance(self):
        """잔액 조회"""
        return self.__balance

    def deposit(self, amount):
        """입금"""
        if amount > 0:
            self.__balance += amount
            return True
        return False

    def withdraw(self, amount):
        """출금"""
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            return True
        return False

account = BankAccount("김철수", 10000)

# 정상 접근
print(account.get_balance())  # 10000
account.deposit(5000)
print(account.get_balance())  # 15000

# 직접 접근 시도 (실패)
# print(account.__balance)  # AttributeError

# 네임 맹글링으로 접근은 가능하지만 권장하지 않음
# print(account._BankAccount__balance)

추상 클래스 (ABC)

구현을 강제하는 추상 클래스를 만들 수 있습니다.

from abc import ABC, abstractmethod

class Shape(ABC):
    """도형 추상 클래스"""

    @abstractmethod
    def area(self):
        """넓이 계산 (반드시 구현해야 함)"""
        pass

    @abstractmethod
    def perimeter(self):
        """둘레 계산 (반드시 구현해야 함)"""
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

    def perimeter(self):
        return 2 * 3.14 * self.radius

# 사용
rect = Rectangle(10, 5)
circle = Circle(7)

print(rect.area())       # 50
print(circle.area())     # 153.86

# shape = Shape()  # TypeError: 추상 클래스는 인스턴스화 불가

디자인 패턴 예제

싱글톤 패턴

하나의 인스턴스만 생성되도록 보장합니다.

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        self.value = 0

# 여러 번 생성해도 같은 인스턴스
s1 = Singleton()
s2 = Singleton()

print(s1 is s2)  # True

s1.value = 100
print(s2.value)  # 100 (같은 인스턴스)

팩토리 패턴

객체 생성을 별도의 메서드로 분리합니다.

class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "멍멍!"

class Cat(Animal):
    def speak(self):
        return "야옹~"

class Bird(Animal):
    def speak(self):
        return "짹짹!"

class AnimalFactory:
    """동물 생성 팩토리"""

    @staticmethod
    def create_animal(animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        elif animal_type == "bird":
            return Bird()
        else:
            raise ValueError(f"알 수 없는 동물 타입: {animal_type}")

# 사용
factory = AnimalFactory()

dog = factory.create_animal("dog")
cat = factory.create_animal("cat")
bird = factory.create_animal("bird")

print(dog.speak())   # 멍멍!
print(cat.speak())   # 야옹~
print(bird.speak())  # 짹짹!

실무 활용 팁

1. 클래스 문서화

class User:
    """
    사용자 정보를 관리하는 클래스

    Attributes:
        username (str): 사용자 이름
        email (str): 이메일 주소
        age (int): 나이

    Examples:
        >>> user = User("john", "john@example.com", 25)
        >>> print(user.username)
        john
    """

    def __init__(self, username, email, age):
        """
        User 인스턴스를 초기화합니다.

        Args:
            username (str): 사용자 이름
            email (str): 이메일 주소
            age (int): 나이

        Raises:
            ValueError: age가 0보다 작을 경우
        """
        if age < 0:
            raise ValueError("나이는 0보다 커야 합니다")

        self.username = username
        self.email = email
        self.age = age

    def update_email(self, new_email):
        """
        이메일 주소를 업데이트합니다.

        Args:
            new_email (str): 새로운 이메일 주소

        Returns:
            bool: 업데이트 성공 여부
        """
        if "@" not in new_email:
            return False
        self.email = new_email
        return True

2. 타입 힌팅

from typing import List, Optional

class Student:
    def __init__(self, name: str, age: int, grades: List[int]):
        self.name: str = name
        self.age: int = age
        self.grades: List[int] = grades

    def get_average(self) -> float:
        """평균 점수를 반환합니다."""
        if not self.grades:
            return 0.0
        return sum(self.grades) / len(self.grades)

    def find_max_grade(self) -> Optional[int]:
        """최고 점수를 반환합니다."""
        if not self.grades:
            return None
        return max(self.grades)

3. 데이터 클래스 (@dataclass)

간단한 데이터 저장용 클래스를 쉽게 만들 수 있습니다.

from dataclasses import dataclass, field
from typing import List

@dataclass
class Product:
    name: str
    price: int
    quantity: int = 0
    tags: List[str] = field(default_factory=list)

    def total_value(self) -> int:
        return self.price * self.quantity

# 사용
product = Product("노트북", 1500000, 5, ["전자제품", "컴퓨터"])
print(product)  # Product(name='노트북', price=1500000, quantity=5, ...)
print(product.total_value())  # 7500000

자주 하는 실수와 해결법

실수 1: self 빠뜨리기

class Wrong:
    def method(self):
        value = 10  # 지역 변수
        # self.value = 10  # 인스턴스 변수 (올바름)

    def another_method(self):
        # print(value)  # NameError!
        print(self.value)  # 이렇게 해야 함

# ✅ 올바른 예
class Correct:
    def __init__(self):
        self.value = 10  # 인스턴스 변수로 저장

    def method(self):
        print(self.value)  # 정상 작동

실수 2: 가변 객체를 기본값으로 사용

# ❌ 잘못된 예
class Wrong:
    def __init__(self, items=[]):  # 위험!
        self.items = items

w1 = Wrong()
w2 = Wrong()
w1.items.append(1)
print(w2.items)  # [1] - 공유됨!

# ✅ 올바른 예
class Correct:
    def __init__(self, items=None):
        self.items = items if items is not None else []

c1 = Correct()
c2 = Correct()
c1.items.append(1)
print(c2.items)  # [] - 독립적

실수 3: super() 호출 잊기

# ❌ 잘못된 예
class Parent:
    def __init__(self, name):
        self.name = name

class Child(Parent):
    def __init__(self, name, age):
        # super().__init__(name)  # 호출 안 함!
        self.age = age

# child = Child("김철수", 10)
# print(child.name)  # AttributeError!

# ✅ 올바른 예
class ChildCorrect(Parent):
    def __init__(self, name, age):
        super().__init__(name)  # 부모 초기화
        self.age = age

child = ChildCorrect("김철수", 10)
print(child.name)  # 김철수

성능 최적화

slots 사용

메모리 사용량을 줄이고 속성 접근 속도를 높입니다.

# 일반 클래스
class NormalClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# __slots__ 사용
class SlottedClass:
    __slots__ = ['x', 'y']

    def __init__(self, x, y):
        self.x = x
        self.y = y

# 메모리 사용량 비교
import sys

normal = NormalClass(1, 2)
slotted = SlottedClass(1, 2)

print(sys.getsizeof(normal.__dict__))  # 약 112 bytes
print(sys.getsizeof(slotted))          # 약 56 bytes (절반)

# 주의: __slots__ 사용 시 동적 속성 추가 불가
# slotted.z = 3  # AttributeError

테스트 작성

class Calculator:
    def add(self, a, b):
        return a + b

    def subtract(self, a, b):
        return a - b

    def multiply(self, a, b):
        return a * b

    def divide(self, a, b):
        if b == 0:
            raise ValueError("0으로 나눌 수 없습니다")
        return a / b

# 단위 테스트
import unittest

class TestCalculator(unittest.TestCase):
    def setUp(self):
        """각 테스트 전에 실행"""
        self.calc = Calculator()

    def test_add(self):
        self.assertEqual(self.calc.add(2, 3), 5)
        self.assertEqual(self.calc.add(-1, 1), 0)

    def test_divide(self):
        self.assertEqual(self.calc.divide(10, 2), 5)
        with self.assertRaises(ValueError):
            self.calc.divide(10, 0)

# 테스트 실행
# unittest.main()

마무리하며

Python 클래스는 코드를 구조화하고 재사용성을 높이는 강력한 도구입니다.

핵심 요약

  • 클래스: 관련된 데이터와 기능을 묶은 설계도
  • 인스턴스: 클래스로부터 생성된 실제 객체
  • self: 인스턴스 자기 자신을 가리키는 참조
  • init(): 인스턴스 초기화 생성자
  • 상속: 기존 클래스의 기능을 확장
  • 캡슐화: 내부 구현을 숨기고 인터페이스만 노출

학습 로드맵

  1. 기본 클래스 정의와 인스턴스 생성
  2. 메서드 작성과 self 이해
  3. 생성자(init)와 인스턴스 변수
  4. 클래스 변수와 인스턴스 변수 차이
  5. 상속과 메서드 오버라이딩
  6. 특수 메서드와 연산자 오버로딩
  7. 프로퍼티, 클래스 메서드, 정적 메서드
  8. 추상 클래스와 디자인 패턴

실무 적용 팁

  • 작은 단위로 클래스 설계 (단일 책임 원칙)
  • 적절한 캡슐화로 내부 구현 숨기기
  • 문서화와 타입 힌팅으로 가독성 높이기
  • 테스트 코드 작성으로 안정성 확보

클래스는 처음에는 어렵게 느껴질 수 있지만, 실제 프로젝트에 적용하면서 자연스럽게 익숙해집니다. 작은 예제부터 시작해서 점차 복잡한 시스템을 설계해보세요!


참고 자료

관련 글

  • Python 상속과 다형성 완벽 가이드: 고급 객체지향 프로그래밍
  • Python 디자인 패턴: 실무에서 자주 쓰이는 10가지 패턴
  • Python 데이터 클래스(@dataclass) 활용법: 간결한 클래스 작성

댓글 남기기