포스트

[Python 100일 챌린지] Day 31 - 클래스와 객체 기초

[Python 100일 챌린지] Day 31 - 클래스와 객체 기초

지금까지 함수로 “동작”만 묶어왔는데, 현실 세계를 표현하기엔 부족하죠? 🤔 ──예를 들어 “자동차”는 색상, 속도, 제조사 같은 데이터와 주행(), 정지() 같은 동작이 함께 있어야 합니다! 😊

게임 캐릭터도 마찬가지입니다. 이름, HP, 공격력 같은 속성과 공격(), 방어() 같은 행동이 하나로 묶여야 하죠.

바로 이것이 객체지향 프로그래밍(OOP)입니다! 데이터와 기능을 하나로 묶어 현실 세계를 코드로 표현하는 강력한 방법! 💡

🎯 오늘의 학습 목표

  1. 객체지향 프로그래밍의 개념 이해하기
  2. 클래스와 객체의 차이점 알기
  3. 간단한 클래스 정의하고 사용하기
  4. 인스턴스 생성과 속성 접근 방법 익히기

⭐⭐⭐ (30-40분 완독)

📚 사전 지식


🎯 학습 목표 1: 객체지향 프로그래밍의 개념 이해하기

지금까지 우리는 절차적 프로그래밍(Procedural Programming)을 배웠습니다. 순차적으로 코드를 작성하고, 필요할 때 함수를 호출했죠.

이제 객체지향 프로그래밍(Object-Oriented Programming, OOP)이라는 새로운 패러다임을 배워봅시다!

🏠 실생활 비유: 집 설계도와 실제 집

클래스(Class) = 집 설계도 📋

  • 방 개수, 구조, 기능 등을 정의한 설계도
  • 실제로 살 수는 없지만, 집을 만들 수 있는 틀

객체/인스턴스(Object/Instance) = 실제 집 🏠

  • 설계도를 바탕으로 지은 실제 집
  • 실제로 살 수 있고, 각각 다른 주소와 내부 인테리어를 가짐
1
2
3
4
5
6
7
8
# 집 설계도 (클래스)
class House:
    pass

# 실제 집 (객체/인스턴스)
my_house = House()      # 강남구 집
your_house = House()    # 마포구 집
park_house = House()    # 송파구 집

같은 설계도로 여러 집을 지을 수 있듯이, 하나의 클래스로 여러 객체를 만들 수 있습니다!


🎯 학습 목표 2: 클래스와 객체의 차이점 알기

기본 문법

1
2
3
4
5
class ClassName:
    """클래스 설명 (docstring)"""

    # 클래스 내용
    pass

클래스 네이밍 규칙

파스칼 케이스(PascalCase) 사용

  • 각 단어의 첫 글자를 대문자로
  • 단어 사이 공백 없음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ✅ 올바른 클래스 이름
class Student:
    pass

class BankAccount:
    pass

class StudentGradeManager:
    pass

# ❌ 잘못된 클래스 이름
class student:          # 소문자 시작
    pass

class bank_account:     # 스네이크 케이스
    pass

class Student_grade:    # 혼합된 스타일
    pass

첫 번째 클래스 만들기

1
2
3
4
5
6
7
8
9
10
11
class Dog:
    """강아지를 표현하는 클래스"""
    pass

# 객체 생성 (인스턴스화)
my_dog = Dog()
your_dog = Dog()

print(my_dog)       # <__main__.Dog object at 0x...>
print(your_dog)     # <__main__.Dog object at 0x...>
print(type(my_dog)) # <class '__main__.Dog'>

실행 결과:

1
2
3
<__main__.Dog object at 0x10a5e8d90>
<__main__.Dog object at 0x10a5e8dd0>
<class '__main__.Dog'>

💡 각 객체는 다른 메모리 주소(0x...)를 가집니다!


🎯 학습 목표 3: 간단한 클래스 정의하고 사용하기

속성(Attribute) 추가하기

객체에 데이터를 저장하려면 속성을 추가해야 합니다.

방법 1: 객체 생성 후 속성 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Dog:
    pass

# 객체 생성
my_dog = Dog()

# 속성 추가
my_dog.name = "멍멍이"
my_dog.age = 3
my_dog.breed = "진돗개"

# 속성 접근
print(f"이름: {my_dog.name}")
print(f"나이: {my_dog.age}")
print(f"품종: {my_dog.breed}")

실행 결과:

1
2
3
이름: 멍멍이
나이: 3살
품종: 진돗개

방법 2: 클래스 내부에 속성 정의 (권장 ✅)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Dog:
    """강아지를 표현하는 클래스"""

    # 클래스 속성 (모든 인스턴스가 공유)
    species = "Canis familiaris"

    def set_info(self, name, age, breed):
        """강아지 정보를 설정하는 메서드"""
        self.name = name
        self.age = age
        self.breed = breed

    def bark(self):
        """짖는 소리를 출력하는 메서드"""
        return f"{self.name}: 멍멍!"

    def get_info(self):
        """강아지 정보를 반환하는 메서드"""
        return f"{self.name}({self.age}살, {self.breed})"

# 사용 예시
my_dog = Dog()
my_dog.set_info("멍멍이", 3, "진돗개")

print(my_dog.bark())        # 멍멍이: 멍멍!
print(my_dog.get_info())    # 멍멍이(3살, 진돗개)
print(my_dog.species)       # Canis familiaris

