Skip to content

Latest commit

 

History

History

P_09_Class

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

Python 클래스 (Class) 완벽 가이드

목차

  1. 객체 지향 프로그래밍 (OOP) 개요
  2. 클래스 정의
  3. 생성자와 소멸자
  4. 속성과 메소드
  5. is 연산자와 id() 함수
  6. dict 속성
  7. 클래스 변수 vs 인스턴스 변수
  8. 클래스 메소드 vs 인스턴스 메소드
  9. 정적 메소드
  10. 프로퍼티 (Property)
  11. 실전 예제

1. 객체 지향 프로그래밍 (OOP) 개요

1.1 함수 중심 vs 객체 중심

함수 중심 프로그래밍

# 데이터와 동작이 분리됨
def goto(name, address):
    print(f"{name}이(가) {address}으로 갑니다.")

# 함수에 데이터를 인자로 전달
goto("홍길동", "서울시 강남구")
goto("김철수", "부산시 해운대구")

객체 중심 프로그래밍

# 데이터(속성)와 동작(메소드)이 객체로 결합
class Person:
    def __init__(self, name, address):
        self.name = name        # 속성
        self.address = address  # 속성

    def go_home(self):          # 메소드
        print(f"{self.name}이(가) {self.address}으로 갑니다.")

# 객체 생성 및 사용
hong = Person("홍길동", "서울시 강남구")
kim = Person("김철수", "부산시 해운대구")

hong.go_home()  # 출력: 홍길동이(가) 서울시 강남구으로 갑니다.
kim.go_home()   # 출력: 김철수이(가) 부산시 해운대구으로 갑니다.

1.2 OOP의 핵심 개념

개념 영문 설명
클래스 Class 객체를 정의하는 설계도(틀)
객체 Object 클래스로부터 생성된 실체(인스턴스)
속성 Attribute 객체가 가진 데이터(변수)
메소드 Method 객체가 수행하는 동작(함수)
인스턴스 Instance 클래스로부터 만들어진 개별 객체

1.3 OOP의 장점

장점 설명
코드 재사용성 클래스 정의 후 여러 객체 생성 가능
유지보수 용이 관련 데이터와 기능이 한 곳에 모임
확장성 상속을 통한 기능 확장
캡슐화 내부 구현을 숨기고 인터페이스만 노출
다형성 동일한 인터페이스로 다양한 동작 수행

2. 클래스 정의

2.1 기본 클래스 정의

class ClassName:
    """클래스 설명 (docstring)"""
    pass

# 예시
class Cookie:
    """쿠키를 표현하는 클래스"""
    pass

# 객체 생성
cookie1 = Cookie()
cookie2 = Cookie()
cookie3 = Cookie()

print(type(cookie1))  # <class '__main__.Cookie'>

2.2 클래스 명명 규칙

규칙 설명 예시
PascalCase 각 단어 첫 글자 대문자 MyClass, UserAccount
명사 사용 클래스는 사물/개념 표현 Person, Car, Database
단수형 하나의 객체를 표현 Student (not Students)

2.3 self 키워드

self는 인스턴스 자신을 참조하는 매개변수입니다.

class Rectangle:
    def __init__(self, width, height):
        # self를 통해 인스턴스 속성에 접근
        self.width = width
        self.height = height

    def get_area(self):
        # self를 통해 다른 속성에 접근
        return self.width * self.height

    def get_info(self):
        # self를 통해 다른 메소드 호출
        area = self.get_area()
        return f"가로: {self.width}, 세로: {self.height}, 넓이: {area}"

rect = Rectangle(10, 20)
print(rect.get_info())
# 출력: 가로: 10, 세로: 20, 넓이: 200

3. 생성자와 소멸자

3.1 생성자 init

객체가 생성될 때 자동으로 호출되며, 초기화 작업을 수행합니다.

class Account:
    def __init__(self, owner, balance=0):
        """생성자: 계좌 초기화"""
        print(f"계좌 생성: {owner}")
        self.owner = owner
        self.balance = balance
        self.transaction_count = 0

# 객체 생성 시 __init__ 자동 호출
acc1 = Account("홍길동", 10000)
# 출력: 계좌 생성: 홍길동

acc2 = Account("김철수")  # 기본값 사용
# 출력: 계좌 생성: 김철수

