포스트

[이제와서 시작하는 Python 마스터하기 #9] 모듈과 패키지 관리

[이제와서 시작하는 Python 마스터하기 #9] 모듈과 패키지 관리

🎨 5분만에 만드는 나만의 유틸리티 패키지

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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# 프로젝트 구조
"""
my_toolkit/
│
├── __init__.py
├── validators.py      # 유효성 검사 모듈
├── formatters.py      # 포맷팅 모듈
├── calculators.py     # 계산 모듈
└── security.py        # 보안 모듈
"""

# validators.py - 유효성 검사 모듈
import re
from typing import Optional

def validate_korean_phone(phone: str) -> bool:
    """한국 휴대폰 번호 유효성 검사"""
    pattern = r'^01[0-9]-?[0-9]{3,4}-?[0-9]{4}$'
    phone = phone.replace('-', '').replace(' ', '')
    return bool(re.match(pattern, phone))

def validate_email(email: str) -> bool:
    """이메일 유효성 검사"""
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

def validate_korean_id(id_number: str) -> bool:
    """한국 주민등록번호 유효성 검사 (간소화)"""
    if not re.match(r'^\d{6}-?[1-4]\d{6}$', id_number):
        return False

    id_number = id_number.replace('-', '')

    # 체크섬 검증
    weights = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5]
    total = sum(int(id_number[i]) * weights[i] for i in range(12))
    check_digit = (11 - (total % 11)) % 10

    return check_digit == int(id_number[12])

def validate_business_number(biz_no: str) -> bool:
    """사업자등록번호 유효성 검사"""
    biz_no = biz_no.replace('-', '')
    if len(biz_no) != 10 or not biz_no.isdigit():
        return False

    weights = [1, 3, 7, 1, 3, 7, 1, 3, 5]
    total = sum(int(biz_no[i]) * weights[i] for i in range(9))
    total += int(biz_no[8]) * 5 // 10

    return (10 - (total % 10)) % 10 == int(biz_no[9])


# formatters.py - 포맷팅 모듈
from datetime import datetime
from typing import Union

def format_korean_currency(amount: Union[int, float]) -> str:
    """한국 통화 포맷팅 (1,234,567원)"""
    return f"{amount:,.0f}"

def format_phone_number(phone: str, style: str = 'dash') -> str:
    """전화번호 포맷팅"""
    phone = ''.join(filter(str.isdigit, phone))

    if len(phone) == 11:  # 010-1234-5678
        if style == 'dash':
            return f"{phone[:3]}-{phone[3:7]}-{phone[7:]}"
        elif style == 'dot':
            return f"{phone[:3]}.{phone[3:7]}.{phone[7:]}"
        elif style == 'space':
            return f"{phone[:3]} {phone[3:7]} {phone[7:]}"
    elif len(phone) == 10:  # 02-1234-5678
        if phone[:2] == '02':
            return f"{phone[:2]}-{phone[2:6]}-{phone[6:]}"
        else:  # 031-123-4567
            return f"{phone[:3]}-{phone[3:6]}-{phone[6:]}"

    return phone

def format_date_korean(date: datetime) -> str:
    """날짜를 한국식으로 포맷팅"""
    weekdays = ['', '', '', '', '', '', '']
    weekday = weekdays[date.weekday()]
    return f"{date.year}{date.month}{date.day}일 ({weekday})"

def format_file_size(size_bytes: int) -> str:
    """파일 크기 포맷팅"""
    for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
        if size_bytes < 1024.0:
            return f"{size_bytes:.1f} {unit}"
        size_bytes /= 1024.0
    return f"{size_bytes:.1f} PB"


# calculators.py - 계산 모듈
from datetime import datetime, date
from typing import Tuple

def calculate_korean_age(birth_date: date) -> int:
    """한국 나이 계산 (세는 나이)"""
    today = date.today()
    return today.year - birth_date.year + 1

def calculate_interest(principal: float, rate: float, months: int) -> Tuple[float, float]:
    """이자 계산 (단리/복리)"""
    # 단리
    simple_interest = principal * (rate / 100) * (months / 12)
    simple_total = principal + simple_interest

    # 복리
    monthly_rate = rate / 100 / 12
    compound_total = principal * (1 + monthly_rate) ** months
    compound_interest = compound_total - principal

    return simple_total, compound_total

def calculate_bmi(weight: float, height: float) -> Tuple[float, str]:
    """BMI 계산 및 평가"""
    bmi = weight / (height / 100) ** 2

    if bmi < 18.5:
        category = "저체중"
    elif bmi < 23:
        category = "정상"
    elif bmi < 25:
        category = "과체중"
    elif bmi < 30:
        category = "비만"
    else:
        category = "고도비만"

    return round(bmi, 1), category

def calculate_tax(income: int) -> Tuple[int, int, float]:
    """소득세 계산 (간소화 버전)"""
    if income <= 12000000:
        tax = int(income * 0.06)
        rate = 6
    elif income <= 46000000:
        tax = 720000 + int((income - 12000000) * 0.15)
        rate = 15
    elif income <= 88000000:
        tax = 5820000 + int((income - 46000000) * 0.24)
        rate = 24
    else:
        tax = 15900000 + int((income - 88000000) * 0.35)
        rate = 35

    after_tax = income - tax
    return tax, after_tax, rate


# security.py - 보안 모듈
import hashlib
import secrets
import string
from typing import Optional

def generate_password(length: int = 12, include_special: bool = True) -> str:
    """안전한 비밀번호 생성"""
    chars = string.ascii_letters + string.digits
    if include_special:
        chars += string.punctuation

    # 최소 요구사항 보장
    password = [
        secrets.choice(string.ascii_lowercase),
        secrets.choice(string.ascii_uppercase),
        secrets.choice(string.digits)
    ]

    if include_special:
        password.append(secrets.choice(string.punctuation))

    # 나머지 문자 추가
    for _ in range(length - len(password)):
        password.append(secrets.choice(chars))

    # 셔플
    secrets.SystemRandom().shuffle(password)
    return ''.join(password)

def hash_password(password: str, salt: Optional[str] = None) -> Tuple[str, str]:
    """비밀번호 해싱"""
    if salt is None:
        salt = secrets.token_hex(32)

    pwd_hash = hashlib.pbkdf2_hmac(
        'sha256',
        password.encode('utf-8'),
        salt.encode('utf-8'),
        100000  # iterations
    )

    return pwd_hash.hex(), salt

def mask_sensitive_data(data: str, mask_char: str = '*', visible_chars: int = 4) -> str:
    """민감한 데이터 마스킹"""
    if len(data) <= visible_chars * 2:
        return mask_char * len(data)

    return data[:visible_chars] + mask_char * (len(data) - visible_chars * 2) + data[-visible_chars:]


# __init__.py - 패키지 초기화
"""
My Toolkit - 유용한 Python 유틸리티 모음
"""

__version__ = '1.0.0'
__author__ = '김파이썬'

# 편리한 접근을 위한 임포트
from .validators import (
    validate_korean_phone,
    validate_email,
    validate_korean_id,
    validate_business_number
)

from .formatters import (
    format_korean_currency,
    format_phone_number,
    format_date_korean,
    format_file_size
)

from .calculators import (
    calculate_korean_age,
    calculate_interest,
    calculate_bmi,
    calculate_tax
)

from .security import (
    generate_password,
    hash_password,
    mask_sensitive_data
)

# 사용 예제
def demo():
    """패키지 사용 데모"""
    from datetime import date

    print("🎨 My Toolkit 데모\n")

    # 유효성 검사
    phone = "010-1234-5678"
    print(f"휴대폰 {phone}: {validate_korean_phone(phone)}")

    # 포맷팅
    amount = 1234567
    print(f"금액: {format_korean_currency(amount)}")

    # 계산
    birth = date(1990, 1, 1)
    print(f"한국 나이: {calculate_korean_age(birth)}")

    # 보안
    password = generate_password()
    print(f"생성된 비밀번호: {password}")

# 실행
# demo()

📦 모듈과 패키지란?

모듈과 패키지는 Python 코드를 구조화하고 재사용 가능하게 만드는 핵심 요소입니다.

  • 모듈(Module): Python 코드가 들어있는 .py 파일
  • 패키지(Package): 여러 모듈을 포함하는 디렉토리 (__init__.py 파일 포함)

[!TIP] __init__.py가 뭔가요?

이 파일은 “이 디렉토리는 그냥 폴더가 아니라 Python 패키지야!”라고 알려주는 표지판입니다. Python 3.3부터는 없어도 패키지로 인식되긴 하지만, 호환성과 명시적인 초기화를 위해 만들어두는 것이 좋습니다. 비워둬도 괜찮습니다!

graph TD
    A[Python 프로젝트] --> B[모듈<br/>module.py]
    A --> C[패키지<br/>package/]
    C --> D[__init__.py]
    C --> E[module1.py]
    C --> F[module2.py]
    C --> G[서브패키지<br/>subpackage/]
    G --> H[__init__.py]
    G --> I[module3.py]

🔧 모듈 만들기와 사용하기

모듈 생성

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
# math_utils.py - 수학 유틸리티 모듈
"""수학 관련 유틸리티 함수들"""

import math

# 상수 정의
PI = 3.14159265359
E = 2.71828182846

def add(a, b):
    """두 수를 더합니다."""
    return a + b

def multiply(a, b):
    """두 수를 곱합니다."""
    return a * b

def factorial(n):
    """팩토리얼을 계산합니다."""
    if n < 0:
        raise ValueError("음수의 팩토리얼은 정의되지 않습니다.")
    return math.factorial(n)

def distance(x1, y1, x2, y2):
    """두 점 사이의 거리를 계산합니다."""
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

class Circle:
    """원을 나타내는 클래스"""
    
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return PI * self.radius ** 2
    
    def circumference(self):
        return 2 * PI * self.radius

# 모듈이 직접 실행될 때만 실행되는 코드
if __name__ == "__main__":
    print("math_utils 모듈 테스트")
    print(f"2 + 3 = {add(2, 3)}")
    print(f"4 * 5 = {multiply(4, 5)}")
    print(f"5! = {factorial(5)}")

모듈 임포트 방법

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
# 1. 모듈 전체 임포트
import math_utils

result = math_utils.add(5, 3)
circle = math_utils.Circle(5)
print(circle.area())

# 2. 모듈에 별칭 부여
import math_utils as mu

result = mu.multiply(4, 5)

# 3. 특정 함수/클래스만 임포트
from math_utils import add, Circle

result = add(10, 20)
circle = Circle(3)

# 4. 모든 것 임포트 (권장하지 않음)
from math_utils import *

result = multiply(3, 4)  # 네임스페이스 오염 위험

> [!WARNING]
> **(*) 조심하세요!**
>
> `from module import *` 모듈의 모든 것을 가져옵니다.
> 편해 보이지만,  코드에 있는 변수 이름과 겹쳐서 덮어써질 위험(네임스페이스 오염) 큽니다.
> 가능한  필요한 것만  집어서(`from module import a, b`) 가져오세요.
# 5. 여러 항목 임포트
from math_utils import (
    add, multiply,
    Circle, PI
)

모듈 검색 경로

1
2
3
4
5
6
7
8
9
10
11
12
import sys

# 모듈 검색 경로 확인
print("Python 모듈 검색 경로:")
for path in sys.path:
    print(f"  - {path}")

# 검색 경로 추가
sys.path.append('/custom/module/path')

# 또는 PYTHONPATH 환경 변수 설정
# export PYTHONPATH=/custom/module/path:$PYTHONPATH

[!TIP] ModuleNotFoundError가 뜬다면?

  1. 오타가 없는지 확인하세요. (대소문자 구분!)
  2. 실행하는 위치(현재 디렉토리)가 맞는지 확인하세요.
  3. 패키지를 설치했는지 확인하세요 (pip list).
  4. sys.path를 찍어보고 내 모듈이 있는 경로가 포함되어 있는지 확인하세요.

📁 패키지 구조와 생성

패키지 구조 예시

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
myproject/
│
├── main.py
│
├── utils/                    # 패키지
│   ├── __init__.py
│   ├── string_utils.py
│   ├── file_utils.py
│   └── date_utils.py
│
├── models/                   # 패키지
│   ├── __init__.py
│   ├── user.py
│   ├── product.py
│   └── order.py
│
└── services/                 # 패키지
    ├── __init__.py
    ├── auth/                 # 서브패키지
    │   ├── __init__.py
    │   ├── login.py
    │   └── register.py
    └── payment/              # 서브패키지
        ├── __init__.py
        ├── stripe.py
        └── paypal.py

init.py 파일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# utils/__init__.py
"""유틸리티 패키지"""

# 패키지 레벨에서 사용할 수 있도록 임포트
from .string_utils import capitalize_words, remove_spaces
from .file_utils import read_json, write_json
from .date_utils import format_date, days_between

# 패키지 메타데이터
__version__ = "1.0.0"
__author__ = "Your Name"
__all__ = [
    "capitalize_words", "remove_spaces",
    "read_json", "write_json",
    "format_date", "days_between"
]

# 패키지 초기화 코드
print(f"utils 패키지 v{__version__} 로드됨")

패키지 내 모듈 예시

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
# utils/string_utils.py
"""문자열 처리 유틸리티"""

import re

def capitalize_words(text):
    """각 단어의 첫 글자를 대문자로 변환"""
    return ' '.join(word.capitalize() for word in text.split())

def remove_spaces(text):
    """모든 공백 제거"""
    return re.sub(r'\s+', '', text)

def truncate(text, length, suffix='...'):
    """문자열을 지정된 길이로 자르기"""
    if len(text) <= length:
        return text
    return text[:length - len(suffix)] + suffix

def count_words(text):
    """단어 수 세기"""
    return len(text.split())

# utils/file_utils.py
"""파일 처리 유틸리티"""

import json
import csv
from pathlib import Path

def read_json(filepath):
    """JSON 파일 읽기"""
    with open(filepath, 'r', encoding='utf-8') as f:
        return json.load(f)

def write_json(data, filepath, indent=4):
    """JSON 파일 쓰기"""
    with open(filepath, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=indent)

def read_csv_as_dict(filepath):
    """CSV 파일을 딕셔너리 리스트로 읽기"""
    with open(filepath, 'r', encoding='utf-8') as f:
        return list(csv.DictReader(f))

def ensure_directory(path):
    """디렉토리가 없으면 생성"""
    Path(path).mkdir(parents=True, exist_ok=True)

# utils/date_utils.py
"""날짜 처리 유틸리티"""

from datetime import datetime, timedelta

def format_date(date, format_string="%Y-%m-%d"):
    """날짜 포맷팅"""
    if isinstance(date, str):
        date = datetime.fromisoformat(date)
    return date.strftime(format_string)

def days_between(date1, date2):
    """두 날짜 사이의 일수"""
    if isinstance(date1, str):
        date1 = datetime.fromisoformat(date1)
    if isinstance(date2, str):
        date2 = datetime.fromisoformat(date2)
    return abs((date2 - date1).days)

def add_days(date, days):
    """날짜에 일수 더하기"""
    if isinstance(date, str):
        date = datetime.fromisoformat(date)
    return date + timedelta(days=days)

패키지 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# main.py에서 패키지 사용

# 1. 패키지에서 특정 모듈 임포트
from utils import string_utils
text = string_utils.capitalize_words("hello world")

# 2. 패키지에서 특정 함수 임포트
from utils.string_utils import capitalize_words, truncate
text = capitalize_words("hello world")
short_text = truncate("This is a very long text", 10)

# 3. __init__.py에 정의된 것 임포트
from utils import read_json, write_json
data = read_json("config.json")

# 4. 서브패키지 임포트
from services.auth import login
from services.payment.stripe import process_payment

# 5. 상대 임포트 (패키지 내부에서)
# services/auth/login.py 내부에서
from . import register  # 같은 패키지
from .. import payment  # 상위 패키지
from ..payment import stripe  # 형제 패키지

🌐 표준 라이브러리 모듈

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
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
# os - 운영체제 인터페이스
import os

# 현재 작업 디렉토리
print(os.getcwd())

# 디렉토리 생성
os.makedirs("test/subdir", exist_ok=True)

# 파일 목록
files = os.listdir(".")

# 환경 변수
home = os.environ.get("HOME")

# sys - 시스템 관련
import sys

# Python 버전
print(sys.version)

# 명령줄 인자
print(sys.argv)

# 플랫폼
print(sys.platform)

# datetime - 날짜와 시간
from datetime import datetime, date, time, timedelta

now = datetime.now()
today = date.today()
current_time = time(14, 30, 0)

tomorrow = today + timedelta(days=1)
print(f"내일: {tomorrow}")

# random - 난수 생성
import random

# 난수
print(random.random())  # 0.0 ~ 1.0
print(random.randint(1, 100))  # 1 ~ 100

# 리스트에서 선택
choices = ["apple", "banana", "orange"]
print(random.choice(choices))
random.shuffle(choices)  # 섞기

# collections - 고급 자료구조
from collections import defaultdict, Counter, deque, namedtuple

# defaultdict
dd = defaultdict(list)
dd["key"].append("value")

# Counter
counter = Counter("hello world")
print(counter.most_common(3))

# deque
dq = deque([1, 2, 3])
dq.appendleft(0)
dq.append(4)

# namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)

# itertools - 반복자 도구
import itertools

# 조합
for combo in itertools.combinations([1, 2, 3, 4], 2):
    print(combo)

# 순열
for perm in itertools.permutations([1, 2, 3], 2):
    print(perm)

# 무한 반복자
for i in itertools.count(10, 2):  # 10부터 2씩 증가
    if i > 20:
        break
    print(i)

📦 패키지 관리 (pip)

pip 기본 사용법

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
# 패키지 설치
pip install package_name
pip install package_name==1.2.3  # 특정 버전
pip install package_name>=1.0,<2.0  # 버전 범위

# 여러 패키지 한 번에 설치
pip install requests numpy pandas

# requirements.txt로 설치
pip install -r requirements.txt

# 패키지 업그레이드
pip install --upgrade package_name

# 패키지 제거
pip uninstall package_name

# 설치된 패키지 목록
pip list
pip freeze  # requirements.txt 형식

# 패키지 정보 확인
pip show package_name

# 오래된 패키지 확인
pip list --outdated

requirements.txt 관리

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
# requirements.txt 생성
# pip freeze > requirements.txt

# requirements.txt 예시
"""
# 웹 프레임워크
flask==2.3.2
django>=4.0,<5.0

# 데이터 분석
numpy==1.24.3
pandas~=2.0.0  # 2.0.x 버전
matplotlib>=3.5.0

# API 클라이언트
requests[security]>=2.28.0
httpx~=0.24.0

# 테스팅
pytest>=7.0.0
pytest-cov

# 개발 도구
black  # 최신 버전
flake8
mypy

# Git에서 직접 설치
git+https://github.com/user/repo.git@branch#egg=package_name

# 로컬 패키지
-e ./local_package
"""

# 개발 의존성 분리
# requirements-dev.txt
"""
pytest
black
flake8
mypy
sphinx
"""

# 프로덕션 의존성
# requirements-prod.txt
"""
gunicorn
psycopg2-binary
redis
celery
"""

가상환경과 함께 사용

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
# 가상환경 생성 및 활성화
import subprocess
import sys
import os

def create_virtual_environment(env_name="venv"):
    """가상환경 생성"""
    subprocess.run([sys.executable, "-m", "venv", env_name])
    print(f"가상환경 '{env_name}' 생성 완료")

def install_requirements(requirements_file="requirements.txt"):
    """requirements.txt 설치"""
    subprocess.run([
        sys.executable, "-m", "pip", "install", "-r", requirements_file
    ])

# 프로젝트 초기화 스크립트
def setup_project():
    """프로젝트 환경 설정"""
    # 가상환경 생성
    create_virtual_environment()
    
    # requirements.txt 생성
    requirements = """
numpy>=1.20.0
pandas>=1.3.0
matplotlib>=3.4.0
requests>=2.26.0
pytest>=6.2.0
"""
    
    with open("requirements.txt", "w") as f:
        f.write(requirements.strip())
    
    print("requirements.txt 생성 완료")
    print("다음 명령어로 패키지를 설치하세요:")
    print("  source venv/bin/activate  # Linux/Mac")
    print("  venv\\Scripts\\activate  # Windows")
    print("  pip install -r requirements.txt")

if __name__ == "__main__":
    setup_project()

💡 실전 예제

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
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# project_structure/
# ├── main.py
# ├── config.py
# ├── core/
# │   ├── __init__.py
# │   ├── database.py
# │   └── validators.py
# ├── models/
# │   ├── __init__.py
# │   ├── base.py
# │   ├── user.py
# │   └── product.py
# └── utils/
#     ├── __init__.py
#     ├── decorators.py
#     └── helpers.py

# config.py
"""프로젝트 설정"""

import os
from pathlib import Path

# 프로젝트 루트 디렉토리
BASE_DIR = Path(__file__).parent

# 데이터베이스 설정
DATABASE = {
    'host': os.environ.get('DB_HOST', 'localhost'),
    'port': int(os.environ.get('DB_PORT', 5432)),
    'name': os.environ.get('DB_NAME', 'myapp'),
    'user': os.environ.get('DB_USER', 'user'),
    'password': os.environ.get('DB_PASSWORD', 'password')
}

# API 설정
API_KEY = os.environ.get('API_KEY', 'default-key')
API_BASE_URL = 'https://api.example.com/v1'

# 로깅 설정
LOGGING = {
    'level': 'INFO',
    'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    'file': BASE_DIR / 'logs' / 'app.log'
}

# core/__init__.py
"""핵심 기능 패키지"""

from .database import Database
from .validators import validate_email, validate_phone

__all__ = ['Database', 'validate_email', 'validate_phone']

# core/database.py
"""데이터베이스 연결 관리"""

import sqlite3
from contextlib import contextmanager
from typing import List, Dict, Any

class Database:
    """데이터베이스 관리 클래스"""
    
    def __init__(self, db_path: str):
        self.db_path = db_path
        self.connection = None
    
    def connect(self):
        """데이터베이스 연결"""
        self.connection = sqlite3.connect(self.db_path)
        self.connection.row_factory = sqlite3.Row
    
    def disconnect(self):
        """데이터베이스 연결 해제"""
        if self.connection:
            self.connection.close()
    
    @contextmanager
    def get_cursor(self):
        """커서 컨텍스트 매니저"""
        cursor = self.connection.cursor()
        try:
            yield cursor
            self.connection.commit()
        except Exception as e:
            self.connection.rollback()
            raise e
        finally:
            cursor.close()
    
    def execute(self, query: str, params: tuple = ()) -> List[Dict[str, Any]]:
        """쿼리 실행"""
        with self.get_cursor() as cursor:
            cursor.execute(query, params)
            return [dict(row) for row in cursor.fetchall()]
    
    def execute_many(self, query: str, params_list: List[tuple]):
        """여러 쿼리 실행"""
        with self.get_cursor() as cursor:
            cursor.executemany(query, params_list)

# core/validators.py
"""유효성 검사 함수들"""

import re
from typing import Optional

def validate_email(email: str) -> bool:
    """이메일 유효성 검사"""
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

def validate_phone(phone: str) -> bool:
    """전화번호 유효성 검사"""
    # 한국 전화번호 형식
    pattern = r'^0\d{1,2}-?\d{3,4}-?\d{4}$'
    return bool(re.match(pattern, phone.replace(' ', '')))

def validate_password(password: str) -> tuple[bool, Optional[str]]:
    """비밀번호 유효성 검사"""
    if len(password) < 8:
        return False, "비밀번호는 8자 이상이어야 합니다."
    
    if not re.search(r'[A-Z]', password):
        return False, "대문자를 포함해야 합니다."
    
    if not re.search(r'[a-z]', password):
        return False, "소문자를 포함해야 합니다."
    
    if not re.search(r'\d', password):
        return False, "숫자를 포함해야 합니다."
    
    if not re.search(r'[!@#$%^&*]', password):
        return False, "특수문자를 포함해야 합니다."
    
    return True, None

# models/__init__.py
"""모델 패키지"""

from .base import BaseModel
from .user import User
from .product import Product

__all__ = ['BaseModel', 'User', 'Product']

# models/base.py
"""기본 모델 클래스"""

from datetime import datetime
from typing import Dict, Any

class BaseModel:
    """모든 모델의 기본 클래스"""
    
    def __init__(self, **kwargs):
        self.id = kwargs.get('id')
        self.created_at = kwargs.get('created_at', datetime.now())
        self.updated_at = kwargs.get('updated_at', datetime.now())
    
    def to_dict(self) -> Dict[str, Any]:
        """딕셔너리로 변환"""
        return {
            'id': self.id,
            'created_at': self.created_at.isoformat() if self.created_at else None,
            'updated_at': self.updated_at.isoformat() if self.updated_at else None
        }
    
    def update(self, **kwargs):
        """속성 업데이트"""
        for key, value in kwargs.items():
            if hasattr(self, key):
                setattr(self, key, value)
        self.updated_at = datetime.now()

# models/user.py
"""사용자 모델"""

from typing import Dict, Any
from ..core.validators import validate_email
from .base import BaseModel

class User(BaseModel):
    """사용자 모델"""
    
    def __init__(self, username: str, email: str, **kwargs):
        super().__init__(**kwargs)
        self.username = username
        self.email = email
        self.is_active = kwargs.get('is_active', True)
        self.is_admin = kwargs.get('is_admin', False)
    
    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, value):
        if not validate_email(value):
            raise ValueError("유효하지 않은 이메일 주소입니다.")
        self._email = value
    
    def to_dict(self) -> Dict[str, Any]:
        """딕셔너리로 변환"""
        data = super().to_dict()
        data.update({
            'username': self.username,
            'email': self.email,
            'is_active': self.is_active,
            'is_admin': self.is_admin
        })
        return data
    
    def __repr__(self):
        return f"User(username='{self.username}', email='{self.email}')"

