콘텐츠로 이동

6장: 클래스 — 객체를 설계하는 틀

함수로는 부족할 때

함수는 코드를 재사용할 수 있게 해줍니다.
그런데 데이터와 기능을 하나로 묶어서 관리하고 싶을 때는 어떻게 할까요?

# 학생 2명 — 이름, 점수, 합격 여부를 각각 관리
name1 = "Alice";  score1 = 88;  passed1 = score1 >= 60
name2 = "Bob";    score2 = 45;  passed2 = score2 >= 60

학생이 100명이면? 변수가 300개 필요합니다.
클래스를 쓰면 Student 하나로 묶어서 관리할 수 있습니다.

주요 용어 정리

용어 의미 예시
클래스(Class) 제품의 설계도 class Monster:
객체(Object) 설계도로 만들어진 실제 제품 m = Monster("슬라임")
속성(Attribute) 클래스 안에 있는 변수 self.name, self.hp
메서드(Method) 클래스 안에 있는 함수 def attack(self):
생성자(Constructor) 객체가 생성될 때 가장 먼저 실행되는 함수 def __init__(self):

몬스터 클래스로 이해하기

class Monster:              # 설계도
    def __init__(self, name, hp):   # 생성자
        self.name = name    # 속성 (attribute)
        self.hp   = hp      # 속성 (attribute)

    def attack(self):       # 메서드 (method)
        print(f"{self.name} 공격! (HP: {self.hp})")

    def is_alive(self):     # 메서드
        return self.hp > 0

# 설계도(Monster)로 실제 객체 생성
slime = Monster("슬라임", 30)   # 객체 1
goblin = Monster("고블린", 50)  # 객체 2

slime.attack()    # 슬라임 공격! (HP: 30)
goblin.attack()   # 고블린 공격! (HP: 50)
print(slime.is_alive())   # True

각 객체(slime, goblin)는 같은 설계도(Monster)로 만들었지만 속성값은 독립적입니다.


객체(Object)란?

객체 = 속성(데이터) + 메서드(기능)

강아지 객체:
  속성: 이름 = "초코", 품종 = "말티즈", 나이 = 3
  메서드: 짖는다(), 먹는다(), 잔다()

현실 세계의 모든 사물을 속성과 행동으로 모델링할 수 있습니다.

클래스(Class) 정의

클래스 = 객체를 찍어내는 설계도

class Dog:
    def __init__(self, name, breed):
        self.name  = name    # 인스턴스 변수
        self.breed = breed

    def bark(self):
        print(f"{self.name}: 왈왈!")

# 클래스로 객체(인스턴스) 생성
d1 = Dog("초코", "말티즈")
d2 = Dog("보리", "시바견")

d1.bark()   # 초코: 왈왈!
d2.bark()   # 보리: 왈왈!

__init__ — 생성자

객체가 만들어질 때 자동으로 호출되는 메서드입니다.
초기 속성(인스턴스 변수)을 여기서 설정합니다.

class Circle:
    def __init__(self, radius):
        self.radius = radius          # self.radius : 인스턴스 변수

    def area(self):
        import math
        return math.pi * self.radius ** 2

c = Circle(5)
print(c.radius)         # 5
print(round(c.area(), 2))  # 78.54

self — 나 자신을 가리키는 참조

모든 인스턴스 메서드의 첫 번째 매개변수self입니다.
self현재 객체를 가리킵니다.

class Counter:
    def __init__(self):
        self.count = 0       # 이 객체의 count

    def increment(self):
        self.count += 1      # 이 객체의 count를 증가

    def get(self):
        return self.count

c1 = Counter()
c2 = Counter()
c1.increment()
c1.increment()
print(c1.get())  # 2  (c1만 증가)
print(c2.get())  # 0  (c2는 그대로)

self.count는 각 객체마다 독립적인 공간에 저장됩니다.

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

인스턴스 변수 클래스 변수
정의 위치 __init__ 안, self.이름 클래스 블록, self 없이
공유 여부 객체마다 독립 모든 객체가 공유
접근 self.이름 클래스명.이름 또는 self.이름
class Student:
    school = "한국고등학교"   # 클래스 변수 — 모두 공유

    def __init__(self, name, score):
        self.name  = name    # 인스턴스 변수
        self.score = score

s1 = Student("Alice", 90)
s2 = Student("Bob",   75)

print(s1.school)          # 한국고등학교
print(s2.school)          # 한국고등학교
print(Student.school)     # 한국고등학교

Student.school = "서울고등학교"  # 클래스 변수 변경 → 전체 반영
print(s1.school)          # 서울고등학교
print(s2.school)          # 서울고등학교

클래스 변수의 함정 ⚠️

class Dog:
    tricks = []            # 클래스 변수 (리스트는 공유됨!)

    def add_trick(self, trick):
        self.tricks.append(trick)   # 공유 리스트에 추가됨

d1 = Dog()
d2 = Dog()
d1.add_trick("앉아")
d2.add_trick("손")

print(d1.tricks)   # ['앉아', '손']  ← d1만 추가했는데 d2 것도 있다!
print(d2.tricks)   # ['앉아', '손']

해결책: 리스트는 __init__ 안에서 인스턴스 변수로 만들기