실행 결과:

1
2
3
멍멍이: 멍멍!
멍멍이(3살, 진돗개)
Canis familiaris

🎯 학습 목표 4: 인스턴스 생성과 속성 접근 방법 익히기

self 키워드 이해하기

self는 “현재 객체 자신”을 가리키는 참조입니다.

시각화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Dog:
    def set_name(self, name):
        self.name = name  # "이 객체"의 name 속성에 저장

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

dog1 = Dog()
dog1.set_name("멍멍이")  # self = dog1

dog2 = Dog()
dog2.set_name("바둑이")  # self = dog2

print(dog1.bark())  # dog1.name 사용 → "멍멍이: 멍멍!"
print(dog2.bark())  # dog2.name 사용 → "바둑이: 멍멍!"

작동 원리

1
2
3
4
5
# 이렇게 호출하면
dog1.bark()

# 실제로는 이렇게 동작합니다
Dog.bark(dog1)  # dog1이 self로 전달됨

💡 왜 self를 항상 첫 번째 매개변수로 쓸까?

1
2
3
4
5
6
7
8
9
class Calculator:
    def add(self, a, b):  # self가 첫 번째
        return a + b

    def get_result(self):
        return self.result

calc = Calculator()
# calc.add(5, 3)는 Calculator.add(calc, 5, 3)로 변환됨

⚠️ self는 관례일 뿐 - 다른 이름도 가능

1
2
3
4
5
6
class Dog:
    def bark(this):  # self 대신 this 사용
        return "멍멍!"

dog = Dog()
print(dog.bark())  # 작동은 하지만 권장하지 않음!

하지만 Python 커뮤니티의 99.9%가 self를 사용하므로, self를 쓰세요!


💡 실전 예제와 활용

예제 1: 학생 관리 시스템

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Student:
    """학생 정보를 관리하는 클래스"""

    school_name = "파이썬 고등학교"  # 클래스 속성

    def set_info(self, name, student_id, grade):
        """학생 정보 설정"""
        self.name = name
        self.student_id = student_id
        self.grade = grade
        self.scores = []  # 성적 리스트

    def add_score(self, subject, score):
        """과목 성적 추가"""
        self.scores.append({
            "subject": subject,
            "score": score
        })

    def get_average(self):
        """평균 성적 계산"""
        if not self.scores:
            return 0
        total = sum(item["score"] for item in self.scores)
        return total / len(self.scores)

    def print_report(self):
        """성적표 출력"""
        print(f"\n{'='*40}")
        print(f"학교: {Student.school_name}")
        print(f"이름: {self.name} ({self.student_id})")
        print(f"학년: {self.grade}학년")
        print(f"{'-'*40}")

        if self.scores:
            print("과목별 성적:")
            for item in self.scores:
                print(f"  - {item['subject']}: {item['score']}")
            print(f"{'-'*40}")
            print(f"평균: {self.get_average():.2f}")
        else:
            print("등록된 성적이 없습니다.")

        print(f"{'='*40}\n")

# 사용 예시
student1 = Student()
student1.set_info("홍길동", "2024001", 2)
student1.add_score("수학", 95)
student1.add_score("영어", 88)
student1.add_score("과학", 92)

student1.print_report()

student2 = Student()
student2.set_info("김철수", "2024002", 2)
student2.add_score("수학", 78)
student2.add_score("영어", 85)

student2.print_report()

실행 결과:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
========================================
학교: 파이썬 고등학교
이름: 홍길동 (2024001)
학년: 2학년
----------------------------------------
과목별 성적:
  - 수학: 95점
  - 영어: 88점
  - 과학: 92점
----------------------------------------
평균: 91.67점
========================================

========================================
학교: 파이썬 고등학교
이름: 김철수 (2024002)
학년: 2학년
----------------------------------------
과목별 성적:
  - 수학: 78점
  - 영어: 85점
----------------------------------------
평균: 81.50점
========================================

예제 2: 은행 계좌 시스템

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class BankAccount:
    """은행 계좌를 표현하는 클래스"""

    bank_name = "파이썬 은행"

    def set_account(self, owner, account_number, balance=0):
        """계좌 정보 설정"""
        self.owner = owner
        self.account_number = account_number
        self.balance = balance
        self.transactions = []  # 거래 내역

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

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

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

        if amount > self.balance:
            return f"잔액이 부족합니다. (현재 잔액: {self.balance:,}원)"

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

    def get_balance(self):
        """잔액 조회"""
        return f"{self.owner}님의 잔액: {self.balance:,}"

    def print_statement(self):
        """거래 내역 출력"""
        print(f"\n{'='*50}")
        print(f"{BankAccount.bank_name} - 거래 내역서")
        print(f"{'='*50}")
        print(f"예금주: {self.owner}")
        print(f"계좌번호: {self.account_number}")
        print(f"{'-'*50}")

        if self.transactions:
            print("거래 내역:")
            for i, transaction in enumerate(self.transactions, 1):
                print(f"  {i}. {transaction}")
        else:
            print("거래 내역이 없습니다.")

        print(f"{'-'*50}")
        print(f"현재 잔액: {self.balance:,}")
        print(f"{'='*50}\n")

# 사용 예시
account = BankAccount()
account.set_account("홍길동", "123-456-789", 10000)

print(account.deposit(50000))
print(account.withdraw(20000))
print(account.deposit(30000))
print(account.withdraw(100000))  # 잔액 부족
print(account.get_balance())