# utils/__init__.py
"""유틸리티 패키지"""

from .decorators import timer, cache, retry
from .helpers import generate_id, format_currency, parse_date

__all__ = [
    'timer', 'cache', 'retry',
    'generate_id', 'format_currency', 'parse_date'
]

# utils/decorators.py
"""유용한 데코레이터들"""

import time
import functools
from typing import Callable, Any

def timer(func: Callable) -> Callable:
    """함수 실행 시간 측정 데코레이터"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs) -> Any:
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 실행 시간: {end - start:.4f}")
        return result
    return wrapper

def cache(func: Callable) -> Callable:
    """간단한 캐싱 데코레이터"""
    cached = {}
    
    @functools.wraps(func)
    def wrapper(*args, **kwargs) -> Any:
        key = str(args) + str(kwargs)
        if key not in cached:
            cached[key] = func(*args, **kwargs)
        return cached[key]
    return wrapper

def retry(max_attempts: int = 3, delay: float = 1.0):
    """재시도 데코레이터"""
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs) -> Any:
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    print(f"시도 {attempt + 1} 실패: {e}")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

# utils/helpers.py
"""헬퍼 함수들"""

import uuid
from datetime import datetime
from typing import Union

def generate_id() -> str:
    """고유 ID 생성"""
    return str(uuid.uuid4())

def format_currency(amount: float, currency: str = "KRW") -> str:
    """통화 포맷팅"""
    if currency == "KRW":
        return f"{amount:,.0f}"
    elif currency == "USD":
        return f"${amount:,.2f}"
    else:
        return f"{amount:,.2f} {currency}"

def parse_date(date_string: str) -> datetime:
    """날짜 문자열 파싱"""
    formats = [
        "%Y-%m-%d",
        "%Y/%m/%d",
        "%d-%m-%Y",
        "%d/%m/%Y",
        "%Y-%m-%d %H:%M:%S",
        "%Y/%m/%d %H:%M:%S"
    ]
    
    for fmt in formats:
        try:
            return datetime.strptime(date_string, fmt)
        except ValueError:
            continue
    
    raise ValueError(f"날짜 형식을 인식할 수 없습니다: {date_string}")

# main.py
"""메인 애플리케이션"""

import sys
from pathlib import Path

# 프로젝트 루트를 Python 경로에 추가
sys.path.insert(0, str(Path(__file__).parent))

from config import DATABASE, LOGGING
from core import Database, validate_email
from models import User, Product
from utils import timer, generate_id

@timer
def main():
    """메인 함수"""
    # 데이터베이스 초기화
    db = Database(":memory:")  # 메모리 DB
    db.connect()
    
    # 테이블 생성
    db.execute("""
        CREATE TABLE users (
            id TEXT PRIMARY KEY,
            username TEXT NOT NULL,
            email TEXT NOT NULL,
            created_at TIMESTAMP
        )
    """)
    
    # 사용자 생성
    user = User(
        username="testuser",
        email="test@example.com",
        id=generate_id()
    )
    
    print(f"생성된 사용자: {user}")
    print(f"사용자 정보: {user.to_dict()}")
    
    # 데이터베이스에 저장
    db.execute(
        "INSERT INTO users (id, username, email, created_at) VALUES (?, ?, ?, ?)",
        (user.id, user.username, user.email, user.created_at)
    )
    
    # 조회
    users = db.execute("SELECT * FROM users")
    print(f"저장된 사용자: {users}")
    
    db.disconnect()

if __name__ == "__main__":
    main()

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
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# plugin_system.py
"""간단한 플러그인 시스템 구현"""

import os
import importlib
import inspect
from abc import ABC, abstractmethod
from typing import Dict, List, Type
from pathlib import Path

class PluginInterface(ABC):
    """플러그인 인터페이스"""
    
    @property
    @abstractmethod
    def name(self) -> str:
        """플러그인 이름"""
        pass
    
    @property
    @abstractmethod
    def version(self) -> str:
        """플러그인 버전"""
        pass
    
    @abstractmethod
    def execute(self, *args, **kwargs):
        """플러그인 실행"""
        pass

class PluginManager:
    """플러그인 관리자"""
    
    def __init__(self, plugin_dir: str = "plugins"):
        self.plugin_dir = Path(plugin_dir)
        self.plugins: Dict[str, PluginInterface] = {}
        self._ensure_plugin_dir()
    
    def _ensure_plugin_dir(self):
        """플러그인 디렉토리 생성"""
        self.plugin_dir.mkdir(exist_ok=True)
        init_file = self.plugin_dir / "__init__.py"
        if not init_file.exists():
            init_file.touch()
    
    def discover_plugins(self):
        """플러그인 자동 탐색"""
        # 플러그인 디렉토리를 sys.path에 추가
        import sys
        plugin_path = str(self.plugin_dir.parent)
        if plugin_path not in sys.path:
            sys.path.insert(0, plugin_path)
        
        # 플러그인 파일 찾기
        for file in self.plugin_dir.glob("*.py"):
            if file.name.startswith("_"):
                continue
            
            module_name = f"{self.plugin_dir.name}.{file.stem}"
            try:
                module = importlib.import_module(module_name)
                self._load_plugins_from_module(module)
            except Exception as e:
                print(f"플러그인 로드 실패 {module_name}: {e}")
    
    def _load_plugins_from_module(self, module):
        """모듈에서 플러그인 클래스 로드"""
        for name, obj in inspect.getmembers(module):
            if (inspect.isclass(obj) and 
                issubclass(obj, PluginInterface) and 
                obj is not PluginInterface):
                try:
                    plugin_instance = obj()
                    self.register_plugin(plugin_instance)
                except Exception as e:
                    print(f"플러그인 인스턴스 생성 실패 {name}: {e}")
    
    def register_plugin(self, plugin: PluginInterface):
        """플러그인 등록"""
        if plugin.name in self.plugins:
            print(f"경고: 플러그인 '{plugin.name}'이(가) 이미 등록되어 있습니다.")
        self.plugins[plugin.name] = plugin
        print(f"플러그인 등록: {plugin.name} v{plugin.version}")
    
    def get_plugin(self, name: str) -> PluginInterface:
        """플러그인 가져오기"""
        return self.plugins.get(name)
    
    def list_plugins(self) -> List[str]:
        """등록된 플러그인 목록"""
        return list(self.plugins.keys())
    
    def execute_plugin(self, name: str, *args, **kwargs):
        """플러그인 실행"""
        plugin = self.get_plugin(name)
        if plugin:
            return plugin.execute(*args, **kwargs)
        else:
            raise ValueError(f"플러그인을 찾을 수 없습니다: {name}")

# plugins/hello_plugin.py
"""Hello 플러그인"""

from plugin_system import PluginInterface

class HelloPlugin(PluginInterface):
    """인사 플러그인"""
    
    @property
    def name(self) -> str:
        return "hello"
    
    @property
    def version(self) -> str:
        return "1.0.0"
    
    def execute(self, name: str = "World"):
        """인사 메시지 출력"""
        return f"Hello, {name}!"

# plugins/math_plugin.py
"""수학 연산 플러그인"""

from plugin_system import PluginInterface

class MathPlugin(PluginInterface):
    """수학 연산 플러그인"""
    
    @property
    def name(self) -> str:
        return "math"
    
    @property
    def version(self) -> str:
        return "1.0.0"
    
    def execute(self, operation: str, a: float, b: float):
        """수학 연산 수행"""
        operations = {
            "add": lambda x, y: x + y,
            "subtract": lambda x, y: x - y,
            "multiply": lambda x, y: x * y,
            "divide": lambda x, y: x / y if y != 0 else "Division by zero"
        }
        
        if operation in operations:
            return operations[operation](a, b)
        else:
            return f"Unknown operation: {operation}"

# plugins/file_plugin.py
"""파일 처리 플러그인"""

import json
from pathlib import Path
from plugin_system import PluginInterface

class FilePlugin(PluginInterface):
    """파일 처리 플러그인"""
    
    @property
    def name(self) -> str:
        return "file"
    
    @property
    def version(self) -> str:
        return "1.0.0"
    
    def execute(self, action: str, filename: str, data=None):
        """파일 작업 수행"""
        path = Path(filename)
        
        if action == "read":
            if path.exists():
                if path.suffix == ".json":
                    with open(path, 'r') as f:
                        return json.load(f)
                else:
                    return path.read_text()
            return None
        
        elif action == "write":
            if data is not None:
                if path.suffix == ".json":
                    with open(path, 'w') as f:
                        json.dump(data, f, indent=2)
                else:
                    path.write_text(str(data))
                return True
            return False
        
        elif action == "exists":
            return path.exists()
        
        else:
            return f"Unknown action: {action}"

# main_plugin_demo.py
"""플러그인 시스템 데모"""

def main():
    # 플러그인 매니저 생성
    manager = PluginManager()
    
    # 플러그인 자동 탐색
    manager.discover_plugins()
    
    # 등록된 플러그인 확인
    print("등록된 플러그인:")
    for plugin_name in manager.list_plugins():
        plugin = manager.get_plugin(plugin_name)
        print(f"  - {plugin_name} v{plugin.version}")
    
    # 플러그인 실행
    print("\n플러그인 실행:")
    
    # Hello 플러그인
    result = manager.execute_plugin("hello", "Python")
    print(f"Hello Plugin: {result}")
    
    # Math 플러그인
    result = manager.execute_plugin("math", "add", 10, 20)
    print(f"Math Plugin (10 + 20): {result}")
    
    # File 플러그인
    data = {"message": "Hello from plugin!", "number": 42}
    manager.execute_plugin("file", "write", "test.json", data)
    loaded_data = manager.execute_plugin("file", "read", "test.json")
    print(f"File Plugin: {loaded_data}")

if __name__ == "__main__":
    main()

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
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
# dynamic_loader.py
"""동적 모듈 로딩 시스템"""

import importlib
import importlib.util
import sys
from pathlib import Path
from typing import Any, Dict, Optional, List

class DynamicModuleLoader:
    """동적 모듈 로더"""
    
    def __init__(self):
        self.loaded_modules: Dict[str, Any] = {}
    
    def load_module_from_file(self, file_path: str, module_name: Optional[str] = None) -> Any:
        """파일에서 모듈 로드"""
        path = Path(file_path)
        
        if not path.exists():
            raise FileNotFoundError(f"모듈 파일을 찾을 수 없습니다: {file_path}")
        
        if module_name is None:
            module_name = path.stem
        
        # 모듈 스펙 생성
        spec = importlib.util.spec_from_file_location(module_name, path)
        if spec is None:
            raise ImportError(f"모듈 스펙을 생성할 수 없습니다: {file_path}")
        
        # 모듈 로드
        module = importlib.util.module_from_spec(spec)
        sys.modules[module_name] = module
        spec.loader.exec_module(module)
        
        self.loaded_modules[module_name] = module
        return module
    
    def load_module_by_name(self, module_name: str) -> Any:
        """이름으로 모듈 로드"""
        try:
            module = importlib.import_module(module_name)
            self.loaded_modules[module_name] = module
            return module
        except ImportError as e:
            raise ImportError(f"모듈을 로드할 수 없습니다: {module_name}") from e
    
    def reload_module(self, module_name: str) -> Any:
        """모듈 다시 로드"""
        if module_name in self.loaded_modules:
            module = self.loaded_modules[module_name]
            return importlib.reload(module)
        else:
            raise ValueError(f"로드되지 않은 모듈입니다: {module_name}")
    
    def get_module_attribute(self, module_name: str, attribute_name: str) -> Any:
        """모듈의 특정 속성 가져오기"""
        if module_name not in self.loaded_modules:
            raise ValueError(f"로드되지 않은 모듈입니다: {module_name}")
        
        module = self.loaded_modules[module_name]
        if hasattr(module, attribute_name):
            return getattr(module, attribute_name)
        else:
            raise AttributeError(f"모듈 '{module_name}''{attribute_name}' 속성이 없습니다.")
    
    def list_module_contents(self, module_name: str) -> List[str]:
        """모듈의 내용 나열"""
        if module_name not in self.loaded_modules:
            raise ValueError(f"로드되지 않은 모듈입니다: {module_name}")
        
        module = self.loaded_modules[module_name]
        return [name for name in dir(module) if not name.startswith('_')]

class ModuleRegistry:
    """모듈 레지스트리"""
    
    def __init__(self):
        self.registry: Dict[str, Dict[str, Any]] = {}
        self.loader = DynamicModuleLoader()
    
    def register_module(self, 
                       name: str, 
                       path: Optional[str] = None,
                       module_name: Optional[str] = None,
                       metadata: Optional[Dict[str, Any]] = None):
        """모듈 등록"""
        entry = {
            'path': path,
            'module_name': module_name or name,
            'metadata': metadata or {},
            'loaded': False,
            'module': None
        }
        self.registry[name] = entry
    
    def load_module(self, name: str) -> Any:
        """등록된 모듈 로드"""
        if name not in self.registry:
            raise ValueError(f"등록되지 않은 모듈입니다: {name}")
        
        entry = self.registry[name]
        
        if not entry['loaded']:
            if entry['path']:
                module = self.loader.load_module_from_file(
                    entry['path'], 
                    entry['module_name']
                )
            else:
                module = self.loader.load_module_by_name(entry['module_name'])
            
            entry['module'] = module
            entry['loaded'] = True
        
        return entry['module']
    
    def get_module_info(self, name: str) -> Dict[str, Any]:
        """모듈 정보 가져오기"""
        if name not in self.registry:
            raise ValueError(f"등록되지 않은 모듈입니다: {name}")
        
        return self.registry[name].copy()
    
    def list_registered_modules(self) -> List[str]:
        """등록된 모듈 목록"""
        return list(self.registry.keys())

# 사용 예제
def demo_dynamic_loading():
    """동적 로딩 데모"""
    
    # 테스트용 모듈 파일 생성
    test_module_content = '''
def greet(name):
    """인사 함수"""
    return f"안녕하세요, {name}님!"

class Calculator:
    """계산기 클래스"""
    
    def add(self, a, b):
        return a + b
    
    def multiply(self, a, b):
        return a * b

PI = 3.14159
'''
    
    # 테스트 모듈 파일 생성
    with open("test_module.py", "w") as f:
        f.write(test_module_content)
    
    # 동적 로더 사용
    loader = DynamicModuleLoader()
    
    # 파일에서 모듈 로드
    test_module = loader.load_module_from_file("test_module.py")
    
    # 모듈 사용
    print(test_module.greet("Python"))
    
    calc = test_module.Calculator()
    print(f"10 + 20 = {calc.add(10, 20)}")
    print(f"5 * 6 = {calc.multiply(5, 6)}")
    print(f"PI = {test_module.PI}")
    
    # 모듈 내용 확인
    contents = loader.list_module_contents("test_module")
    print(f"\n모듈 내용: {contents}")
    
    # 레지스트리 사용
    registry = ModuleRegistry()
    
    # 모듈 등록
    registry.register_module("test", path="test_module.py")
    registry.register_module("json", module_name="json")
    registry.register_module("math", module_name="math")
    
    # 등록된 모듈 확인
    print(f"\n등록된 모듈: {registry.list_registered_modules()}")
    
    # 모듈 로드 및 사용
    json_module = registry.load_module("json")
    data = {"name": "Python", "version": 3.9}
    json_string = json_module.dumps(data)
    print(f"\nJSON: {json_string}")
    
    # 정리
    import os
    os.remove("test_module.py")

if __name__ == "__main__":
    demo_dynamic_loading()

⚠️ 초보자가 자주 하는 실수

1. 순환 임포트 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ❌ file1.py
from file2 import func2

def func1():
    return func2()

# ❌ file2.py
from file1 import func1  # ImportError!

def func2():
    return func1()

# ✅ 해결 방법 1: 함수 내부에서 임포트
def func2():
    from file1 import func1
    return func1()

# ✅ 해결 방법 2: 구조 재설계
# 공통 기능을 별도 모듈로 분리

2. init.py 누락

1
2
3
4
5
6
7
8
9
10
11
12
# ❌ 패키지 구조
mypackage/
    module1.py  # __init__.py 없음!
    module2.py

# Python 3.3+에서는 동작하지만 권장하지 않음

# ✅ 올바른 구조
mypackage/
    __init__.py  # 빈 파일이라도 생성
    module1.py
    module2.py

3. 상대 임포트 실수

1
2
3
4
5
6
7
8
# ❌ 패키지 내부에서 절대 경로 사용
# mypackage/module1.py
import mypackage.module2  # 패키지 이름 변경시 문제

# ✅ 상대 임포트 사용
# mypackage/module1.py
from . import module2
from ..other_package import module3

4. 모듈명과 변수명 충돌

1
2
3
4
5
6
7
# ❌ 내장 모듈과 같은 이름 사용
math = 10  # 내장 math 모듈과 충돌
import math  # 이제 math 변수가 모듈을 덮어씀

# ✅ 다른 이름 사용
math_score = 10
import math  # 정상 동작

5. if name == “main” 미사용

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ 모듈 임포트시에도 실행됨
# module.py
def my_function():
    return "Hello"

print(my_function())  # 임포트할 때마다 실행!

# ✅ 직접 실행시만 실행
def my_function():
    return "Hello"

if __name__ == "__main__":
    print(my_function())  # 직접 실행시만

🎯 핵심 정리

모듈/패키지 구조 Best Practices

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
# 1. 명확한 디렉토리 구조
project/
├── src/              # 소스 코드
   ├── __init__.py
   ├── core/        # 핵심 기능
   ├── utils/       # 유틸리티
   └── models/      # 데이터 모델
├── tests/           # 테스트 코드
├── docs/            # 문서
├── requirements.txt
└── setup.py

# 2. __init__.py 활용
# 패키지 레벨 임포트 제공
from .module1 import important_function
from .module2 import ImportantClass

__all__ = ['important_function', 'ImportantClass']

# 3. 순환 임포트 방지
# 나쁨
# module_a.py
from module_b import function_b

# module_b.py
from module_a import function_a  # 순환 임포트!

# 좋음 - 함수 내부에서 임포트
def function_that_needs_b():
    from module_b import function_b
    return function_b()

임포트 스타일 가이드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 임포트 순서 (PEP 8)
# 1. 표준 라이브러리
import os
import sys
from datetime import datetime

# 2. 서드파티 라이브러리
import numpy as np
import pandas as pd
from flask import Flask, request

# 3. 로컬 애플리케이션/라이브러리
from myapp.core import database
from myapp.utils import helpers

# 절대 임포트 사용 권장
from myapp.models.user import User  # 좋음
from .user import User  # 상대 임포트는 패키지 내부에서만

# 명시적 임포트
from module import function1, function2  # 좋음
from module import *  # 나쁨 (네임스페이스 오염)

패키지 배포 준비

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
# setup.py 예시
from setuptools import setup, find_packages

setup(
    name="mypackage",
    version="0.1.0",
    author="Your Name",
    author_email="your.email@example.com",
    description="A short description of your package",
    long_description=open("README.md").read(),
    long_description_content_type="text/markdown",
    url="https://github.com/yourusername/mypackage",
    packages=find_packages(where="src"),
    package_dir={"": "src"},
    classifiers=[
        "Development Status :: 3 - Alpha",
        "Intended Audience :: Developers",
        "License :: OSI Approved :: MIT License",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
    ],
    python_requires=">=3.8",
    install_requires=[
        "requests>=2.25.0",
        "numpy>=1.19.0",
    ],
    extras_require={
        "dev": ["pytest", "black", "flake8"],
        "docs": ["sphinx", "sphinx-rtd-theme"],
    },
)

🎓 파이썬 마스터하기 시리즈

📚 기초편 (1-7)

  1. Python 소개와 개발 환경 설정
  2. 변수, 자료형, 연산자 완벽 정리
  3. 조건문과 반복문 마스터하기
  4. 함수와 람다 완벽 가이드
  5. 리스트, 튜플, 딕셔너리 정복하기
  6. 문자열 처리와 정규표현식
  7. 파일 입출력과 예외 처리

🚀 중급편 (8-12)

  1. 클래스와 객체지향 프로그래밍
  2. 모듈과 패키지 관리 (현재 글)
  3. 데코레이터와 제너레이터
  4. 비동기 프로그래밍 (async/await)
  5. 데이터베이스 연동하기

💼 고급편 (13-16)

  1. 웹 스크래핑과 API 개발
  2. 테스트와 디버깅 전략
  3. 성능 최적화 기법
  4. 멀티프로세싱과 병렬 처리

이전글: 클래스와 객체지향 프로그래밍 ⬅️ 현재글: 모듈과 패키지 관리 다음글: 데코레이터와 제너레이터 ➡️


이번 포스트에서는 Python의 모듈과 패키지 시스템을 완벽히 마스터했습니다. 다음 포스트에서는 데코레이터와 제너레이터에 대해 자세히 알아보겠습니다. Happy Coding! 🐍✨

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.