[이제와서 시작하는 Python 마스터하기 #20] 실전 프로젝트와 베스트 프랙티스: 완성도 높은 Python 개발
💼 실무 예시: 네이버 같은 대기업 Python 개발 환경 구축하기
베스트 프랙티스를 배우기 전에, 실제 한국 대기업들이 어떻게 Python 프로젝트를 관리하고 있는지 살펴보겠습니다.
네이버/카카오 스타일의 엔터프라이즈 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
# 대기업 Python 프로젝트 표준
"""
네이버/카카오/삼성전자 Python 개발팀이 하는 일:
1. 프로젝트 구조 표준화
- 마이크로서비스 아키텍처 (MSA)
- API 게이트웨이 패턴
- 도메인 주도 설계 (DDD)
2. 코드 품질 관리
- 코드 리뷰 시스템 (Gerrit, GitHub Enterprise)
- 자동화된 테스트 (단위/통합/E2E)
- 정적 분석 도구 (SonarQube, CodeClimate)
3. CI/CD 파이프라인
- Jenkins/GitHub Actions 자동화
- 컨테이너 기반 배포 (Docker, Kubernetes)
- 무중단 배포 (Blue-Green, Canary)
4. 모니터링 및 로깅
- 실시간 모니터링 (Prometheus, Grafana)
- 중앙 집중식 로깅 (ELK Stack)
- 분산 추적 (Jaeger, Zipkin)
5. 보안 및 컴플라이언스
- 개인정보보호법 준수
- 보안 스캔 자동화
- 접근 권한 관리 (IAM)
실제 사용 기업:
- 네이버: LINE, 웹툰, 클라우드 서비스
- 카카오: 카카오톡, 카카오페이, 카카오뱅크
- 삼성전자: SmartThings, Bixby 백엔드
- LG전자: webOS, ThinQ 플랫폼
- SK텔레콤: T맵, NUGU 서비스
"""
# 네이버 스타일 프로젝트 구조
enterprise_project_structure = {
"src/": {
"domain/": "비즈니스 로직 (도메인 모델)",
"application/": "애플리케이션 서비스",
"infrastructure/": "외부 시스템 연동",
"interfaces/": "API 컨트롤러"
},
"tests/": {
"unit/": "단위 테스트 (80% 커버리지)",
"integration/": "통합 테스트",
"e2e/": "종단간 테스트"
},
"deployment/": {
"docker/": "컨테이너 설정",
"k8s/": "Kubernetes 매니페스트",
"terraform/": "인프라 코드"
},
"monitoring/": {
"grafana/": "대시보드 설정",
"prometheus/": "메트릭 수집 규칙"
}
}
print("대기업 Python 개발 표준:")
print("- 엔터프라이즈급 아키텍처 설계")
print("- 자동화된 품질 관리 시스템")
print("- DevOps 기반 운영 자동화")
print("- 글로벌 서비스 수준의 가용성")
이제 이런 실무 수준의 Python 프로젝트를 구축할 수 있는 베스트 프랙티스를 배워보겠습니다.
🎯 Python 마스터하기: 실전의 완성
20번째 포스트에 도달한 여러분을 진심으로 축하합니다! 이제 Python의 기초부터 고급 기술까지 모든 것을 익혔습니다. 이번 마지막 포스트에서는 지금까지 배운 모든 지식을 실전 프로젝트에 적용하는 방법과 베스트 프랙티스를 배워보겠습니다.
graph TD
A[Python 마스터하기] --> B[실전 프로젝트]
A --> C[코딩 표준]
A --> D[프로젝트 관리]
A --> E[배포 전략]
B --> B1[웹 애플리케이션]
B --> B2[데이터 파이프라인]
C --> C1[클린 코드 원칙]
C --> C2[디자인 패턴 적용]
D --> D1[버전 관리]
D --> D2[패키지 관리]
D --> D3[문서화]
E --> E1[배포 자동화]
E --> E2[모니터링 설정]
E --> E3[CI/CD 파이프라인]
E --> E4[성능 최적화]
📁 완성형 프로젝트 구조 가이드
표준 프로젝트 구조
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
my_project/
├── README.md # 프로젝트 설명
├── requirements.txt # 의존성 목록
├── setup.py # 패키지 설정
├── .env.example # 환경 변수 템플릿
├── .gitignore # Git 제외 파일
├── .github/ # GitHub 설정
│ └── workflows/
│ └── ci.yml # CI/CD 파이프라인
├── docs/ # 문서
│ ├── api.md
│ └── user_guide.md
├── tests/ # 테스트 코드
│ ├── __init__.py
│ ├── unit/
│ ├── integration/
│ └── fixtures/
├── src/ # 소스 코드
│ └── my_project/
│ ├── __init__.py
│ ├── __main__.py # 진입점
│ ├── config.py # 설정
│ ├── models/ # 데이터 모델
│ ├── services/ # 비즈니스 로직
│ ├── api/ # API 엔드포인트
│ ├── utils/ # 유틸리티 함수
│ └── exceptions.py # 예외 정의
└── scripts/ # 배포 스크립트
├── setup.sh
└── deploy.sh
[!TIP] 프로젝트 구조는 일관성이 중요해요!
어떤 프로젝트를 보더라도 비슷한 구조로 되어 있으면 이해하기 쉽습니다.
src/: 실제 코드는 여기에tests/: 테스트 코드는 별도로docs/: 문서는 따로 분리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
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
# project_initializer.py
import os
import json
from pathlib import Path
class ProjectInitializer:
"""Python 프로젝트 초기화 도구"""
def __init__(self, project_name, author="Your Name"):
self.project_name = project_name
self.author = author
self.root = Path(project_name)
def create_directory_structure(self):
"""디렉토리 구조 생성"""
directories = [
"src/" + self.project_name,
"tests/unit",
"tests/integration",
"tests/fixtures",
"docs",
"scripts",
".github/workflows"
]
for directory in directories:
(self.root / directory).mkdir(parents=True, exist_ok=True)
# __init__.py 파일 생성
if "src/" in directory or "tests/" in directory:
(self.root / directory / "__init__.py").touch()
def create_requirements_txt(self):
"""requirements.txt 생성"""
requirements = [
"# 프로덕션 의존성",
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"pydantic>=2.4.0",
"sqlalchemy>=2.0.0",
"alembic>=1.12.0",
"",
"# 개발 의존성",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"black>=23.9.0",
"flake8>=6.1.0",
"mypy>=1.6.0",
"pre-commit>=3.5.0"
]
content = "\n".join(requirements)
(self.root / "requirements.txt").write_text(content)
def create_setup_py(self):
"""setup.py 생성"""
setup_content = f'''from setuptools import setup, find_packages
setup(
name="{self.project_name}",
version="0.1.0",
author="{self.author}",
description="A modern Python project",
packages=find_packages(where="src"),
package_dir={"": "src"},
python_requires=">=3.8",
install_requires=[
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
],
extras_require={
"dev": [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"black>=23.9.0",
"flake8>=6.1.0",
"mypy>=1.6.0",
]
},
entry_points={
"console_scripts": [
f"{self.project_name}={self.project_name}.__main__:main",
],
},
)
'''
(self.root / "setup.py").write_text(setup_content)
def create_gitignore(self):
""".gitignore 생성"""
gitignore_content = """# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
# Distribution
dist/
build/
*.egg-info/
# Environment
.env
*.log
# OS
.DS_Store
Thumbs.db
"""
(self.root / ".gitignore").write_text(gitignore_content)
def create_ci_pipeline(self):
"""GitHub Actions CI 파이프라인 생성"""
ci_content = """name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8
run: |
flake8 src/ tests/ --count --max-line-length=88
- name: Type check with mypy
run: |
mypy src/
- name: Test with pytest
run: |
pytest tests/ --cov=src/ --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
"""
(self.root / ".github/workflows/ci.yml").write_text(ci_content)
def create_readme(self):
"""README.md 생성"""
readme_content = f"""# {self.project_name}
## 프로젝트 설명
{self.project_name}는 모던 Python 개발 방법론을 적용한 프로젝트입니다.
## 설치 방법
```bash
# 가상환경 생성
python -m venv venv
source venv/bin/activate # Windows: venv\\Scripts\\activate
# 의존성 설치
pip install -r requirements.txt
# 개발 모드 설치
pip install -e .[dev]
사용 방법
1
2
3
4
5
6
7
8
9
10
11
# 애플리케이션 실행
python -m {self.project_name}
# 테스트 실행
pytest
# 코드 포맷팅
black src/ tests/
# 타입 체크
mypy src/
개발 가이드
코드 스타일
- Black을 사용한 자동 포맷팅
- Flake8을 사용한 린팅
- MyPy를 사용한 타입 체크
테스트
- pytest를 사용한 단위 테스트
- 최소 80% 코드 커버리지 유지
커밋 메시지
1
2
3
4
5
6
7
feat: 새로운 기능 추가
fix: 버그 수정
docs: 문서 수정
style: 코드 포맷팅
refactor: 코드 리팩토링
test: 테스트 코드 추가
chore: 빌드 스크립트 수정
라이선스
MIT License “”” (self.root / “README.md”).write_text(readme_content)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def initialize(self):
"""전체 프로젝트 초기화"""
print(f"프로젝트 '{self.project_name}' 초기화 중...")
self.create_directory_structure()
self.create_requirements_txt()
self.create_setup_py()
self.create_gitignore()
self.create_ci_pipeline()
self.create_readme()
print("✅ 프로젝트 초기화 완료!")
print(f"📁 프로젝트 경로: {self.root.absolute()}")
print("\n다음 단계:")
print("1. cd " + self.project_name)
print("2. python -m venv venv")
print("3. source venv/bin/activate")
print("4. pip install -r requirements.txt")
사용 예시
if name == “main”: initializer = ProjectInitializer(“awesome_project”, “개발자”) initializer.initialize()
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
## 🏗️ 클린 코드 아키텍처
### SOLID 원칙 적용
```python
# 1. Single Responsibility Principle (단일 책임 원칙)
class UserRepository:
"""사용자 데이터 접근만 담당"""
def save(self, user):
# 데이터베이스에 사용자 저장
pass
def find_by_id(self, user_id):
# ID로 사용자 찾기
pass
class UserValidator:
"""사용자 검증만 담당"""
def validate_email(self, email):
# 이메일 유효성 검증
pass
def validate_password(self, password):
# 패스워드 강도 검증
pass
class UserService:
"""사용자 비즈니스 로직만 담당"""
def __init__(self, repository: UserRepository, validator: UserValidator):
self.repository = repository
self.validator = validator
def create_user(self, user_data):
# 검증 후 사용자 생성
self.validator.validate_email(user_data['email'])
self.validator.validate_password(user_data['password'])
return self.repository.save(user_data)
# 2. Open/Closed Principle (개방/폐쇄 원칙)
from abc import ABC, abstractmethod
class NotificationSender(ABC):
"""알림 전송 인터페이스"""
@abstractmethod
def send(self, message: str, recipient: str):
pass
class EmailSender(NotificationSender):
"""이메일 전송 구현"""
def send(self, message: str, recipient: str):
print(f"이메일 전송: {recipient}에게 {message}")
class SMSSender(NotificationSender):
"""SMS 전송 구현"""
def send(self, message: str, recipient: str):
print(f"SMS 전송: {recipient}에게 {message}")
class NotificationService:
"""새로운 전송 방식 추가 시 코드 수정 없이 확장 가능"""
def __init__(self, sender: NotificationSender):
self.sender = sender
def notify(self, message: str, recipient: str):
self.sender.send(message, recipient)
# 3. Dependency Inversion Principle (의존성 역전 원칙)
class DatabaseInterface(ABC):
@abstractmethod
def save(self, data): pass
class PostgreSQLDatabase(DatabaseInterface):
def save(self, data):
print("PostgreSQL에 저장")
class MongoDatabase(DatabaseInterface):
def save(self, data):
print("MongoDB에 저장")
class DataService:
"""구체적인 데이터베이스가 아닌 인터페이스에 의존"""
def __init__(self, database: DatabaseInterface):
self.database = database # 추상화에 의존
def process_data(self, data):
# 비즈니스 로직
processed_data = self.transform(data)
self.database.save(processed_data)
def transform(self, data):
return {"processed": data}
[!IMPORTANT] 코드를 작성하기 전에 설계부터!
코드를 바로 작성하기 전에 먼저 어떻게 만들지 설계하세요.
- 단일 책임 원칙: 하나의 클래스는 하나의 일만 하기
- 의존성 역전: 구체적인 것이 아닌 추상화에 의존하기
- 열림/닫힘 원칙: 확장에는 열려 있고, 수정에는 닫혀 있기
이런 원칙들이 처음에는 어려워 보이지만, 지키면 나중에 유지보수가 훨씬 쉽습니다!
디자인 패턴 활용
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
# Singleton 패턴 - 설정 관리
class Config:
"""애플리케이션 설정 관리"""
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not self._initialized:
self.database_url = "postgresql://localhost/myapp"
self.api_key = "secret-key"
self.debug = True
self._initialized = True
# Factory 패턴 - 객체 생성 추상화
class DatabaseConnectionFactory:
"""데이터베이스 연결 팩토리"""
@staticmethod
def create_connection(db_type: str):
if db_type == "postgresql":
return PostgreSQLConnection()
elif db_type == "mysql":
return MySQLConnection()
elif db_type == "mongodb":
return MongoDBConnection()
else:
raise ValueError(f"지원하지 않는 데이터베이스: {db_type}")
# Observer 패턴 - 이벤트 시스템
class EventPublisher:
"""이벤트 발행자"""
def __init__(self):
self._subscribers = []
def subscribe(self, subscriber):
self._subscribers.append(subscriber)
def unsubscribe(self, subscriber):
self._subscribers.remove(subscriber)
def notify(self, event):
for subscriber in self._subscribers:
subscriber.handle(event)
class EmailSubscriber:
"""이메일 알림 구독자"""
def handle(self, event):
print(f"이메일 발송: {event}")
class LogSubscriber:
"""로그 기록 구독자"""
def handle(self, event):
print(f"로그 기록: {event}")
# Strategy 패턴 - 알고리즘 교체
class SortStrategy(ABC):
@abstractmethod
def sort(self, data): pass
class QuickSortStrategy(SortStrategy):
def sort(self, data):
# 퀵소트 구현
return sorted(data)
class MergeSortStrategy(SortStrategy):
def sort(self, data):
# 머지소트 구현
return sorted(data)
class DataSorter:
def __init__(self, strategy: SortStrategy):
self.strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self.strategy = strategy
def sort_data(self, data):
return self.strategy.sort(data)
🧪 포괄적인 테스트 전략
테스트 피라미드 구현
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
# tests/unit/test_user_service.py
import pytest
from unittest.mock import Mock, MagicMock
from src.myproject.services.user_service import UserService
from src.myproject.models.user import User
class TestUserService:
"""단위 테스트 - 빠르고 독립적"""
def setup_method(self):
"""각 테스트 메서드 실행 전 설정"""
self.mock_repository = Mock()
self.mock_validator = Mock()
self.user_service = UserService(
self.mock_repository,
self.mock_validator
)
def test_create_user_success(self):
"""사용자 생성 성공 테스트"""
# Given
user_data = {
'email': 'test@example.com',
'password': 'StrongPass123!'
}
expected_user = User(id=1, email=user_data['email'])
# Mock 설정
self.mock_validator.validate_email.return_value = True
self.mock_validator.validate_password.return_value = True
self.mock_repository.save.return_value = expected_user
# When
result = self.user_service.create_user(user_data)
# Then
assert result == expected_user
self.mock_validator.validate_email.assert_called_once_with(user_data['email'])
self.mock_validator.validate_password.assert_called_once_with(user_data['password'])
self.mock_repository.save.assert_called_once_with(user_data)
def test_create_user_invalid_email(self):
"""잘못된 이메일로 사용자 생성 실패 테스트"""
# Given
user_data = {'email': 'invalid-email', 'password': 'StrongPass123!'}
self.mock_validator.validate_email.side_effect = ValueError("잘못된 이메일")
# When & Then
with pytest.raises(ValueError, match="잘못된 이메일"):
self.user_service.create_user(user_data)
# 검증: save 메서드가 호출되지 않았는지 확인
self.mock_repository.save.assert_not_called()
# tests/integration/test_user_api.py
import pytest
from fastapi.testclient import TestClient
from src.myproject.main import app
class TestUserAPI:
"""통합 테스트 - 실제 데이터베이스 사용"""
def setup_method(self):
"""각 테스트 전 데이터베이스 초기화"""
self.client = TestClient(app)
# 테스트 데이터베이스 설정
self.setup_test_database()
def setup_test_database(self):
"""테스트용 데이터베이스 설정"""
# 실제 구현에서는 테스트 DB 설정
pass
def test_create_user_api(self):
"""사용자 생성 API 통합 테스트"""
# Given
user_data = {
"email": "test@example.com",
"password": "StrongPass123!",
"name": "테스트 사용자"
}
# When
response = self.client.post("/api/users", json=user_data)
# Then
assert response.status_code == 201
response_data = response.json()
assert response_data["email"] == user_data["email"]
assert "id" in response_data
assert "password" not in response_data # 패스워드는 응답에 포함되지 않음
# tests/e2e/test_user_workflow.py
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
class TestUserWorkflow:
"""E2E 테스트 - 전체 사용자 시나리오"""
def setup_method(self):
"""브라우저 설정"""
self.driver = webdriver.Chrome()
self.driver.implicitly_wait(10)
def teardown_method(self):
"""브라우저 종료"""
self.driver.quit()
def test_user_registration_workflow(self):
"""사용자 등록 전체 워크플로우 테스트"""
# Given: 홈페이지 접속
self.driver.get("http://localhost:8000")
# When: 회원가입 페이지로 이동
signup_link = self.driver.find_element(By.LINK_TEXT, "회원가입")
signup_link.click()
# 회원가입 폼 작성
email_input = self.driver.find_element(By.NAME, "email")
password_input = self.driver.find_element(By.NAME, "password")
submit_button = self.driver.find_element(By.TYPE, "submit")
email_input.send_keys("test@example.com")
password_input.send_keys("StrongPass123!")
submit_button.click()
# Then: 성공 메시지 확인
success_message = self.driver.find_element(By.CLASS_NAME, "success-message")
assert "회원가입이 완료되었습니다" in success_message.text
# pytest 설정 파일
# conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.myproject.database import Base, get_db
from src.myproject.main import app
@pytest.fixture(scope="session")
def test_db():
"""테스트용 데이터베이스 설정"""
engine = create_engine("sqlite:///./test.db")
Base.metadata.create_all(bind=engine)
yield engine
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def db_session(test_db):
"""테스트용 DB 세션"""
Session = sessionmaker(bind=test_db)
session = Session()
yield session
session.close()
@pytest.fixture
def override_get_db(db_session):
"""의존성 주입 오버라이드"""
def _override_get_db():
yield db_session
app.dependency_overrides[get_db] = _override_get_db
yield
app.dependency_overrides.clear()
🚀 실전 프로젝트: 종합 웹 애플리케이션
FastAPI 기반 RESTful API
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
# src/myproject/main.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer
from sqlalchemy.orm import Session
import uvicorn
from .database import get_db
from .routers import users, products, orders
from .middleware import LoggingMiddleware, RateLimitMiddleware
from .config import settings
# FastAPI 앱 설정
app = FastAPI(
title="E-Commerce API",
description="완성도 높은 이커머스 API",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# 미들웨어 설정
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(LoggingMiddleware)
app.add_middleware(RateLimitMiddleware, calls=100, period=60)
# 라우터 등록
app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
app.include_router(products.router, prefix="/api/v1/products", tags=["products"])
app.include_router(orders.router, prefix="/api/v1/orders", tags=["orders"])
@app.get("/")
async def root():
"""헬스 체크 엔드포인트"""
return {"message": "E-Commerce API is running", "version": "1.0.0"}
@app.get("/health")
async def health_check(db: Session = Depends(get_db)):
"""상세 헬스 체크"""
try:
# 데이터베이스 연결 확인
db.execute("SELECT 1")
db_status = "healthy"
except Exception:
db_status = "unhealthy"
return {
"status": "healthy" if db_status == "healthy" else "unhealthy",
"database": db_status,
"version": "1.0.0"
}
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=settings.DEBUG,
log_level="info"
)
# src/myproject/models/user.py
from sqlalchemy import Column, Integer, String, DateTime, Boolean
from sqlalchemy.orm import relationship
from datetime import datetime
from ..database import Base
class User(Base):
"""사용자 모델"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
full_name = Column(String)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 관계 설정
orders = relationship("Order", back_populates="customer")
# src/myproject/schemas/user.py
from pydantic import BaseModel, EmailStr, validator
from datetime import datetime
from typing import Optional
class UserBase(BaseModel):
"""사용자 기본 스키마"""
email: EmailStr
full_name: Optional[str] = None
is_active: bool = True
class UserCreate(UserBase):
"""사용자 생성 스키마"""
password: str
@validator('password')
def validate_password(cls, v):
if len(v) < 8:
raise ValueError('패스워드는 최소 8자 이상이어야 합니다')
if not any(c.isupper() for c in v):
raise ValueError('패스워드에 대문자가 포함되어야 합니다')
if not any(c.islower() for c in v):
raise ValueError('패스워드에 소문자가 포함되어야 합니다')
if not any(c.isdigit() for c in v):
raise ValueError('패스워드에 숫자가 포함되어야 합니다')
return v
class UserUpdate(UserBase):
"""사용자 수정 스키마"""
email: Optional[EmailStr] = None
is_active: Optional[bool] = None
class UserResponse(UserBase):
"""사용자 응답 스키마"""
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# src/myproject/services/user_service.py
from sqlalchemy.orm import Session
from passlib.context import CryptContext
from ..models.user import User
from ..schemas.user import UserCreate, UserUpdate
from ..exceptions import UserNotFoundError, DuplicateEmailError
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class UserService:
"""사용자 비즈니스 로직"""
def __init__(self, db: Session):
self.db = db
def create_user(self, user_data: UserCreate) -> User:
"""사용자 생성"""
# 이메일 중복 체크
if self.get_user_by_email(user_data.email):
raise DuplicateEmailError("이미 존재하는 이메일입니다")
# 패스워드 해싱
hashed_password = pwd_context.hash(user_data.password)
# 사용자 생성
db_user = User(
email=user_data.email,
hashed_password=hashed_password,
full_name=user_data.full_name,
is_active=user_data.is_active
)
self.db.add(db_user)
self.db.commit()
self.db.refresh(db_user)
return db_user
def get_user_by_id(self, user_id: int) -> User:
"""ID로 사용자 조회"""
user = self.db.query(User).filter(User.id == user_id).first()
if not user:
raise UserNotFoundError(f"사용자를 찾을 수 없습니다: {user_id}")
return user
def get_user_by_email(self, email: str) -> User:
"""이메일로 사용자 조회"""
return self.db.query(User).filter(User.email == email).first()
def update_user(self, user_id: int, user_data: UserUpdate) -> User:
"""사용자 정보 수정"""
user = self.get_user_by_id(user_id)
update_data = user_data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(user, field, value)
self.db.commit()
self.db.refresh(user)
return user
def delete_user(self, user_id: int) -> bool:
"""사용자 삭제 (소프트 삭제)"""
user = self.get_user_by_id(user_id)
user.is_active = False
self.db.commit()
return True
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
"""패스워드 검증"""
return pwd_context.verify(plain_password, hashed_password)
# src/myproject/routers/users.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from ..database import get_db
from ..schemas.user import UserCreate, UserUpdate, UserResponse
from ..services.user_service import UserService
from ..exceptions import UserNotFoundError, DuplicateEmailError
from ..dependencies import get_current_user
router = APIRouter()
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
user_data: UserCreate,
db: Session = Depends(get_db)
):
"""사용자 생성"""
try:
user_service = UserService(db)
user = user_service.create_user(user_data)
return user
except DuplicateEmailError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""사용자 조회"""
try:
user_service = UserService(db)
user = user_service.get_user_by_id(user_id)
return user
except UserNotFoundError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="사용자를 찾을 수 없습니다"
)
@router.put("/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
user_data: UserUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""사용자 정보 수정"""
try:
user_service = UserService(db)
user = user_service.update_user(user_id, user_data)
return user
except UserNotFoundError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="사용자를 찾을 수 없습니다"
)
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""사용자 삭제"""
try:
user_service = UserService(db)
user_service.delete_user(user_id)
except UserNotFoundError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="사용자를 찾을 수 없습니다"
)
⚠️ 초보자들이 자주 하는 실수
Python 프로젝트를 처음 구축할 때 자주 발생하는 실수들을 정리했습니다. 이런 실수들을 미리 알고 피해가세요!
1. 프로젝트 구조를 체계적으로 설계하지 않기
1
2
3
4
5
6
7
8
9
10
11
12
# ❌ 잘못된 예: 모든 코드를 한 파일에
# main.py (500+ 줄)
from fastapi import FastAPI
# 모든 모델, 라우터, 서비스가 하나의 파일에...
# ✅ 올바른 예: 기능별로 모듈 분리
src/
├── models/ # 데이터 모델
├── schemas/ # API 스키마
├── services/ # 비즈니스 로직
├── routers/ # API 라우터
└── utils/ # 공통 유틸리티
2. 환경 변수 관리 소홀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ❌ 잘못된 예: 하드코딩된 설정값
DATABASE_URL = "postgresql://user:password@localhost/db"
SECRET_KEY = "my-secret-key-123"
DEBUG = True
# ✅ 올바른 예: 환경 변수와 설정 관리
import os
from pydantic import BaseSettings
class Settings(BaseSettings):
database_url: str
secret_key: str
debug: bool = False
class Config:
env_file = ".env"
settings = Settings()
3. 예외 처리를 제대로 하지 않기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ❌ 잘못된 예: 일반적인 Exception만 캐치
try:
user = get_user_by_id(user_id)
return user
except Exception: # 너무 광범위
return None
# ✅ 올바른 예: 구체적인 예외 처리
try:
user = get_user_by_id(user_id)
return user
except UserNotFoundError:
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다")
except DatabaseConnectionError:
raise HTTPException(status_code=500, detail="데이터베이스 연결 오류")
4. 로깅을 적절히 설정하지 않기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ❌ 잘못된 예: print문으로 디버깅
def create_user(user_data):
print(f"Creating user: {user_data}") # 프로덕션에서 문제
# ...
print("User created successfully")
# ✅ 올바른 예: 구조화된 로깅
import logging
logger = logging.getLogger(__name__)
def create_user(user_data):
logger.info("사용자 생성 시작", extra={"user_email": user_data.email})
try:
# 사용자 생성 로직
logger.info("사용자 생성 완료", extra={"user_id": user.id})
return user
except Exception as e:
logger.error("사용자 생성 실패", extra={"error": str(e)})
raise
5. 데이터베이스 연결을 제대로 관리하지 않기
1
2
3
4
5
6
7
8
9
10
11
# ❌ 잘못된 예: 연결 누수
def get_users():
conn = create_connection()
users = conn.execute("SELECT * FROM users")
return users # 연결이 닫히지 않음
# ✅ 올바른 예: 컨텍스트 매니저 사용
def get_users():
with get_db_session() as session:
users = session.query(User).all()
return users # 자동으로 연결 해제
6. 보안을 고려하지 않은 API 설계
1
2
3
4
5
6
7
8
9
# ❌ 잘못된 예: 인증/권한 없는 API
@app.get("/admin/users")
def get_all_users(): # 누구나 접근 가능
return get_all_users_from_db()
# ✅ 올바른 예: 인증/권한 확인
@app.get("/admin/users")
def get_all_users(current_user: User = Depends(get_current_admin_user)):
return get_all_users_from_db()
7. 테스트 코드를 작성하지 않기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ❌ 잘못된 예: 테스트 없이 개발
def calculate_total_price(items):
total = 0
for item in items:
total += item.price * item.quantity
return total
# 수동으로 매번 테스트
# ✅ 올바른 예: 자동화된 테스트
def test_calculate_total_price():
items = [
Item(price=1000, quantity=2),
Item(price=2000, quantity=1)
]
assert calculate_total_price(items) == 4000
def test_calculate_total_price_empty_list():
assert calculate_total_price([]) == 0
8. 성능을 고려하지 않은 데이터베이스 쿼리
1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ 잘못된 예: N+1 쿼리 문제
def get_users_with_orders():
users = session.query(User).all()
result = []
for user in users: # N번의 추가 쿼리 발생
orders = session.query(Order).filter(Order.user_id == user.id).all()
result.append({"user": user, "orders": orders})
return result
# ✅ 올바른 예: JOIN 또는 eager loading 사용
def get_users_with_orders():
users = session.query(User).options(joinedload(User.orders)).all()
return [{"user": user, "orders": user.orders} for user in users]
이런 실수들을 피하면 더 안정적이고 확장 가능한 Python 프로젝트를 만들 수 있습니다!
🎉 Python 마스터하기 시리즈 완주를 축하합니다!
20개의 포스트를 통해 Python의 기초부터 실전 프로젝트까지 모든 것을 배웠습니다. 이제 여러분은:
✅ 기초 문법을 완벽히 이해하고 ✅ 객체지향 프로그래밍을 활용할 수 있으며 ✅ 웹 프레임워크로 API를 구축할 수 있고 ✅ 데이터 분석과 머신러닝을 적용할 수 있으며 ✅ 실무 수준의 프로젝트를 설계할 수 있습니다!
📚 전체 시리즈 복습
기초편 (1-7)
- Python 소개와 개발 환경 설정
- 변수, 자료형, 연산자 완벽 정리
- 조건문과 반복문 마스터하기
- 함수와 람다 완벽 가이드
- 리스트, 튜플, 딕셔너리 정복하기
- 문자열 처리와 정규표현식
- 파일 입출력과 예외 처리
중급편 (8-12)
고급편 (13-16)
실전편 (17-20)
- 웹 프레임워크 입문 (Flask/Django)
- 데이터 분석 도구 활용 (Pandas/NumPy)
- 머신러닝 기초 (scikit-learn)
- 실전 프로젝트와 베스트 프랙티스 ← 현재 글
🚀 다음 단계 추천
- 실제 프로젝트 구축: 배운 내용을 활용해 포트폴리오 프로젝트 만들기
- 오픈소스 기여: GitHub에서 Python 프로젝트에 기여하기
- 전문 분야 심화: 웹 개발, 데이터 분석, AI/ML 중 관심 분야 전문화
- 커뮤니티 참여: 파이썬 사용자 모임, 컨퍼런스 참석하기
이전글: 머신러닝 기초 (scikit-learn) ⬅️ 현재글: 실전 프로젝트와 베스트 프랙티스 (시리즈 완결!)
20개의 포스트로 이루어진 Python 마스터하기 시리즈가 모두 완료되었습니다! 여러분의 Python 여정이 계속해서 발전하고 성장하기를 응원합니다. Happy Coding! 🐍✨