account.print_statement()

실행 결과:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
50,000원이 입금되었습니다. (잔액: 60,000원)
20,000원이 출금되었습니다. (잔액: 40,000원)
30,000원이 입금되었습니다. (잔액: 70,000원)
잔액이 부족합니다. (현재 잔액: 70,000원)
홍길동님의 잔액: 70,000원

==================================================
파이썬 은행 - 거래 내역서
==================================================
예금주: 홍길동
계좌번호: 123-456-789
--------------------------------------------------
거래 내역:
  1. 입금: +50,000원
  2. 출금: -20,000원
  3. 입금: +30,000원
--------------------------------------------------
현재 잔액: 70,000원
==================================================

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class Character:
    """게임 캐릭터를 표현하는 클래스"""

    def create(self, name, char_class, level=1):
        """캐릭터 생성"""
        self.name = name
        self.char_class = char_class
        self.level = level
        self.hp = 100
        self.max_hp = 100
        self.exp = 0
        self.exp_to_next_level = 100

    def take_damage(self, damage):
        """피해 입기"""
        self.hp = max(0, self.hp - damage)

        if self.hp == 0:
            return f"{self.name}이(가) 쓰러졌습니다!"
        else:
            return f"{self.name}이(가) {damage}의 피해를 입었습니다. (HP: {self.hp}/{self.max_hp})"

    def heal(self, amount):
        """회복"""
        old_hp = self.hp
        self.hp = min(self.max_hp, self.hp + amount)
        healed = self.hp - old_hp
        return f"{self.name}이(가) {healed}만큼 회복했습니다. (HP: {self.hp}/{self.max_hp})"

    def gain_exp(self, exp):
        """경험치 획득"""
        self.exp += exp
        message = f"{self.name}이(가) {exp} 경험치를 획득했습니다."

        # 레벨업 체크
        if self.exp >= self.exp_to_next_level:
            self.level_up()
            message += f"\n🎉 레벨 업! {self.name}의 레벨이 {self.level}이 되었습니다!"

        return message

    def level_up(self):
        """레벨업"""
        self.level += 1
        self.exp -= self.exp_to_next_level
        self.exp_to_next_level = int(self.exp_to_next_level * 1.5)

        # 스탯 증가
        self.max_hp += 20
        self.hp = self.max_hp

    def get_status(self):
        """상태 확인"""
        status = f"""
{'='*40}
[캐릭터 정보]
이름: {self.name}
직업: {self.char_class}
레벨: {self.level}
HP: {self.hp}/{self.max_hp}
경험치: {self.exp}/{self.exp_to_next_level}
{'='*40}
        """
        return status.strip()

# 사용 예시
player = Character()
player.create("용사", "전사")

print(player.get_status())
print("\n" + "="*40)
print("전투 시작!")
print("="*40 + "\n")

print(player.take_damage(30))
print(player.take_damage(20))
print(player.heal(15))
print(player.gain_exp(80))
print(player.gain_exp(50))  # 레벨업!

print("\n" + player.get_status())

실행 결과:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
========================================
[캐릭터 정보]
이름: 용사
직업: 전사
레벨: 1
HP: 100/100
경험치: 0/100
========================================

========================================
전투 시작!
========================================

용사이(가) 30의 피해를 입었습니다. (HP: 70/100)
용사이(가) 20의 피해를 입었습니다. (HP: 50/100)
용사이(가) 15만큼 회복했습니다. (HP: 65/100)
용사이(가) 80 경험치를 획득했습니다.
용사이(가) 50 경험치를 획득했습니다.
🎉 레벨 업! 용사의 레벨이 2이 되었습니다!

========================================
[캐릭터 정보]
이름: 용사
직업: 전사
레벨: 2
HP: 120/120
경험치: 30/150
========================================

클래스 속성 vs 인스턴스 속성

클래스 속성 (Class Attribute)

모든 인스턴스가 공유하는 속성입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Dog:
    species = "Canis familiaris"  # 클래스 속성

    def set_name(self, name):
        self.name = name  # 인스턴스 속성

dog1 = Dog()
dog2 = Dog()

print(dog1.species)  # Canis familiaris
print(dog2.species)  # Canis familiaris

# 클래스 속성 변경
Dog.species = "개과"
print(dog1.species)  # 개과 (모든 인스턴스에 영향!)
print(dog2.species)  # 개과

인스턴스 속성 (Instance Attribute)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog:
    def set_info(self, name, age):
        self.name = name  # 인스턴스 속성
        self.age = age    # 인스턴스 속성

dog1 = Dog()
dog1.set_info("멍멍이", 3)

dog2 = Dog()
dog2.set_info("바둑이", 5)

print(dog1.name, dog1.age)  # 멍멍이 3
print(dog2.name, dog2.age)  # 바둑이 5

혼합 사용 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Employee:
    company_name = "파이썬 주식회사"  # 클래스 속성
    employee_count = 0                # 클래스 속성

    def set_info(self, name, position):
        self.name = name              # 인스턴스 속성
        self.position = position      # 인스턴스 속성
        Employee.employee_count += 1  # 클래스 속성 증가

    def introduce(self):
        return f"{Employee.company_name}{self.position} {self.name}입니다."

emp1 = Employee()
emp1.set_info("홍길동", "개발자")

emp2 = Employee()
emp2.set_info("김철수", "디자이너")