print(acc1.balance)  # 10000
print(acc2.balance)  # 0

3.2 소멸자 del

객체가 소멸될 때 자동으로 호출되며, 정리 작업을 수행합니다.

class FileHandler:
    def __init__(self, filename):
        print(f"파일 열기: {filename}")
        self.filename = filename
        # 실제로는 파일을 열 것임

    def __del__(self):
        print(f"파일 닫기: {self.filename}")
        # 실제로는 파일을 닫을 것임

# 객체 생성
handler = FileHandler("data.txt")
# 출력: 파일 열기: data.txt

# 명시적 삭제
del handler
# 출력: 파일 닫기: data.txt

3.3 생성자 오버로딩 대안

Python은 메소드 오버로딩을 지원하지 않지만, 다양한 방법으로 유사한 기능을 구현할 수 있습니다.

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

    @classmethod
    def from_tuple(cls, coords):
        """튜플로부터 Point 생성"""
        return cls(coords[0], coords[1])

    @classmethod
    def from_string(cls, coord_str):
        """문자열로부터 Point 생성 (예: '3,4')"""
        x, y = map(int, coord_str.split(','))
        return cls(x, y)

# 다양한 방법으로 객체 생성
p1 = Point(3, 4)
p2 = Point.from_tuple((5, 6))
p3 = Point.from_string("7,8")

print(f"p1: ({p1.x}, {p1.y})")  # p1: (3, 4)
print(f"p2: ({p2.x}, {p2.y})")  # p2: (5, 6)
print(f"p3: ({p3.x}, {p3.y})")  # p3: (7, 8)

4. 속성과 메소드

4.1 속성 정의

class Car:
    def __init__(self, brand, model, year):
        # 인스턴스 속성 정의
        self.brand = brand
        self.model = model
        self.year = year
        self.mileage = 0
        self.is_running = False

# 속성 접근
car = Car("현대", "소나타", 2023)
print(car.brand)  # 현대
print(car.year)   # 2023

# 속성 수정
car.mileage = 1000
print(car.mileage)  # 1000

4.2 동적 속성 추가

Python은 런타임에 속성을 동적으로 추가할 수 있습니다.

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

person = Person("홍길동")

# 동적으로 속성 추가
person.age = 25
person.city = "서울"

print(person.age)   # 25
print(person.city)  # 서울

# 다른 인스턴스에는 해당 속성이 없음
person2 = Person("김철수")
# print(person2.age)  # AttributeError!

4.3 메소드 정의

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

    def deposit(self, amount):
        """입금 메소드"""
        if amount > 0:
            self.balance += amount
            return f"{amount}원 입금. 잔액: {self.balance}원"
        return "입금액은 0보다 커야 합니다."

    def withdraw(self, amount):
        """출금 메소드"""
        if amount > self.balance:
            return "잔액이 부족합니다."
        if amount <= 0:
            return "출금액은 0보다 커야 합니다."
        self.balance -= amount
        return f"{amount}원 출금. 잔액: {self.balance}원"

    def get_balance(self):
        """잔액 조회 메소드"""
        return self.balance

# 메소드 사용
account = BankAccount("홍길동", 10000)
print(account.deposit(5000))   # 5000원 입금. 잔액: 15000원
print(account.withdraw(3000))  # 3000원 출금. 잔액: 12000원
print(account.get_balance())   # 12000

4.4 메소드 호출 방식

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

calc = Calculator()

# 인스턴스를 통한 호출 (일반적)
result1 = calc.add(3, 5)
print(result1)  # 8

# 클래스를 통한 호출 (self를 명시적으로 전달)
result2 = Calculator.add(calc, 3, 5)
print(result2)  # 8

# 내장 타입도 동일한 방식
print("hello".upper())       # HELLO
print(str.upper("hello"))    # HELLO

5. is 연산자와 id() 함수

5.1 is 연산자

두 객체가 **동일한 객체(메모리 주소)**인지 확인합니다.

class Box:
    def __init__(self, value):
        self.value = value

box1 = Box(10)
box2 = Box(10)
box3 = box1  # 같은 객체 참조

# is 연산자: 동일 객체 여부
print(box1 is box2)  # False (값은 같지만 다른 객체)
print(box1 is box3)  # True (같은 객체)

