포스트

[Python 100일 챌린지] Day 40 - 미니 프로젝트: 도서 관리 시스템

[Python 100일 챌린지] Day 40 - 미니 프로젝트: 도서 관리 시스템

🎉 Phase 4 완료! 지금까지 배운 객체지향 프로그래밍의 모든 것을 활용해서 실전 도서 관리 시스템을 만들어봅시다! 클래스, 상속, 캡슐화, 다형성을 모두 적용한 완성도 높은 프로젝트로 OOP 마스터의 첫 걸음을 내딛어요!

🎯 오늘의 학습 목표

⭐⭐⭐⭐ (50-60분 완독)

📚 사전 지식


🎯 학습 목표 1: Phase 4 내용 복습하기

Phase 4에서 배운 내용

Day 31-34: 클래스 기초

  • 클래스와 객체의 개념
  • 생성자(init)와 소멸자(del)
  • 인스턴스/클래스/정적 메서드
  • self 매개변수의 역할

Day 35-36: 상속과 오버라이딩

  • 상속을 통한 코드 재사용
  • super()로 부모 클래스 접근
  • 메서드 오버라이딩으로 기능 확장
  • 다중 상속과 MRO

Day 37-39: 고급 OOP 개념

  • 캡슐화와 접근 제어
  • 프로퍼티로 속성 관리
  • 다형성과 덕 타이핑
  • 특수 메서드와 연산자 오버로딩

오늘 만들 프로젝트

도서 관리 시스템 - Phase 4의 모든 OOP 개념을 활용합니다!


🎯 학습 목표 2: 도서 관리 시스템 설계하기

프로젝트 목표

도서관 관리 시스템 구현:

  • 도서 등록, 대출, 반납 관리
  • 회원 관리 및 대출 이력 추적
  • 연체료 계산 및 통계 기능
  • 데이터 저장 및 로드

활용할 OOP 개념

graph TD
    A[Phase 4 OOP 개념] --> B[클래스와 객체]
    A --> C[생성자/소멸자]
    A --> D[인스턴스/클래스 변수]
    A --> E[상속]
    A --> F[메서드 오버라이딩]
    A --> G[캡슐화]
    A --> H[Property]
    A --> I[다형성]
    A --> J[특수 메서드]

    B --> K[Book, Member 클래스]
    C --> K
    D --> L[Library 클래스]
    E --> M[VIPMember 상속]
    F --> M
    G --> N[대출 정보 보호]
    H --> N
    I --> O[Payment 다형성]
    J --> P[연산자 오버로딩]

🎯 학습 목표 3: OOP 개념을 활용하여 프로젝트 구현하기

📁 프로젝트 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library_management/
│
├── models/
│   ├── __init__.py
│   ├── book.py          # 도서 클래스
│   ├── member.py        # 회원 클래스
│   └── loan.py          # 대출 클래스
│
├── library.py           # 도서관 메인 클래스
├── storage.py           # 데이터 저장/로드
├── main.py              # 실행 파일
└── data/
    ├── books.json
    └── members.json

📚 1. Book 클래스 (도서)

1.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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# models/book.py
from datetime import datetime