print(emp1.introduce())
print(emp2.introduce())
print(f"총 직원 수: {Employee.employee_count}")

실행 결과:

1
2
3
파이썬 주식회사의 개발자 홍길동입니다.
파이썬 주식회사의 디자이너 김철수입니다.
총 직원 수: 2명

💡 핵심 실전 팁

OOP를 처음 배우는 분들을 위해 가장 중요한 10가지 핵심 팁만 정리했습니다. 이것만 잘 이해하고 실천하면 클래스를 자유롭게 사용할 수 있습니다!

Tip 1: self 빠뜨리지 않기 ⭐⭐⭐

클래스 메서드의 첫 번째 매개변수는 항상 self여야 합니다.

1
2
3
4
5
6
class Dog:
    def bark():  # ❌ self가 없음!
        return "멍멍!"

dog = Dog()
dog.bark()  # TypeError: bark() takes 0 positional arguments but 1 was given

해결:

1
2
3
class Dog:
    def bark(self):  # ✅ self 추가
        return "멍멍!"

Tip 2: self로 속성 접근하기 ⭐⭐⭐

인스턴스 속성을 사용할 때는 반드시 self.속성명 형태로 접근합니다.

1
2
3
4
5
6
class Dog:
    def set_name(self, name):
        name = name  # ❌ 인스턴스 속성에 저장 안 됨!

    def bark(self):
        return f"{name}: 멍멍!"  # NameError!

해결:

1
2
3
4
5
6
class Dog:
    def set_name(self, name):
        self.name = name  # ✅ self.name으로 저장

    def bark(self):
        return f"{self.name}: 멍멍!"  # ✅ self.name으로 접근

Tip 3: 클래스 이름은 PascalCase로 ⭐⭐

파이썬 커뮤니티의 표준 명명 규칙을 따릅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ 잘못된 명명
class my_class:
    pass

# ✅ 올바른 명명 (PEP 8 권장)
class MyClass:
    pass

class Student:
    pass

class BankAccount:
    pass

규칙: 클래스는 PascalCase, 함수/변수는 snake_case

Tip 4: __init__으로 초기화하기 ⭐⭐⭐

객체를 생성할 때 필요한 정보는 __init__ 메서드에서 받습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ❌ 비효율적인 방법
class Student:
    def set_info(self, name, age):
        self.name = name
        self.age = age

student = Student()
student.set_info("김철수", 20)  # 생성 후 별도로 설정

# ✅ 권장 방법
class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

student = Student("김철수", 20)  # 생성과 동시에 초기화

Tip 5: 메서드 vs 일반 함수 이해하기 ⭐⭐

둘의 차이를 명확히 알아야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 일반 함수 - 클래스 밖에서 정의
def calculate_area(width, height):
    return width * height

# 메서드 - 클래스 안에서 정의, self로 속성 접근
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

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

# 사용
area1 = calculate_area(10, 5)     # 함수: 인자 전달

rect = Rectangle(10, 5)
area2 = rect.calculate_area()     # 메서드: self로 자동 접근

Tip 6: 클래스 vs 함수 선택하기 ⭐⭐

모든 것을 클래스로 만들 필요는 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ❌ 불필요한 클래스 사용
class Calculator:
    def add(self, a, b):
        return a + b

# ✅ 간단한 연산은 함수로
def add(a, b):
    return a + b

# ✅ 상태를 유지해야 할 때는 클래스
class ShoppingCart:
    def __init__(self):
        self.items = []  # 상태 유지

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

    def get_total(self):
        return sum(item['price'] for item in self.items)

선택 기준:

  • 함수: 상태 없는 단순 연산 (계산기, 변환 함수)
  • 클래스: 상태를 유지해야 하는 경우 (장바구니, 게임 캐릭터)

Tip 7: 속성 명명 규칙 이해하기 ⭐

언더스코어(_)의 의미를 이해하면 코드가 명확해집니다.

1
2
3
4
5
6
7
8
9
10
11
12
class Student:
    def __init__(self, name, student_id):
        # ✅ 공개 속성: 일반 이름
        self.name = name
        self.student_id = student_id

        # ✅ 내부 사용: 언더스코어(_) 접두사
        self._grade = None

student = Student("김철수", "2024001")
print(student.name)         # ✅ 공개 속성 접근
print(student._grade)       # ⚠️ 가능하지만 "건드리지 마세요" 신호

규칙:

  • attribute: 공개 속성 (자유롭게 사용)
  • _attribute: 내부 사용 (접근하지 않는 것이 관례)

Tip 8: 메서드는 동사로 명명하기 ⭐

메서드 이름은 “무엇을 하는지” 명확하게 표현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    # ✅ 동작을 나타내는 동사 사용
    def deposit(self, amount):       # 입금하다
        self.balance += amount

    def withdraw(self, amount):      # 출금하다
        self.balance -= amount

    def get_balance(self):           # 잔액을 가져오다
        return self.balance

Tip 9: 클래스와 인스턴스 구분하기 ⭐⭐

클래스는 설계도, 인스턴스는 실제 객체입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog:
    species = ""  # 클래스 속성

# ❌ 잘못된 사용
print(Dog.name)  # AttributeError: 클래스에는 name 속성이 없음!

dog = Dog()
dog.name = "멍멍이"  # 인스턴스 속성 생성