# == 연산자: 값 비교 (기본적으로 is와 동일, 재정의 가능)
print(box1 == box2)  # False (기본 동작)
print(box1 == box3)  # True

5.2 is와 == 비교

연산자 비교 대상 설명
is 정체성 (Identity) 메모리 주소가 같은지
== 동등성 (Equality) 값이 같은지 (재정의 가능)
# 리스트 예제
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1

print(list1 == list2)  # True (값이 같음)
print(list1 is list2)  # False (다른 객체)
print(list1 is list3)  # True (같은 객체)

# 작은 정수는 캐싱됨 (Python 최적화)
a = 256
b = 256
print(a is b)  # True (캐싱된 객체)

c = 257
d = 257
print(c is d)  # False (새로 생성됨) - 실행 환경에 따라 다를 수 있음

5.3 id() 함수

객체의 **고유 ID(메모리 주소)**를 반환합니다.

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

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = p1

print(f"p1 id: {id(p1)}")  # 예: 140234567890
print(f"p2 id: {id(p2)}")  # 예: 140234567891 (다름)
print(f"p3 id: {id(p3)}")  # 예: 140234567890 (p1과 같음)

# id 비교 = is 연산자와 동일
print(id(p1) == id(p3))  # True
print(p1 is p3)          # True

5.4 타입 확인

class Animal:
    pass

class Dog(Animal):
    pass

dog = Dog()

# type() 사용 - 정확한 타입만 확인
print(type(dog) is Dog)     # True
print(type(dog) is Animal)  # False

# isinstance() 사용 - 상속 관계 포함
print(isinstance(dog, Dog))     # True
print(isinstance(dog, Animal))  # True (상속)
print(isinstance(dog, object))  # True (모든 클래스의 최상위)

6. dict 속성

6.1 인스턴스의 dict

객체가 가진 속성과 값을 딕셔너리 형태로 반환합니다.

class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
        self.scores = []

student = Student("홍길동", 3)
student.scores = [90, 85, 88]

# 인스턴스 속성 확인
print(student.__dict__)
# 출력: {'name': '홍길동', 'grade': 3, 'scores': [90, 85, 88]}

# 동적 속성 추가 후 확인
student.email = "hong@example.com"
print(student.__dict__)
# 출력: {'name': '홍길동', 'grade': 3, 'scores': [90, 85, 88], 'email': 'hong@example.com'}

6.2 클래스의 dict

클래스가 가진 메소드, 클래스 변수 등을 확인할 수 있습니다.

class Rectangle:
    # 클래스 변수
    count = 0

    def __init__(self, width, height):
        self.width = width
        self.height = height
        Rectangle.count += 1

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

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

# 클래스 속성 확인
print("클래스 __dict__:")
for key, value in Rectangle.__dict__.items():
    if not key.startswith('__'):
        print(f"  {key}: {value}")

# 출력:
# 클래스 __dict__:
#   count: 0
#   area: <function Rectangle.area at ...>
#   perimeter: <function Rectangle.perimeter at ...>

6.3 __dict__를 통한 속성 조작

class Config:
    def __init__(self):
        self.debug = False
        self.version = "1.0"

config = Config()

# 딕셔너리처럼 속성 접근
print(config.__dict__['debug'])  # False

# 딕셔너리처럼 속성 수정
config.__dict__['debug'] = True
print(config.debug)  # True

# 딕셔너리처럼 속성 추가
config.__dict__['new_setting'] = "value"
print(config.new_setting)  # value

# vars() 함수도 동일한 기능
print(vars(config))  # __dict__와 동일

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

7.1 정의와 차이점

구분 클래스 변수 인스턴스 변수
선언 위치 클래스 내부, 메소드 바깥 self.변수명
소속 클래스 자체 각 인스턴스
공유 여부 모든 인스턴스가 공유 인스턴스별 독립
접근 방법 클래스명.변수 (권장) self.변수 또는 인스턴스.변수
네임스페이스 클래스 네임스페이스 인스턴스 네임스페이스

7.2 클래스 변수

class Account:
    # 클래스 변수: 모든 인스턴스가 공유
    bank_name = "파이썬 은행"
    interest_rate = 0.02
    total_accounts = 0

    def __init__(self, owner, balance=0):
        self.owner = owner      # 인스턴스 변수
        self.balance = balance  # 인스턴스 변수
        Account.total_accounts += 1  # 클래스 변수 수정