class Book:
    """도서 클래스"""

    # 클래스 변수
    _next_id = 1
    total_books = 0

    def __init__(self, title, author, isbn, category="일반"):
        """
        Args:
            title: 도서 제목
            author: 저자
            isbn: ISBN 번호
            category: 카테고리 (일반, IT, 소설, 과학 등)
        """
        self.__book_id = Book._next_id
        Book._next_id += 1
        Book.total_books += 1

        self.__title = title
        self.__author = author
        self.__isbn = isbn
        self.__category = category
        self.__is_available = True  # 대출 가능 여부
        self.__registered_at = datetime.now()

    # Property - 읽기 전용
    @property
    def book_id(self):
        return self.__book_id

    @property
    def title(self):
        return self.__title

    @property
    def author(self):
        return self.__author

    @property
    def isbn(self):
        return self.__isbn

    @property
    def category(self):
        return self.__category

    @property
    def is_available(self):
        return self.__is_available

    # 대출 상태 관리 (내부 메서드)
    def _set_available(self, status):
        """도서 대출 상태 변경 (Library에서만 호출)"""
        self.__is_available = status

    # 특수 메서드
    def __str__(self):
        """사용자용 표현"""
        status = "대출가능" if self.__is_available else "대출중"
        return f"[{self.__book_id}] {self.__title} - {self.__author} ({status})"

    def __repr__(self):
        """개발자용 표현"""
        return (f"Book(id={self.__book_id}, title='{self.__title}', "
                f"author='{self.__author}', isbn='{self.__isbn}')")

    def __eq__(self, other):
        """ISBN으로 동일 도서 판별"""
        if not isinstance(other, Book):
            return NotImplemented
        return self.__isbn == other.isbn

    def __hash__(self):
        """ISBN 기반 해시 (딕셔너리 키 사용 가능)"""
        return hash(self.__isbn)

    # 데이터 직렬화
    def to_dict(self):
        """JSON 저장용 딕셔너리 변환"""
        return {
            'book_id': self.__book_id,
            'title': self.__title,
            'author': self.__author,
            'isbn': self.__isbn,
            'category': self.__category,
            'is_available': self.__is_available,
            'registered_at': self.__registered_at.isoformat()
        }

    @classmethod
    def from_dict(cls, data):
        """딕셔너리에서 Book 객체 생성"""
        book = cls(
            title=data['title'],
            author=data['author'],
            isbn=data['isbn'],
            category=data.get('category', '일반')
        )
        book.__book_id = data['book_id']
        book.__is_available = data['is_available']
        book.__registered_at = datetime.fromisoformat(data['registered_at'])

        # 클래스 변수 갱신
        if book.book_id >= cls._next_id:
            cls._next_id = book.book_id + 1

        return book

👤 2. Member 클래스 (회원)

2.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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# models/member.py
from datetime import datetime

class Member:
    """도서관 회원 클래스"""

    # 클래스 변수
    _next_id = 1
    total_members = 0

    def __init__(self, name, email, phone):
        self.__member_id = Member._next_id
        Member._next_id += 1
        Member.total_members += 1

        self.__name = name
        self.__email = email
        self.__phone = phone
        self.__joined_at = datetime.now()
        self.__loan_history = []  # 대출 이력
        self.__max_books = 3       # 최대 대출 권수

    # Property
    @property
    def member_id(self):
        return self.__member_id

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

    @property
    def email(self):
        return self.__email

    @property
    def phone(self):
        return self.__phone

    @property
    def max_books(self):
        """최대 대출 가능 권수"""
        return self.__max_books

    @property
    def current_loans(self):
        """현재 대출 중인 도서 수"""
        return len([loan for loan in self.__loan_history if not loan.is_returned])

    def can_borrow(self):
        """대출 가능 여부"""
        return self.current_loans < self.max_books

    def add_loan(self, loan):
        """대출 이력 추가"""
        self.__loan_history.append(loan)

    def get_active_loans(self):
        """현재 대출 중인 도서 목록"""
        return [loan for loan in self.__loan_history if not loan.is_returned]

    def get_loan_history(self):
        """전체 대출 이력"""
        return self.__loan_history.copy()

    # 특수 메서드
    def __str__(self):
        return f"[{self.__member_id}] {self.__name} ({self.__email})"

    def __repr__(self):
        return (f"Member(id={self.__member_id}, name='{self.__name}', "
                f"email='{self.__email}')")

    def __eq__(self, other):
        if not isinstance(other, Member):
            return NotImplemented
        return self.__member_id == other.member_id

    def __hash__(self):
        return hash(self.__member_id)

    # 데이터 직렬화
    def to_dict(self):
        return {
            'member_id': self.__member_id,
            'name': self.__name,
            'email': self.__email,
            'phone': self.__phone,
            'joined_at': self.__joined_at.isoformat(),
            'max_books': self.__max_books
        }

    @classmethod
    def from_dict(cls, data):
        member = cls(
            name=data['name'],
            email=data['email'],
            phone=data['phone']
        )
        member.__member_id = data['member_id']
        member.__joined_at = datetime.fromisoformat(data['joined_at'])
        member.__max_books = data.get('max_books', 3)

        if member.member_id >= cls._next_id:
            cls._next_id = member.member_id + 1

        return member