# ✅ 올바른 사용
print(Dog.species)    # 클래스 속성은 클래스로 접근
print(dog.name)       # 인스턴스 속성은 인스턴스로 접근
print(dog.species)    # 클래스 속성도 인스턴스로 접근 가능

Tip 10: 단일 책임 원칙 지키기 ⭐⭐

하나의 클래스는 하나의 역할만 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# ❌ 너무 많은 책임
class Student:
    def __init__(self, name):
        self.name = name

    def study(self):              # 학생의 역할 ✅
        pass

    def save_to_database(self):   # DB의 역할 ❌
        pass

    def send_email(self):         # 이메일의 역할 ❌
        pass

# ✅ 단일 책임 원칙
class Student:
    def __init__(self, name):
        self.name = name

    def study(self):
        pass

class StudentRepository:  # DB 저장 전담
    def save(self, student):
        pass

class EmailService:  # 이메일 전담
    def send(self, student, message):
        pass

원칙: 클래스는 하나의 변경 이유만 가져야 합니다 (Single Responsibility Principle)


🧪 연습 문제

문제 1: 도서 관리 클래스

다음 요구사항을 만족하는 Book 클래스를 작성하세요:

요구사항:

  1. 책 정보(제목, 저자, ISBN, 가격)를 설정하는 메서드
  2. 할인율을 적용한 가격을 반환하는 메서드
  3. 책 정보를 보기 좋게 출력하는 메서드
1
2
3
4
5
# 사용 예시
book = Book()
book.set_info("파이썬 정복", "홍길동", "978-1234567890", 25000)
print(book.get_discounted_price(10))  # 10% 할인
book.print_info()

예상 출력:

1
2
3
4
5
6
7
22500.0
=====================================
제목: 파이썬 정복
저자: 홍길동
ISBN: 978-1234567890
가격: 25,000원
=====================================
💡 힌트
1
2
3
4
5
6
7
8
9
10
11
12
class Book:
    def set_info(self, title, author, isbn, price):
        # 인스턴스 속성 설정
        pass

    def get_discounted_price(self, discount_rate):
        # 할인된 가격 = 원가 * (1 - 할인율/100)
        pass

    def print_info(self):
        # 책 정보 출력
        pass
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Book:
    """도서 정보를 관리하는 클래스"""

    def set_info(self, title, author, isbn, price):
        """책 정보 설정"""
        self.title = title
        self.author = author
        self.isbn = isbn
        self.price = price

    def get_discounted_price(self, discount_rate):
        """할인된 가격 반환"""
        discount = self.price * (discount_rate / 100)
        return self.price - discount

    def print_info(self):
        """책 정보 출력"""
        print("=" * 37)
        print(f"제목: {self.title}")
        print(f"저자: {self.author}")
        print(f"ISBN: {self.isbn}")
        print(f"가격: {self.price:,}")
        print("=" * 37)

# 테스트
book = Book()
book.set_info("파이썬 정복", "홍길동", "978-1234567890", 25000)
print(book.get_discounted_price(10))
book.print_info()

문제 2: 쇼핑 카트 시스템

다음 요구사항을 만족하는 ShoppingCart 클래스를 작성하세요:

요구사항:

  1. 상품 추가 메서드 (상품명, 가격, 수량)
  2. 상품 제거 메서드 (상품명)
  3. 총 금액 계산 메서드
  4. 장바구니 내용 출력 메서드
1
2
3
4
5
6
7
8
# 사용 예시
cart = ShoppingCart()
cart.set_owner("홍길동")
cart.add_item("노트북", 1500000, 1)
cart.add_item("마우스", 30000, 2)
cart.add_item("키보드", 80000, 1)
cart.print_cart()
print(f"총 금액: {cart.get_total():,}")

예상 출력:

1
2
3
4
5
6
7
8
========================================
홍길동님의 장바구니
========================================
1. 노트북 - 1,500,000원 x 1개
2. 마우스 - 30,000원 x 2개
3. 키보드 - 80,000원 x 1개
========================================
총 금액: 1,640,000원
💡 힌트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ShoppingCart:
    def set_owner(self, owner):
        self.owner = owner
        self.items = []  # 상품 리스트

    def add_item(self, name, price, quantity):
        # 딕셔너리 형태로 items에 추가
        pass

    def remove_item(self, name):
        # items에서 해당 상품 제거
        pass

    def get_total(self):
        # 각 상품의 (가격 * 수량)을 모두 더하기
        pass

    def print_cart(self):
        # 장바구니 내용 출력
        pass
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class ShoppingCart:
    """쇼핑 카트를 관리하는 클래스"""

    def set_owner(self, owner):
        """카트 소유자 설정"""
        self.owner = owner
        self.items = []

    def add_item(self, name, price, quantity):
        """상품 추가"""
        self.items.append({
            "name": name,
            "price": price,
            "quantity": quantity
        })

    def remove_item(self, name):
        """상품 제거"""
        for item in self.items:
            if item["name"] == name:
                self.items.remove(item)
                return f"{name}이(가) 제거되었습니다."
        return f"{name}을(를) 찾을 수 없습니다."

    def get_total(self):
        """총 금액 계산"""
        total = 0
        for item in self.items:
            total += item["price"] * item["quantity"]
        return total

    def print_cart(self):
        """장바구니 출력"""
        print("=" * 40)
        print(f"{self.owner}님의 장바구니")
        print("=" * 40)

        if not self.items:
            print("장바구니가 비어있습니다.")
        else:
            for i, item in enumerate(self.items, 1):
                print(f"{i}. {item['name']} - {item['price']:,}원 x {item['quantity']}")

        print("=" * 40)