# 인스턴스 생성
acc1 = Account("홍길동", 10000)
acc2 = Account("김철수", 20000)

# 클래스 변수 접근 (권장: 클래스명으로)
print(Account.bank_name)       # 파이썬 은행
print(Account.total_accounts)  # 2

# 인스턴스를 통해서도 읽기 가능 (비권장)
print(acc1.bank_name)  # 파이썬 은행

7.3 인스턴스 변수

class Student:
    school = "파이썬 고등학교"  # 클래스 변수

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

s1 = Student("홍길동", 1)
s2 = Student("김철수", 2)

# 인스턴스 변수는 독립적
print(s1.name, s1.grade)  # 홍길동 1
print(s2.name, s2.grade)  # 김철수 2

# 한 인스턴스 변경이 다른 인스턴스에 영향 없음
s1.grade = 3
print(s1.grade)  # 3
print(s2.grade)  # 2 (변경 없음)

7.4 주의: 인스턴스를 통한 클래스 변수 수정

class Counter:
    count = 0  # 클래스 변수

c1 = Counter()
c2 = Counter()

# 클래스명으로 수정 (올바른 방법)
Counter.count = 10
print(c1.count)  # 10
print(c2.count)  # 10

# 인스턴스로 수정하면 새 인스턴스 변수가 생성됨! (주의)
c1.count = 100  # c1에 인스턴스 변수 count 생성
print(c1.count)         # 100 (인스턴스 변수)
print(c2.count)         # 10  (클래스 변수)
print(Counter.count)    # 10  (클래스 변수)

# 확인
print(c1.__dict__)  # {'count': 100} - 인스턴스 변수
print(c2.__dict__)  # {} - 인스턴스 변수 없음

7.5 가변 객체 클래스 변수 주의

# 잘못된 예: 가변 객체를 클래스 변수로
class BadExample:
    shared_list = []  # 위험! 모든 인스턴스가 공유

    def add_item(self, item):
        self.shared_list.append(item)

b1 = BadExample()
b2 = BadExample()

b1.add_item("a")
b2.add_item("b")

print(b1.shared_list)  # ['a', 'b'] - 예상과 다름!
print(b2.shared_list)  # ['a', 'b']

# 올바른 예: 인스턴스 변수로 사용
class GoodExample:
    def __init__(self):
        self.my_list = []  # 인스턴스별 독립

    def add_item(self, item):
        self.my_list.append(item)

g1 = GoodExample()
g2 = GoodExample()

g1.add_item("a")
g2.add_item("b")

print(g1.my_list)  # ['a']
print(g2.my_list)  # ['b']

8. 클래스 메소드 vs 인스턴스 메소드

8.1 인스턴스 메소드

첫 번째 매개변수로 self를 받으며, 인스턴스를 통해 호출합니다.

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

    def get_area(self):
        """인스턴스 메소드: self를 통해 인스턴스 속성 접근"""
        return 3.14159 * self.radius ** 2

    def get_circumference(self):
        """인스턴스 메소드"""
        return 2 * 3.14159 * self.radius

circle = Circle(5)
print(circle.get_area())          # 78.53975
print(circle.get_circumference()) # 31.4159

8.2 클래스 메소드 (@classmethod)

첫 번째 매개변수로 cls(클래스 자체)를 받습니다.

class Employee:
    company = "파이썬 주식회사"
    employee_count = 0

    def __init__(self, name, department):
        self.name = name
        self.department = department
        Employee.employee_count += 1

    @classmethod
    def get_company(cls):
        """클래스 메소드: 클래스 변수에 접근"""
        return cls.company

    @classmethod
    def get_employee_count(cls):
        """클래스 메소드: 클래스 변수에 접근"""
        return cls.employee_count

    @classmethod
    def from_string(cls, emp_string):
        """팩토리 메소드: 문자열로부터 객체 생성"""
        name, department = emp_string.split('-')
        return cls(name, department)

# 클래스 메소드 호출 (인스턴스 없이)
print(Employee.get_company())  # 파이썬 주식회사

# 객체 생성
emp1 = Employee("홍길동", "개발팀")
emp2 = Employee.from_string("김철수-마케팅팀")

print(Employee.get_employee_count())  # 2
print(emp2.name, emp2.department)     # 김철수 마케팅팀