2.2 VIP 회원 (상속)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class VIPMember(Member):
    """VIP 회원 (일반 회원 상속)"""

    def __init__(self, name, email, phone):
        super().__init__(name, email, phone)
        # VIP는 최대 대출 권수가 더 많음
        self._Member__max_books = 5
        self.__discount_rate = 0.2  # 연체료 20% 할인

    @property
    def discount_rate(self):
        return self.__discount_rate

    def can_borrow(self):
        """VIP는 추가 혜택 (오버라이딩)"""
        # VIP는 한 권 더 빌릴 수 있음
        return self.current_loans < (self.max_books + 1)

    def __str__(self):
        return f"[VIP {self.member_id}] {self.name} ({self.email})"

    def __repr__(self):
        return (f"VIPMember(id={self.member_id}, name='{self.name}', "
                f"email='{self.email}')")

📋 3. Loan 클래스 (대출 기록)

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# models/loan.py
from datetime import datetime, timedelta

class Loan:
    """대출 기록 클래스"""

    # 클래스 변수
    _next_id = 1
    LOAN_PERIOD = 14  # 대출 기간 (일)
    OVERDUE_FEE = 100  # 연체료 (일당)

    def __init__(self, book, member):
        self.__loan_id = Loan._next_id
        Loan._next_id += 1

        self.__book = book
        self.__member = member
        self.__loan_date = datetime.now()
        self.__due_date = self.__loan_date + timedelta(days=self.LOAN_PERIOD)
        self.__return_date = None
        self.__is_returned = False

    # Property
    @property
    def loan_id(self):
        return self.__loan_id

    @property
    def book(self):
        return self.__book

    @property
    def member(self):
        return self.__member

    @property
    def loan_date(self):
        return self.__loan_date

    @property
    def due_date(self):
        return self.__due_date

    @property
    def return_date(self):
        return self.__return_date

    @property
    def is_returned(self):
        return self.__is_returned

    def is_overdue(self):
        """연체 여부 확인"""
        if self.__is_returned:
            return False
        return datetime.now() > self.__due_date

    def days_overdue(self):
        """연체 일수"""
        if not self.is_overdue():
            return 0

        if self.__is_returned:
            overdue = (self.__return_date - self.__due_date).days
        else:
            overdue = (datetime.now() - self.__due_date).days

        return max(0, overdue)

    def calculate_fee(self, discount_rate=0):
        """연체료 계산"""
        overdue_days = self.days_overdue()
        if overdue_days == 0:
            return 0

        fee = overdue_days * self.OVERDUE_FEE
        return int(fee * (1 - discount_rate))

    def return_book(self):
        """반납 처리"""
        if self.__is_returned:
            raise ValueError("이미 반납된 도서입니다")

        self.__return_date = datetime.now()
        self.__is_returned = True
        self.__book._set_available(True)

        fee = self.calculate_fee(
            discount_rate=getattr(self.__member, 'discount_rate', 0)
        )

        return fee

    # 특수 메서드
    def __str__(self):
        status = "반납완료" if self.__is_returned else "대출중"
        overdue = f" (연체 {self.days_overdue()}일)" if self.is_overdue() else ""

        return (f"[{self.__loan_id}] {self.__book.title} - "
                f"{self.__member.name} ({status}{overdue})")

    def __repr__(self):
        return (f"Loan(id={self.__loan_id}, book_id={self.__book.book_id}, "
                f"member_id={self.__member.member_id})")

    # 비교 연산 (대출일 기준)
    def __lt__(self, other):
        if not isinstance(other, Loan):
            return NotImplemented
        return self.__loan_date < other.loan_date

    def __eq__(self, other):
        if not isinstance(other, Loan):
            return NotImplemented
        return self.__loan_id == other.loan_id

🏛️ 4. Library 클래스 (도서관)

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# library.py
from datetime import datetime
from models.book import Book
from models.member import Member, VIPMember
from models.loan import Loan