# 테스트
cart = ShoppingCart()
cart.set_owner("홍길동")
cart.add_item("노트북", 1500000, 1)
cart.add_item("마우스", 30000, 2)
cart.add_item("키보드", 80000, 1)
cart.print_cart()
print(f"총 금액: {cart.get_total():,}")

문제 3: 온도 변환기 클래스

섭씨, 화씨, 켈빈 온도를 관리하고 변환하는 Temperature 클래스를 작성하세요:

요구사항:

  1. 섭씨 온도를 설정하는 메서드
  2. 화씨로 변환하는 메서드 (°F = °C × 9/5 + 32)
  3. 켈빈으로 변환하는 메서드 (K = °C + 273.15)
  4. 모든 온도 정보를 출력하는 메서드
1
2
3
4
# 사용 예시
temp = Temperature()
temp.set_celsius(25)
temp.print_all()

예상 출력:

1
2
3
4
5
6
7
========================================
온도 정보
========================================
섭씨: 25.0°C
화씨: 77.0°F
켈빈: 298.15K
========================================
💡 힌트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Temperature:
    def set_celsius(self, celsius):
        self.celsius = celsius

    def to_fahrenheit(self):
        # 화씨 = 섭씨 × 9/5 + 32
        pass

    def to_kelvin(self):
        # 켈빈 = 섭씨 + 273.15
        pass

    def print_all(self):
        # 모든 온도 출력
        pass
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Temperature:
    """온도 변환을 관리하는 클래스"""

    def set_celsius(self, celsius):
        """섭씨 온도 설정"""
        self.celsius = celsius

    def to_fahrenheit(self):
        """화씨로 변환"""
        return self.celsius * 9/5 + 32

    def to_kelvin(self):
        """켈빈으로 변환"""
        return self.celsius + 273.15

    def print_all(self):
        """모든 온도 정보 출력"""
        print("=" * 40)
        print("온도 정보")
        print("=" * 40)
        print(f"섭씨: {self.celsius}°C")
        print(f"화씨: {self.to_fahrenheit()}°F")
        print(f"켈빈: {self.to_kelvin()}K")
        print("=" * 40)

# 테스트
temp = Temperature()
temp.set_celsius(25)
temp.print_all()

print()

temp2 = Temperature()
temp2.set_celsius(0)
temp2.print_all()

print()

temp3 = Temperature()
temp3.set_celsius(100)
temp3.print_all()

실행 결과:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
========================================
온도 정보
========================================
섭씨: 25°C
화씨: 77.0°F
켈빈: 298.15K
========================================

========================================
온도 정보
========================================
섭씨: 0°C
화씨: 32.0°F
켈빈: 273.15K
========================================

========================================
온도 정보
========================================
섭씨: 100°C
화씨: 212.0°F
켈빈: 373.15K
========================================

📝 오늘 배운 내용 정리

개념 설명 예시
클래스 객체를 만들기 위한 설계도 class Dog:
객체/인스턴스 클래스로 만든 실제 데이터 my_dog = Dog()
속성 객체가 가진 데이터 self.name = "멍멍이"
메서드 객체가 수행하는 동작 def bark(self):
self 현재 인스턴스를 가리키는 참조 self.name
클래스 속성 모든 인스턴스가 공유하는 속성 species = "개"
인스턴스 속성 각 인스턴스가 독립적으로 가지는 속성 self.name = name

클래스 작성 순서

  1. 클래스 정의 (class ClassName:)
  2. 독스트링 작성 (클래스 설명)
  3. 클래스 속성 정의 (필요한 경우)
  4. 메서드 작성 (항상 첫 번째 매개변수는 self)
  5. 객체 생성 및 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 템플릿
class MyClass:
    """클래스 설명"""

    class_attribute = "공유 속성"  # 클래스 속성

    def set_info(self, param1, param2):
        """정보 설정"""
        self.attribute1 = param1  # 인스턴스 속성
        self.attribute2 = param2

    def do_something(self):
        """동작 수행"""
        return f"{self.attribute1}이(가) {self.attribute2}을(를) 실행"

# 사용
obj = MyClass()
obj.set_info("값1", "값2")
print(obj.do_something())

❓ FAQ - 자주 묻는 질문

Q1: 클래스와 객체의 차이가 정확히 뭔가요?

A: 클래스는 설계도, 객체는 실제 제품입니다.

1
2
3
4
5
6
7
8
# 클래스 = 자동차 설계도
class Car:
    def __init__(self, color):
        self.color = color

# 객체(인스턴스) = 실제 자동차
my_car = Car("빨강")    # 빨간 자동차 1대
your_car = Car("파랑")  # 파란 자동차 1대

비유:

  • 클래스: 붕어빵 틀 (하나만 있으면 됨)
  • 객체: 붕어빵 (틀로 여러 개 만듦)

핵심:

  • 클래스 1개로 객체 여러 개 생성 가능
  • 각 객체는 독립적인 데이터를 가짐
Q2: 왜 객체지향을 사용해야 하나요? 함수만으로는 안 되나요?

A: 함수만으로도 가능하지만, 복잡한 프로그램은 객체지향이 훨씬 관리하기 쉽습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# ❌ 함수만 사용 - 데이터와 기능이 분리됨
student_name = "홍길동"
student_age = 20
student_score = 85