class Dog:
    def __init__(self):
        self.tricks = []   # 각 객체마다 독립적인 리스트

    def add_trick(self, trick):
        self.tricks.append(trick)

d1 = Dog(); d2 = Dog()
d1.add_trick("앉아")
d2.add_trick("손")
print(d1.tricks)   # ['앉아']
print(d2.tricks)   # ['손']

특수 메서드(Magic Method)

__이름__ 형태의 메서드로, 파이썬이 특정 상황에서 자동 호출합니다.

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

    def __str__(self):          # print() 할 때 호출
        return f"({self.x}, {self.y})"

    def __add__(self, other):   # + 연산자 사용 시 호출
        return Point(self.x + other.x, self.y + other.y)

p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1)         # (1, 2)        ← __str__ 호출
print(p1 + p2)    # (4, 6)        ← __add__ 호출

상속(Inheritance)

기존 클래스의 속성과 메서드를 물려받아 새 클래스를 만듭니다.

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

    def speak(self):
        print(f"{self.name}: ...")

class Cat(Animal):              # Animal을 상속
    def speak(self):            # 메서드 오버라이딩
        print(f"{self.name}: 야옹")

class Dog(Animal):
    def speak(self):
        print(f"{self.name}: 멍멍")

animals = [Cat("나비"), Dog("초코"), Animal("?")]
for a in animals:
    a.speak()
나비: 야옹
초코: 멍멍
?: ...

super() — 부모 메서드 호출

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

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)   # 부모의 __init__ 호출
        self.breed = breed       # 자식만의 추가 속성

    def info(self):
        return f"{self.name} ({self.breed})"

d = Dog("초코", "말티즈")
print(d.info())    # 초코 (말티즈)

super().__init__() 을 빠뜨리면 부모의 속성이 초기화되지 않습니다.

접근 제어 — ___

표기 의미 실제 동작
name 공개 (public) 어디서든 접근
_name 비공개 관례 접근 가능하지만 "건드리지 마세요"
__name 이름 맹글링 _클래스명__name 으로 변환됨
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance    # 외부 직접 접근 차단

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount

    def get_balance(self):
        return self.__balance

acc = BankAccount(1000)
# print(acc.__balance)      # AttributeError!
acc.deposit(500)
print(acc.get_balance())    # 1500

실습 미션

미션 1: 직사각형 클래스

class Rectangle:
    def __init__(self, width, height):
        # 여기에 코드 작성
        pass

    def area(self):
        # 넓이 반환
        pass

    def perimeter(self):
        # 둘레 반환
        pass

    def __str__(self):
        # "Rectangle(3x5)" 형태로 반환
        pass

r = Rectangle(3, 5)
print(r)             # Rectangle(3x5)
print(r.area())      # 15
print(r.perimeter()) # 16

미션 2: 은행 계좌

class BankAccount:
    def __init__(self, owner, balance=0):
        pass

    def deposit(self, amount):   # 입금 (0 이하 무시)
        pass

    def withdraw(self, amount):  # 출금 (잔액 초과 시 "잔액 부족")
        pass

    def __str__(self):
        pass

acc = BankAccount("Alice", 1000)
acc.deposit(500)
acc.withdraw(200)
print(acc)           # Alice: 1300원
acc.withdraw(2000)   # 잔액 부족

미션 3: 동물 상속

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

    def info(self):
        return f"{self.name} ({self.age}살)"

class Dog(Animal):
    def __init__(self, name, age, breed):
        # super() 사용
        pass

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

class Cat(Animal):
    def speak(self):
        return f"{self.name}: 야옹~"

d = Dog("초코", 3, "말티즈")
c = Cat("나비", 5)
print(d.info())    # 초코 (3살)
print(d.speak())   # 초코: 멍멍!
print(c.speak())   # 나비: 야옹~

미션 4 (심화): 성적 관리 시스템

class Student:
    student_count = 0   # 클래스 변수: 전체 학생 수

    def __init__(self, name):
        self.name   = name
        self.scores = {}
        Student.student_count += 1

    def add_score(self, subject, score):
        self.scores[subject] = score

    def average(self):
        if not self.scores:
            return 0
        return sum(self.scores.values()) / len(self.scores)

    def __str__(self):
        return f"{self.name}: 평균 {self.average():.1f}"

s1 = Student("Alice")
s1.add_score("수학", 90)
s1.add_score("영어", 85)
s2 = Student("Bob")
s2.add_score("수학", 70)

print(s1)                       # Alice: 평균 87.5
print(Student.student_count)    # 2

핵심 요약

개념 설명
class 객체를 만드는 설계도
__init__ 객체 생성 시 자동 호출되는 생성자
self 현재 객체 자신을 가리키는 참조
인스턴스 변수 self.이름 — 객체마다 독립적
클래스 변수 클래스 블록에 정의 — 모든 객체가 공유
클래스 변수 함정 가변 객체(리스트 등)는 __init__
__str__ print() 시 호출되는 문자열 표현
상속 class Child(Parent): — 부모 기능 물려받기
super() 부모 클래스의 메서드 호출
__name 이름 맹글링 — 외부 직접 접근 차단