class Library:
    """도서관 관리 시스템"""

    def __init__(self, name):
        self.name = name
        self.__books = {}      # {book_id: Book}
        self.__members = {}    # {member_id: Member}
        self.__loans = []      # [Loan]

    # === 도서 관리 ===

    def add_book(self, title, author, isbn, category="일반"):
        """도서 등록"""
        book = Book(title, author, isbn, category)
        self.__books[book.book_id] = book
        print(f"✅ 도서 등록 완료: {book}")
        return book

    def find_book(self, keyword):
        """도서 검색 (제목 또는 저자)"""
        results = []
        keyword_lower = keyword.lower()

        for book in self.__books.values():
            if (keyword_lower in book.title.lower() or
                keyword_lower in book.author.lower()):
                results.append(book)

        return results

    def get_book_by_id(self, book_id):
        """도서 ID로 조회"""
        return self.__books.get(book_id)

    def list_books(self, available_only=False):
        """도서 목록"""
        books = list(self.__books.values())

        if available_only:
            books = [b for b in books if b.is_available]

        return sorted(books, key=lambda b: b.book_id)

    # === 회원 관리 ===

    def register_member(self, name, email, phone, vip=False):
        """회원 등록"""
        if vip:
            member = VIPMember(name, email, phone)
        else:
            member = Member(name, email, phone)

        self.__members[member.member_id] = member

        member_type = "VIP 회원" if vip else "일반 회원"
        print(f"{member_type} 등록 완료: {member}")
        return member

    def get_member_by_id(self, member_id):
        """회원 ID로 조회"""
        return self.__members.get(member_id)

    def list_members(self):
        """회원 목록"""
        return sorted(self.__members.values(), key=lambda m: m.member_id)

    # === 대출/반납 ===

    def borrow_book(self, member_id, book_id):
        """도서 대출"""
        member = self.get_member_by_id(member_id)
        book = self.get_book_by_id(book_id)

        # 유효성 검사
        if not member:
            raise ValueError(f"회원을 찾을 수 없습니다: ID {member_id}")

        if not book:
            raise ValueError(f"도서를 찾을 수 없습니다: ID {book_id}")

        if not book.is_available:
            raise ValueError(f"이미 대출 중인 도서입니다: {book.title}")

        if not member.can_borrow():
            max_books = member.max_books
            raise ValueError(
                f"대출 한도 초과: 최대 {max_books}권까지 대출 가능합니다"
            )

        # 대출 처리
        loan = Loan(book, member)
        self.__loans.append(loan)
        member.add_loan(loan)
        book._set_available(False)

        print(f"✅ 대출 완료: {book.title}{member.name}")
        print(f"   반납 기한: {loan.due_date.strftime('%Y-%m-%d')}")

        return loan

    def return_book(self, loan_id):
        """도서 반납"""
        loan = self.__find_loan_by_id(loan_id)

        if not loan:
            raise ValueError(f"대출 기록을 찾을 수 없습니다: ID {loan_id}")

        if loan.is_returned:
            raise ValueError("이미 반납된 도서입니다")

        # 반납 처리
        fee = loan.return_book()

        print(f"✅ 반납 완료: {loan.book.title}")

        if fee > 0:
            print(f"   ⚠️ 연체료: {fee:,}원 ({loan.days_overdue()}일 연체)")
        else:
            print("   연체료 없음")

        return fee

    def __find_loan_by_id(self, loan_id):
        """대출 ID로 대출 기록 찾기"""
        for loan in self.__loans:
            if loan.loan_id == loan_id:
                return loan
        return None

    def get_member_loans(self, member_id):
        """회원의 대출 내역"""
        member = self.get_member_by_id(member_id)
        if not member:
            return []
        return member.get_active_loans()

    def get_overdue_loans(self):
        """연체 도서 목록"""
        return [loan for loan in self.__loans
                if not loan.is_returned and loan.is_overdue()]

    # === 통계 ===

    def get_statistics(self):
        """도서관 통계"""
        total_books = len(self.__books)
        available_books = sum(1 for b in self.__books.values() if b.is_available)
        total_members = len(self.__members)
        vip_members = sum(1 for m in self.__members.values()
                         if isinstance(m, VIPMember))
        active_loans = sum(1 for loan in self.__loans if not loan.is_returned)
        overdue_loans = len(self.get_overdue_loans())
        total_loans = len(self.__loans)

        return {
            'total_books': total_books,
            'available_books': available_books,
            'borrowed_books': total_books - available_books,
            'total_members': total_members,
            'vip_members': vip_members,
            'active_loans': active_loans,
            'overdue_loans': overdue_loans,
            'total_loans': total_loans
        }

    def print_statistics(self):
        """통계 출력"""
        stats = self.get_statistics()

        print(f"\n📊 {self.name} 통계")
        print("=" * 50)
        print(f"📚 도서:")
        print(f"   • 총 도서: {stats['total_books']}")
        print(f"   • 대출 가능: {stats['available_books']}")
        print(f"   • 대출 중: {stats['borrowed_books']}")
        print(f"\n👥 회원:")
        print(f"   • 총 회원: {stats['total_members']}")
        print(f"   • VIP 회원: {stats['vip_members']}")
        print(f"\n📋 대출:")
        print(f"   • 현재 대출: {stats['active_loans']}")
        print(f"   • 연체 중: {stats['overdue_loans']}")
        print(f"   • 누적 대출: {stats['total_loans']}")
        print("=" * 50)

    # 특수 메서드
    def __len__(self):
        """도서관의 총 도서 수"""
        return len(self.__books)

    def __contains__(self, item):
        """도서 또는 회원 존재 확인"""
        if isinstance(item, Book):
            return item.book_id in self.__books
        elif isinstance(item, Member):
            return item.member_id in self.__members
        return False

    def __str__(self):
        return f"{self.name} (도서 {len(self.__books)}권, 회원 {len(self.__members)}명)"