def print_student(name, age, score):
    print(f"{name}({age}세): {score}")

def update_score(score, delta):
    return score + delta

# 여러 학생이면 변수가 폭발적으로 증가!
student1_name = "홍길동"
student1_age = 20
student1_score = 85
student2_name = "김철수"
student2_age = 21
student2_score = 90
# ...

# ✅ 객체지향 - 데이터와 기능이 묶여 있음
class Student:
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score

    def print_info(self):
        print(f"{self.name}({self.age}세): {self.score}")

    def update_score(self, delta):
        self.score += delta

# 여러 학생도 간단!
student1 = Student("홍길동", 20, 85)
student2 = Student("김철수", 21, 90)

객체지향의 장점:

  1. 관련 데이터와 기능을 묶어 관리 (응집도 높음)
  2. 재사용성 (클래스 1개로 객체 여러 개 생성)
  3. 유지보수 용이 (변경사항이 클래스 안에서만 처리됨)
  4. 현실 세계 모델링 (게임 캐릭터, 은행 계좌 등)
Q3: 인스턴스, 객체, 오브젝트는 모두 같은 말인가요?

A: 거의 같은 의미지만, 미묘한 차이가 있습니다.

1
2
3
4
5
class Dog:
    def __init__(self, name):
        self.name = name

my_dog = Dog("뽀삐")

용어 정리:

  • 객체(Object): 일반적인 용어, 가장 넓은 의미
  • 인스턴스(Instance): 특정 클래스로 만든 객체를 강조할 때
  • 오브젝트: 객체의 영어 표현

예시:

  • my_dog객체입니다 (일반적 표현)
  • my_dogDog 클래스의 인스턴스입니다 (클래스 강조)
  • Python에서 모든 것은 객체입니다 (숫자, 문자열, 함수 등)

실무에서:

  • “Dog 클래스의 인스턴스를 생성했다” ✅
  • “Dog 객체를 만들었다” ✅
  • 혼용해도 대부분 문제없음
Q4: self는 왜 필요한가요? 생략하면 안 되나요?

A: self는 필수입니다. “이 객체 자신”을 가리키기 위해 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Counter:
    def __init__(self):
        self.count = 0  # self.count = 이 객체의 count

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

# 두 개의 독립적인 카운터
counter1 = Counter()
counter2 = Counter()

counter1.increment()  # 1 (counter1의 count)
counter1.increment()  # 2 (counter1의 count)

counter2.increment()  # 1 (counter2의 count)

self가 없으면?

1
2
3
4
5
6
class Counter:
    def __init__():  # ❌ 에러! self 필수
        count = 0  # 지역 변수 (객체 속성 아님)

    def increment():  # ❌ 에러! self 필수
        count += 1  # count를 찾을 수 없음

self의 역할:

  1. 객체의 속성 접근: self.name, self.age
  2. 객체의 메서드 호출: self.other_method()
  3. 여러 객체 구분: 각 객체가 독립적인 데이터 유지

이름 규칙:

  • self는 관례일 뿐, 다른 이름도 가능 (예: this, me)
  • 하지만 절대 self를 사용하세요! (Python 관례)
Q5: 클래스 이름은 왜 대문자로 시작하나요?

A: PEP 8 스타일 가이드에서 권장하는 Python 명명 규칙입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ✅ 올바른 명명
class Student:      # 클래스: 대문자로 시작 (파스칼 케이스)
    pass

class MyBankAccount:  # 여러 단어: 각 단어 대문자
    pass

# ✅ 올바른 명명
def calculate_sum():  # 함수: 소문자 + 언더스코어 (스네이크 케이스)
    pass

student1 = Student()  # 변수: 소문자

# ❌ 나쁜 예시
class student:  # 클래스인데 소문자
    pass

class my_bank_account:  # 클래스인데 스네이크 케이스
    pass

명명 규칙 요약:

  • 클래스: PascalCase (각 단어 첫 글자 대문자)
  • 함수/변수: snake_case (소문자 + 언더스코어)
  • 상수: UPPER_SNAKE_CASE (모두 대문자)

이유:

  1. 가독성: 코드를 보는 순간 클래스인지 함수인지 구분
  2. 국제 표준: 전 세계 Python 개발자들이 사용하는 관례
  3. 도구 지원: IDE가 자동완성, 경고 등에 활용
Q6: 속성을 직접 접근하는 것과 메서드로 접근하는 것의 차이는?

A: Python에서는 직접 접근도 가능하지만, 메서드를 사용하면 더 안전하고 유연합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def deposit(self, amount):
        """입금 메서드 - 검증 로직 포함"""
        if amount > 0:
            self.balance += amount
            print(f"{amount:,}원 입금 완료")
        else:
            print("❌ 양수만 입금 가능합니다")

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

account = BankAccount(10000)

# 방법 1: 직접 접근 (간단하지만 위험)
account.balance += 5000  # 검증 없이 변경됨
account.balance = -1000  # ❌ 음수 잔액도 가능!

# 방법 2: 메서드 사용 (안전)
account.deposit(5000)   # ✅ 검증 로직 실행
account.deposit(-1000)  # ❌ 자동으로 거부됨

