[이제와서 시작하는 Python 마스터하기 #20] 실전 프로젝트와 베스트 프랙티스: 완성도 높은 Python 개발
마지막 글에서는 새 문법을 늘리기보다 요청을 받는 코드, 업무 규칙, 저장을 맡는 코드를 나누는 기준을 정리합니다. 작은 사용자 생성 기능 하나를 끝까지 추적하면 프로젝트 구조와 테스트 전략을 함께 이해할 수 있습니다.
읽기 전 준비: 모듈·클래스·예외 처리와 기본 테스트를 복습해두세요. 먼저 프로젝트 구조 → SOLID의 책임 분리 → 사용자 서비스 테스트 순서로 읽으세요. 초기화 자동화, 여러 디자인 패턴, FastAPI 통합 코드는 필요한 부분을 찾아보는 확장 자료입니다. 파일과 DB·인증 설정을 함께 준비하는 예제가 포함되어 있으므로 한 코드 블록을 하나의 실행 파일로 복사하기보다 파일 경계와 의존성을 먼저 확인하세요.
💼 실무 예시: 네이버 같은 대기업 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[성능 최적화]
📁 완성형 프로젝트 구조 가이드
표준 프로젝트 구조
폴더 이름을 그대로 따라 만드는 것보다 변경 이유가 다른 코드를 분리하는 것이 목적입니다. 아래에서 src는 실행 코드, tests는 동작을 확인하는 코드, 설정 파일은 도구가 프로젝트를 다루는 기준으로 읽으세요. 작은 프로젝트라면 실제로 필요한 모듈부터 시작하고 역할이 늘어날 때 나누어도 됩니다.
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
my_project/
├── README.md # 프로젝트 설명
├── pyproject.toml # 프로젝트 메타데이터와 의존성
├── uv.lock # uv 사용 시 재현 가능한 lockfile
├── requirements.txt # 레거시/배포용 의존성 목록
├── .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
프로젝트 구조는 일관성이 중요해요!
어떤 프로젝트를 보더라도 비슷한 구조로 되어 있으면 이해하기 쉽습니다.
src/: 실제 코드는 여기에tests/: 테스트 코드는 별도로docs/: 문서는 따로 분리pyproject.toml: 프로젝트 메타데이터와 의존성의 기준uv.lock: 팀 전체가 같은 의존성 해석을 쓰도록 고정requirements.txt: 레거시 배포 환경이나 단순 스크립트에서 사용이 구조를 지키면 나중에 협업할 때도 훨씬 수월합니다!
프로젝트 초기화 자동화
이 생성기는 디렉터리뿐 아니라 패키지 설정, 테스트 설정, CI와 README까지 만듭니다. 먼저 initialize()가 호출하는 메서드를 보고 어떤 파일을 생성하는지 목록과 대응시켜보세요. 생성되는 템플릿의 도구 설정은 앞에서 본 프로젝트 구조를 자동으로 만드는 한 가지 선택입니다.
기본 구조를 이해하는 데 생성기를 실행할 필요는 없습니다. 반복해서 비슷한 프로젝트를 시작할 때 펼쳐보고, 프로젝트 이름과 출력 위치를 확인한 뒤 필요한 템플릿만 선택하세요.
프로젝트 파일·CI·README를 만드는 초기화 도구 펼치기
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
# 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.115.0",
"uvicorn[standard]>=0.30.0",
"pydantic>=2.8.0",
"sqlalchemy>=2.0.0",
"alembic>=1.13.0",
"",
"# 개발 의존성",
"pytest>=8.0.0",
"pytest-cov>=5.0.0",
"ruff>=0.5.0",
"mypy>=1.10.0",
"pre-commit>=3.7.0"
]
content = "\n".join(requirements)
(self.root / "requirements.txt").write_text(content)
def create_pyproject_toml(self):
"""pyproject.toml 생성"""
pyproject = f'''[project]
name = "{self.project_name}"
version = "0.1.0"
description = "A modern Python project"
authors = [{{ name = "{self.author}" }}]
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.30.0",
"pydantic>=2.8.0",
"sqlalchemy>=2.0.0",
"alembic>=1.13.0",
]
[dependency-groups]
dev = [
"pytest>=8.0.0",
"pytest-cov>=5.0.0",
"ruff>=0.5.0",
"mypy>=1.10.0",
"pre-commit>=3.7.0",
]
'''
(self.root / "pyproject.toml").write_text(pyproject)
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]
```
## 사용 방법
```bash
# 애플리케이션 실행
python -m {self.project_name}
# 테스트 실행
pytest
# 코드 포맷팅
black src/ tests/
# 타입 체크
mypy src/
```
## 개발 가이드
### 코드 스타일
- Black을 사용한 자동 포맷팅
- Flake8을 사용한 린팅
- MyPy를 사용한 타입 체크
### 테스트
- pytest를 사용한 단위 테스트
- 최소 80% 코드 커버리지 유지
### 커밋 메시지
```
feat: 새로운 기능 추가
fix: 버그 수정
docs: 문서 수정
style: 코드 포맷팅
refactor: 코드 리팩토링
test: 테스트 코드 추가
chore: 빌드 스크립트 수정
```
## 라이선스
MIT License
"""
(self.root / "README.md").write_text(readme_content)
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()
🏗️ 클린 코드 아키텍처
SOLID 원칙 적용
첫 예제에서는 사용자 검증과 저장을 서로 다른 클래스로 나누고 UserService가 두 역할을 연결합니다. 저장 방식이 바뀌어도 사용자 검증 규칙까지 수정할 필요가 줄어드는 이유를 확인해보세요. 원칙 이름을 외우기보다 한 요구사항이 바뀔 때 몇 군데를 고쳐야 하는가로 읽으면 구체적입니다.
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
# 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}
코드를 작성하기 전에 설계부터!
코드를 바로 작성하기 전에 먼저 어떻게 만들지 설계하세요.
- 단일 책임 원칙: 하나의 클래스는 하나의 일만 하기
- 의존성 역전: 구체적인 것이 아닌 추상화에 의존하기
- 열림/닫힘 원칙: 확장에는 열려 있고, 수정에는 닫혀 있기
이런 원칙들이 처음에는 어려워 보이지만, 지키면 나중에 유지보수가 훨씬 쉽습니다!
디자인 패턴 활용
패턴은 프로젝트마다 모두 넣는 체크리스트가 아닙니다. 아래 예제에서는 설정 인스턴스 공유, DB 연결 객체 생성, 이벤트 알림, 정렬 방식 교체가 각각 어떤 반복 문제를 줄이는지 비교해보세요. 특히 Strategy는 DataSorter를 고치지 않고 정렬 구현을 교체하는 흐름에 집중하면 앞의 의존성 분리와 연결됩니다.
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)
🧪 포괄적인 테스트 전략
테스트 피라미드 구현
동일한 사용자 생성 기능을 세 범위에서 확인합니다. 단위 테스트는 UserService의 규칙, 통합 테스트는 API와 DB 연결, 브라우저 테스트는 사용자의 입력 흐름을 확인합니다. 먼저 성공 케이스에서 검증하는 값이 각 계층마다 어떻게 달라지는지 비교하세요.
외부 계층이 많을수록 준비와 실패 원인도 늘어납니다. 기본 학습에서는 서비스의 성공·잘못된 이메일 테스트를 먼저 읽고, DB fixture와 브라우저 설정은 해당 실습을 준비할 때 이어서 확인하면 됩니다.
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
아래 코드는 main.py, 모델, 스키마, 서비스, 라우터의 파일별 발췌를 한 블록에 모은 참고 코드입니다. DB 연결·설정·인증·미들웨어처럼 다른 모듈에서 가져오는 구성도 필요합니다. 파일 주석을 경계로 읽고, 블록 전체를 단일 파일에 붙여 실행하는 예제로 받아들이지 마세요.
사용자 생성 요청 하나를 골라 라우터 → 입력 스키마 → UserService.create_user() → DB 모델 순서로 추적해보세요. 중복 이메일은 서비스가 판단하고, HTTP 상태 코드로 바꾸는 일은 라우터가 맡는다는 차이를 확인하면 구조를 나눈 이유가 보입니다. 제품·주문 라우터와 운영용 미들웨어는 이 흐름을 확장하는 연결 지점입니다.
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
# 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
class UserBase(BaseModel):
"""사용자 기본 스키마"""
email: EmailStr
full_name: str | None = 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 ..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 프로젝트를 만들 수 있습니다!
스스로 확인하기: 사용자 저장소를 다른 DB로 바꾼다면 입력 스키마와 이메일 검증까지 함께 바꿔야 할까요? 또 중복 이메일을 금지하는 규칙은 단위 테스트에서, API가 올바른 오류 응답을 주는지는 어느 테스트에서 확인할지 나누어보세요. 변경할 파일과 확인할 동작을 짝지을 수 있으면 이 글의 프로젝트 구조를 자신의 코드에도 적용할 준비가 된 것입니다.
🎉 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! 🐍✨