💾 5. Storage 클래스 (데이터 저장)

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
# storage.py
import json
import os
from models.book import Book
from models.member import Member, VIPMember

class Storage:
    """데이터 저장/로드"""

    def __init__(self, data_dir="data"):
        self.data_dir = data_dir
        os.makedirs(data_dir, exist_ok=True)

        self.books_file = os.path.join(data_dir, "books.json")
        self.members_file = os.path.join(data_dir, "members.json")

    def save_books(self, books):
        """도서 데이터 저장"""
        data = [book.to_dict() for book in books.values()]

        with open(self.books_file, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

        print(f"💾 도서 데이터 저장 완료: {len(data)}")

    def load_books(self):
        """도서 데이터 로드"""
        if not os.path.exists(self.books_file):
            return {}

        with open(self.books_file, 'r', encoding='utf-8') as f:
            data = json.load(f)

        books = {}
        for book_data in data:
            book = Book.from_dict(book_data)
            books[book.book_id] = book

        print(f"📂 도서 데이터 로드 완료: {len(books)}")
        return books

    def save_members(self, members):
        """회원 데이터 저장"""
        data = []
        for member in members.values():
            member_data = member.to_dict()
            member_data['is_vip'] = isinstance(member, VIPMember)
            data.append(member_data)

        with open(self.members_file, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

        print(f"💾 회원 데이터 저장 완료: {len(data)}")

    def load_members(self):
        """회원 데이터 로드"""
        if not os.path.exists(self.members_file):
            return {}

        with open(self.members_file, 'r', encoding='utf-8') as f:
            data = json.load(f)

        members = {}
        for member_data in data:
            is_vip = member_data.pop('is_vip', False)

            if is_vip:
                member = VIPMember.from_dict(member_data)
            else:
                member = Member.from_dict(member_data)

            members[member.member_id] = member

        print(f"📂 회원 데이터 로드 완료: {len(members)}")
        return members

🎮 6. 메인 프로그램

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# main.py
from library import Library
from storage import Storage

def print_menu():
    """메뉴 출력"""
    print("\n" + "=" * 50)
    print("📚 도서관 관리 시스템")
    print("=" * 50)
    print("1. 도서 등록")
    print("2. 도서 검색")
    print("3. 도서 목록")
    print("4. 회원 등록")
    print("5. 회원 목록")
    print("6. 도서 대출")
    print("7. 도서 반납")
    print("8. 연체 도서 확인")
    print("9. 통계 보기")
    print("0. 종료")
    print("=" * 50)

def add_book(library):
    """도서 등록"""
    print("\n📚 도서 등록")
    title = input("제목: ")
    author = input("저자: ")
    isbn = input("ISBN: ")
    category = input("카테고리 (기본: 일반): ") or "일반"

    try:
        library.add_book(title, author, isbn, category)
    except Exception as e:
        print(f"❌ 오류: {e}")

def search_books(library):
    """도서 검색"""
    print("\n🔍 도서 검색")
    keyword = input("검색어 (제목 또는 저자): ")

    results = library.find_book(keyword)

    if not results:
        print("검색 결과가 없습니다.")
        return

    print(f"\n검색 결과: {len(results)}")
    for book in results:
        print(f"  {book}")

def list_books(library):
    """도서 목록"""
    print("\n📖 도서 목록")
    available_only = input("대출 가능 도서만? (y/N): ").lower() == 'y'

    books = library.list_books(available_only=available_only)

    if not books:
        print("등록된 도서가 없습니다.")
        return

    print(f"\n{len(books)}권:")
    for book in books:
        print(f"  {book}")

def register_member(library):
    """회원 등록"""
    print("\n👤 회원 등록")
    name = input("이름: ")
    email = input("이메일: ")
    phone = input("전화번호: ")
    vip = input("VIP 회원? (y/N): ").lower() == 'y'

    try:
        library.register_member(name, email, phone, vip=vip)
    except Exception as e:
        print(f"❌ 오류: {e}")

def list_members(library):
    """회원 목록"""
    print("\n👥 회원 목록")
    members = library.list_members()

    if not members:
        print("등록된 회원이 없습니다.")
        return

    print(f"\n{len(members)}명:")
    for member in members:
        active_loans = len(library.get_member_loans(member.member_id))
        print(f"  {member} - 대출 중: {active_loans}")

def borrow_book(library):
    """도서 대출"""
    print("\n📤 도서 대출")

    try:
        member_id = int(input("회원 ID: "))
        book_id = int(input("도서 ID: "))

        library.borrow_book(member_id, book_id)
    except ValueError as e:
        print(f"❌ 오류: {e}")
    except Exception as e:
        print(f"❌ 예상치 못한 오류: {e}")

def return_book(library):
    """도서 반납"""
    print("\n📥 도서 반납")

    # 회원 ID로 대출 목록 조회
    try:
        member_id = int(input("회원 ID: "))
        loans = library.get_member_loans(member_id)

        if not loans:
            print("대출 중인 도서가 없습니다.")
            return

        print("\n대출 중인 도서:")
        for loan in loans:
            print(f"  [{loan.loan_id}] {loan.book.title} "
                  f"(반납기한: {loan.due_date.strftime('%Y-%m-%d')})")

        loan_id = int(input("\n반납할 대출 ID: "))
        library.return_book(loan_id)

    except ValueError as e:
        print(f"❌ 오류: {e}")
    except Exception as e:
        print(f"❌ 예상치 못한 오류: {e}")

def check_overdue(library):
    """연체 도서 확인"""
    print("\n⚠️ 연체 도서")
    overdue_loans = library.get_overdue_loans()

    if not overdue_loans:
        print("연체 도서가 없습니다.")
        return

    print(f"\n{len(overdue_loans)}건:")
    for loan in overdue_loans:
        fee = loan.calculate_fee(
            discount_rate=getattr(loan.member, 'discount_rate', 0)
        )
        print(f"  {loan} - 연체료: {fee:,}")

def main():
    """메인 함수"""
    library = Library("중앙 도서관")
    storage = Storage()

    print("\n📚 도서관 관리 시스템을 시작합니다...")

    # 데이터 로드 시도 (있으면)
    # library._Library__books = storage.load_books()
    # library._Library__members = storage.load_members()

    while True:
        print_menu()
        choice = input("\n선택: ")

        if choice == '1':
            add_book(library)
        elif choice == '2':
            search_books(library)
        elif choice == '3':
            list_books(library)
        elif choice == '4':
            register_member(library)
        elif choice == '5':
            list_members(library)
        elif choice == '6':
            borrow_book(library)
        elif choice == '7':
            return_book(library)
        elif choice == '8':
            check_overdue(library)
        elif choice == '9':
            library.print_statistics()
        elif choice == '0':
            print("\n종료합니다...")
            # 데이터 저장
            # storage.save_books(library._Library__books)
            # storage.save_members(library._Library__members)
            break
        else:
            print("❌ 잘못된 선택입니다.")

if __name__ == "__main__":
    main()

🎯 학습 목표 4: 테스트와 확장으로 프로젝트 마무리하기

🧪 테스트 시나리오

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# test_library.py
from library import Library
from datetime import datetime, timedelta

def test_scenario():
    """전체 시나리오 테스트"""
    print("🧪 도서관 시스템 테스트 시작\n")

    # 도서관 생성
    library = Library("테스트 도서관")

    # 1. 도서 등록
    print("=" * 50)
    print("1️⃣ 도서 등록 테스트")
    print("=" * 50)

    book1 = library.add_book("클린 코드", "로버트 C. 마틴", "978-8966260959", "IT")
    book2 = library.add_book("이펙티브 파이썬", "브렛 슬랫킨", "978-8966262861", "IT")
    book3 = library.add_book("해리 포터", "J.K. 롤링", "978-8983920775", "소설")

    # 2. 회원 등록
    print("\n" + "=" * 50)
    print("2️⃣ 회원 등록 테스트")
    print("=" * 50)

    alice = library.register_member("Alice", "alice@example.com", "010-1111-1111")
    bob = library.register_member("Bob", "bob@example.com", "010-2222-2222", vip=True)

    # 3. 도서 대출
    print("\n" + "=" * 50)
    print("3️⃣ 도서 대출 테스트")
    print("=" * 50)

    loan1 = library.borrow_book(alice.member_id, book1.book_id)
    loan2 = library.borrow_book(bob.member_id, book2.book_id)
    loan3 = library.borrow_book(bob.member_id, book3.book_id)

    # 4. 대출 한도 테스트
    print("\n" + "=" * 50)
    print("4️⃣ 대출 한도 테스트")
    print("=" * 50)

    # Alice는 일반 회원 (최대 3권)
    print(f"Alice 대출 가능? {alice.can_borrow()}")
    print(f"Alice 현재 대출: {alice.current_loans}/{alice.max_books}")

    # Bob은 VIP (최대 5+1권)
    print(f"Bob 대출 가능? {bob.can_borrow()}")
    print(f"Bob 현재 대출: {bob.current_loans}/{bob.max_books}")

    # 5. 도서 검색
    print("\n" + "=" * 50)
    print("5️⃣ 도서 검색 테스트")
    print("=" * 50)

    results = library.find_book("파이썬")
    print(f"'파이썬' 검색 결과:")
    for book in results:
        print(f"  {book}")

    # 6. 도서 반납
    print("\n" + "=" * 50)
    print("6️⃣ 도서 반납 테스트")
    print("=" * 50)

    fee = library.return_book(loan1.loan_id)
    print(f"반납 완료! 연체료: {fee}")

    # 7. 통계
    print("\n" + "=" * 50)
    print("7️⃣ 통계 테스트")
    print("=" * 50)

    library.print_statistics()

    # 8. 연체 시뮬레이션 (테스트용)
    print("\n" + "=" * 50)
    print("8️⃣ 연체 테스트 (시뮬레이션)")
    print("=" * 50)

    # loan2의 반납 기한을 과거로 설정 (테스트)
    loan2._Loan__due_date = datetime.now() - timedelta(days=5)

    print(f"연체 여부: {loan2.is_overdue()}")
    print(f"연체 일수: {loan2.days_overdue()}")
    print(f"연체료 (일반): {loan2.calculate_fee()}")
    print(f"연체료 (VIP 20% 할인): {loan2.calculate_fee(0.2)}")

    overdue_loans = library.get_overdue_loans()
    print(f"\n연체 도서: {len(overdue_loans)}")
    for loan in overdue_loans:
        print(f"  {loan}")

    print("\n✅ 테스트 완료!")

if __name__ == "__main__":
    test_scenario()

🚀 확장 아이디어

프로젝트를 더 발전시킬 수 있는 아이디어:

1. 도서 예약 시스템

1
2
3
4
5
6
class Reservation:
    """도서 예약"""
    def __init__(self, book, member):
        self.book = book
        self.member = member
        self.reserved_at = datetime.now()

2. 리뷰 시스템

1
2
3
4
5
6
7
class Review:
    """도서 리뷰"""
    def __init__(self, book, member, rating, comment):
        self.book = book
        self.member = member
        self.rating = rating  # 1-5
        self.comment = comment

3. 알림 시스템

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, member, message):
        pass

class EmailNotification(Notification):
    def send(self, member, message):
        print(f"📧 이메일 발송: {member.email} - {message}")

class SMSNotification(Notification):
    def send(self, member, message):
        print(f"📱 SMS 발송: {member.phone} - {message}")

4. GUI 추가

1
2
3
4
5
6
7
8
9
10
import tkinter as tk
from tkinter import ttk

class LibraryGUI:
    """도서관 GUI"""
    def __init__(self, library):
        self.library = library
        self.root = tk.Tk()
        self.root.title("도서관 관리 시스템")
        # GUI 구현...

🎯 학습 목표 5: Phase 4 총정리 및 성과 확인하기

Day 31-40 복습

Phase 4에서 다룬 OOP 개념:

  1. Day 31 - 클래스와 객체 기초
    • 클래스 정의, 객체 생성
    • 프로젝트 적용: Book, Member, Loan 클래스
  2. Day 32 - 생성자와 소멸자
    • __init__, __del__
    • 프로젝트 적용: 모든 클래스의 초기화
  3. Day 33 - 인스턴스 변수와 메서드
    • 인스턴스별 데이터 관리
    • 프로젝트 적용: 각 도서/회원의 고유 정보
  4. Day 34 - 클래스 변수와 클래스 메서드
    • 공유 데이터, @classmethod, @staticmethod
    • 프로젝트 적용: _next_id, total_books, from_dict()
  5. Day 35 - 상속
    • 코드 재사용, 계층 구조
    • 프로젝트 적용: VIPMember extends Member
  6. Day 36 - 메서드 오버라이딩
    • 부모 메서드 재정의
    • 프로젝트 적용: VIPMember.can_borrow() 오버라이딩
  7. Day 37 - 캡슐화와 정보 은닉
    • Private 변수, @property
    • 프로젝트 적용: __ prefix, Property 데코레이터
  8. Day 38 - 다형성
    • 하나의 인터페이스, 여러 구현
    • 프로젝트 적용: MemberVIPMember 다형적 처리
  9. Day 39 - 특수 메서드
    • __str__, __repr__, __eq__
    • 프로젝트 적용: 모든 클래스에 특수 메서드 구현
  10. Day 40 - 프로젝트 통합
    • 모든 개념을 활용한 실전 시스템 구축

💡 실전 팁 & 주의사항

Tip 1: 클래스 설계는 단순하게

처음부터 완벽한 설계를 하려고 하지 말고, 필요한 기능부터 구현하세요.

1
2
3
4
5
6
7
# ✅ 단순하게 시작
class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

# 나중에 필요할 때 확장

Tip 2: Private 변수로 데이터 보호

중요한 데이터는 Private 변수로 보호하고 Property로 접근하세요.

1
2
3
4
5
6
7
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Private

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

Tip 3: 상속은 “is-a” 관계일 때만

“VIPMember is a Member” 관계일 때 상속을 사용하세요.

1
2
3
# ✅ VIPMember is a Member
class VIPMember(Member):
    pass

📝 오늘 배운 내용 정리

OOP 개념 프로젝트 적용 예시
클래스와 객체 Book, Member, Library 클래스 설계
생성자 __init__으로 초기화
캡슐화 @property로 읽기 전용 속성 관리
상속 VIPMemberMember 확장
다형성 Member 타입으로 일반/VIP 통합 처리
특수 메서드 __str__, __repr__, __eq__, __lt__
클래스 메서드 from_dict() 팩토리 메서드

Phase 4 학습 성과

객체지향 설계 능력:

  • 실세계 문제를 클래스로 모델링
  • 적절한 책임 분리 (Book, Member, Library)
  • 확장 가능한 구조 설계

OOP 4대 원칙 적용:

  • 추상화: 핵심 개념만 클래스로 표현
  • 캡슐화: Property로 데이터 보호
  • 상속: VIPMember로 기능 확장
  • 다형성: 공통 인터페이스로 다양한 타입 처리

Pythonic 코드 작성:

  • 특수 메서드로 내장 타입처럼 동작
  • Context Manager 패턴 이해
  • 클래스 메서드로 유연한 객체 생성

🔗 관련 자료

📚 이전 학습

Day 39: 특수 메서드 (str, repr 등) ⭐⭐⭐⭐

어제는 특수 메서드의 개념과 활용, str__과 __repr, call 호출 가능 객체, enter__와 __exit (Context Manager)를 배웠습니다!

📚 다음 학습

Day 41: 파일 입출력 기초 ⭐⭐⭐

🎉 Phase 4 완료! 축하합니다! 객체지향 프로그래밍의 모든 핵심 개념을 학습하고 실전 프로젝트까지 완성했습니다.

Phase 5 (파일과 데이터 처리)에서는 파일 입출력 (텍스트, CSV, JSON), 예외 처리와 디버깅, 모듈과 패키지 관리, 외부 라이브러리 활용을 배웁니다!


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

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