메서드 사용의 장점:

  1. 검증 로직 추가 가능 (음수 방지, 범위 체크 등)
  2. 로깅/디버깅 (누가 언제 변경했는지 기록)
  3. 나중에 내부 구현 변경 가능 (외부 코드는 변경 불필요)
  4. 부수 효과 처리 (잔액 변경 시 알림 등)

Python의 특징:

  • Java처럼 private 강제 없음 (관례로 _balance 사용)
  • 간단한 속성은 직접 접근도 OK
  • 복잡한 로직 필요하면 메서드 사용
Q7: 하나의 파일에 여러 클래스를 정의할 수 있나요?

A: 네, 가능합니다! 하지만 관련 있는 클래스끼리 묶는 것이 좋습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# game.py - 게임 관련 클래스들
class Player:
    def __init__(self, name):
        self.name = name
        self.hp = 100

    def attack(self):
        return 10

class Enemy:
    def __init__(self, name):
        self.name = name
        self.hp = 50

    def attack(self):
        return 5

class Item:
    def __init__(self, name, effect):
        self.name = name
        self.effect = effect

# 사용
player = Player("용사")
enemy = Enemy("슬라임")
potion = Item("회복 포션", "HP +50")

파일 구성 가이드:

  • 작은 프로젝트: 관련 클래스를 한 파일에
  • 큰 프로젝트: 클래스별로 파일 분리
    1
    2
    3
    4
    
    game/
      player.py   (Player 클래스)
      enemy.py    (Enemy 클래스)
      item.py     (Item 클래스)
    

권장사항:

  1. 응집도 높은 클래스끼리 묶기 (Player, Enemy → game.py)
  2. 한 파일에 3-5개 클래스까지 (그 이상이면 분리 고려)
  3. 메인 클래스 이름으로 파일명 (BankAccount → bank_account.py)
Q8: 클래스를 언제 만들어야 하고, 언제 함수로 충분한가요?

A: 데이터와 기능이 함께 있어야 하면 클래스, 단순 처리만 필요하면 함수!

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ✅ 클래스 사용 - 상태(데이터)를 유지해야 함
class ShoppingCart:
    def __init__(self):
        self.items = []  # 상태 유지

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

    def get_total(self):
        return sum(item.price for item in self.items)

cart = ShoppingCart()
cart.add_item(apple)
cart.add_item(banana)
print(cart.get_total())  # 장바구니 상태가 유지됨

함수로 충분한 경우:

1
2
3
4
5
6
7
8
9
10
11
12
# ✅ 함수 사용 - 상태 유지 불필요, 단순 계산
def calculate_tax(price, tax_rate=0.1):
    """단순 계산, 상태 유지 불필요"""
    return price * tax_rate

def format_currency(amount):
    """단순 변환, 상태 유지 불필요"""
    return f"{amount:,}"

# 매번 독립적으로 실행
tax = calculate_tax(10000)
formatted = format_currency(10000)

판단 기준:

상황 클래스 함수
데이터 유지 필요
관련 기능 여러 개
단순 계산/변환
상태 변경 추적
여러 개 생성 필요

예시:

  • 클래스: 게임 캐릭터, 은행 계좌, 학생 정보, 파일 핸들러
  • 함수: 수학 계산, 문자열 포맷, 데이터 변환, 검증

: 처음에는 함수로 시작하고, 상태 관리가 복잡해지면 클래스로 리팩토링하세요!


🔗 관련 자료

📖 공식 문서

  • Python 공식 문서 - 클래스 파이썬 공식 튜토리얼의 클래스 섹션입니다. 클래스 정의, 인스턴스 객체, 메서드 객체, 클래스 변수와 인스턴스 변수 등 OOP의 기초부터 고급 개념까지 체계적으로 설명되어 있습니다.

  • Python 공식 문서 - 데이터 모델 (Special Methods) __init__, __str__, __repr__ 등 특수 메서드(매직 메서드)에 대한 완벽한 레퍼런스입니다. 각 메서드의 역할과 사용법을 상세히 설명합니다.

🎓 학습 자료

  • Real Python - Object-Oriented Programming (OOP) in Python 3 영문이지만 매우 친절하고 상세한 OOP 튜토리얼입니다. 클래스와 객체의 개념부터 상속, 다형성까지 실용적인 예제와 함께 설명합니다. 초보자도 따라하기 쉽게 구성되어 있습니다.

💡 베스트 프랙티스

  • PEP 8 - Style Guide for Python Code (Class Naming) 파이썬 코딩 스타일 가이드 중 클래스 명명 규칙 섹션입니다. PascalCase 사용, 예외 클래스 명명법 등 파이썬 커뮤니티의 표준 관례를 확인할 수 있습니다.

  • Google Python Style Guide - Classes 구글의 파이썬 스타일 가이드 중 클래스 관련 섹션입니다. 클래스 설계 원칙, docstring 작성법, 메서드 순서 등 실무에서 사용하는 모범 사례를 배울 수 있습니다.

📚 이전 학습

Day 30: 미니 프로젝트 - 계산기 만들기 ⭐⭐⭐

어제는 Phase 3의 모든 함수 개념을 활용해 계산기 프로젝트를 완성했습니다!

📚 다음 학습

Day 32: 생성자와 소멸자 (init, del) ⭐⭐⭐

내일은 __init__ 메서드로 객체를 초기화하고 __del__ 메서드로 객체를 정리하는 방법을 배웁니다!


“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀

Day 31/100 Phase 4: 객체지향 프로그래밍 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.