8.3 self가 없는 메소드 (일반 함수처럼 동작)

class Utility:
    def regular_method():  # self 없음
        """클래스 이름으로만 호출 가능"""
        print("클래스 메소드처럼 동작")

# 클래스 이름으로 호출
Utility.regular_method()  # OK

# 인스턴스로 호출 시 에러
util = Utility()
# util.regular_method()  # TypeError: takes 0 positional arguments but 1 was given

8.4 비교표

구분 인스턴스 메소드 클래스 메소드
데코레이터 없음 @classmethod
첫 번째 매개변수 self (인스턴스) cls (클래스)
접근 대상 인스턴스 속성, 클래스 속성 클래스 속성만
호출 방법 인스턴스.메소드() 클래스.메소드() 또는 인스턴스.메소드()
용도 인스턴스별 동작 클래스 관련 동작, 팩토리 메소드

9. 정적 메소드

9.1 정적 메소드 (@staticmethod)

self도 cls도 받지 않으며, 클래스와 논리적으로 관련된 독립적인 함수입니다.

class MathUtils:
    @staticmethod
    def add(a, b):
        """정적 메소드: 클래스/인스턴스와 무관한 유틸리티"""
        return a + b

    @staticmethod
    def multiply(a, b):
        return a * b

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

# 클래스 이름으로 호출
print(MathUtils.add(3, 5))       # 8
print(MathUtils.is_even(4))      # True

# 인스턴스로도 호출 가능
math_util = MathUtils()
print(math_util.multiply(3, 4))  # 12

9.2 정적 메소드 vs 클래스 메소드 vs 인스턴스 메소드

class Demo:
    class_var = "클래스 변수"

    def __init__(self, value):
        self.instance_var = value

    def instance_method(self):
        """인스턴스 메소드: self, cls, 둘 다 접근 가능"""
        return f"인스턴스: {self.instance_var}, 클래스: {self.class_var}"

    @classmethod
    def class_method(cls):
        """클래스 메소드: cls만 접근 가능, self 불가"""
        return f"클래스: {cls.class_var}"
        # self.instance_var 접근 불가

    @staticmethod
    def static_method(x, y):
        """정적 메소드: self, cls 모두 불가, 독립적 함수"""
        return x + y
        # self, cls 접근 불가

demo = Demo("인스턴스 값")

print(demo.instance_method())  # 인스턴스: 인스턴스 값, 클래스: 클래스 변수
print(Demo.class_method())     # 클래스: 클래스 변수
print(Demo.static_method(1, 2)) # 3

10. 프로퍼티 (Property)

10.1 프로퍼티란?

속성처럼 접근하지만 실제로는 메소드가 호출되는 기능입니다.

10.2 @property 데코레이터

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius  # 내부 저장용 (관례: _로 시작)

    @property
    def celsius(self):
        """Getter: 속성 읽기"""
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        """Setter: 속성 쓰기 (유효성 검사 가능)"""
        if value < -273.15:
            raise ValueError("절대영도 이하는 불가능합니다.")
        self._celsius = value

    @property
    def fahrenheit(self):
        """계산된 속성 (읽기 전용)"""
        return self._celsius * 9/5 + 32

# 사용
temp = Temperature(25)

# 속성처럼 접근
print(temp.celsius)     # 25 (getter 호출)
print(temp.fahrenheit)  # 77.0 (계산된 값)

# 속성처럼 할당
temp.celsius = 30       # setter 호출
print(temp.celsius)     # 30

# 유효성 검사
# temp.celsius = -300   # ValueError!

10.3 프로퍼티의 장점

장점 설명
캡슐화 내부 구현을 숨기고 인터페이스 제공
유효성 검사 setter에서 값 검증 가능
계산된 속성 다른 속성 기반으로 값 계산
하위 호환성 기존 코드 변경 없이 동작 추가
class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("너비는 양수여야 합니다.")
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("높이는 양수여야 합니다.")
        self._height = value

    @property
    def area(self):
        """읽기 전용 계산 속성"""
        return self._width * self._height

    @property
    def perimeter(self):
        """읽기 전용 계산 속성"""
        return 2 * (self._width + self._height)

rect = Rectangle(10, 20)
print(rect.area)       # 200
print(rect.perimeter)  # 60

rect.width = 15
print(rect.area)       # 300 (자동 재계산)

11. 실전 예제

11.1 은행 계좌 시스템

class BankAccount:
    """은행 계좌 클래스"""

    bank_name = "Python Bank"
    interest_rate = 0.02
    _account_counter = 0

    def __init__(self, owner, initial_balance=0):
        BankAccount._account_counter += 1
        self._account_number = f"ACC{BankAccount._account_counter:06d}"
        self._owner = owner
        self._balance = initial_balance
        self._transactions = []

    @property
    def account_number(self):
        return self._account_number

    @property
    def owner(self):
        return self._owner

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        """입금"""
        if amount <= 0:
            raise ValueError("입금액은 양수여야 합니다.")
        self._balance += amount
        self._transactions.append(('입금', amount))
        return self._balance

    def withdraw(self, amount):
        """출금"""
        if amount <= 0:
            raise ValueError("출금액은 양수여야 합니다.")
        if amount > self._balance:
            raise ValueError("잔액이 부족합니다.")
        self._balance -= amount
        self._transactions.append(('출금', amount))
        return self._balance

    def get_transaction_history(self):
        """거래 내역 조회"""
        return self._transactions.copy()

    def apply_interest(self):
        """이자 적용"""
        interest = self._balance * self.interest_rate
        self._balance += interest
        self._transactions.append(('이자', interest))
        return interest

    @classmethod
    def get_total_accounts(cls):
        return cls._account_counter

    def __str__(self):
        return f"[{self.account_number}] {self.owner}: {self.balance:,}원"

    def __repr__(self):
        return f"BankAccount('{self.owner}', {self.balance})"

# 사용 예제
acc1 = BankAccount("홍길동", 100000)
acc2 = BankAccount("김철수", 50000)

acc1.deposit(50000)
acc1.withdraw(30000)
acc1.apply_interest()

print(acc1)
# 출력: [ACC000001] 홍길동: 122,400원

print(f"총 계좌 수: {BankAccount.get_total_accounts()}")
# 출력: 총 계좌 수: 2

print(acc1.get_transaction_history())
# [('입금', 50000), ('출금', 30000), ('이자', 2400.0)]

11.2 학생 관리 시스템

class Student:
    """학생 클래스"""

    school_name = "Python Academy"
    _student_count = 0

    def __init__(self, name, grade):
        Student._student_count += 1
        self._id = f"STU{Student._student_count:04d}"
        self._name = name
        self._grade = grade
        self._scores = {}

    @property
    def id(self):
        return self._id

    @property
    def name(self):
        return self._name

    @property
    def grade(self):
        return self._grade

    @grade.setter
    def grade(self, value):
        if not 1 <= value <= 6:
            raise ValueError("학년은 1~6이어야 합니다.")
        self._grade = value

    def add_score(self, subject, score):
        """과목 점수 추가"""
        if not 0 <= score <= 100:
            raise ValueError("점수는 0~100이어야 합니다.")
        self._scores[subject] = score

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

    def get_grade_letter(self):
        """학점 계산"""
        avg = self.get_average()
        if avg >= 90: return 'A'
        if avg >= 80: return 'B'
        if avg >= 70: return 'C'
        if avg >= 60: return 'D'
        return 'F'

    @classmethod
    def get_student_count(cls):
        return cls._student_count

    def __str__(self):
        return f"{self.name} ({self.grade}학년) - 평균: {self.get_average():.1f}"

# 사용
student = Student("홍길동", 3)
student.add_score("국어", 90)
student.add_score("영어", 85)
student.add_score("수학", 88)

print(student)
# 출력: 홍길동 (3학년) - 평균: 87.7

print(f"학점: {student.get_grade_letter()}")
# 출력: 학점: B

요약 비교표

변수 종류 비교

구분 클래스 변수 인스턴스 변수
선언 클래스 내, 메소드 바깥 self.변수
공유 모든 인스턴스 공유 인스턴스별 독립
접근 클래스명.변수 self.변수
용도 상수, 카운터 객체 상태

메소드 종류 비교

구분 인스턴스 메소드 클래스 메소드 정적 메소드
데코레이터 없음 @classmethod @staticmethod
첫 매개변수 self cls 없음
접근 가능 인스턴스 + 클래스 클래스만 없음
용도 객체 동작 팩토리, 클래스 조작 유틸리티

참고 자료