포스트

[이제와서 시작하는 Python 마스터하기 #20] 실전 프로젝트와 베스트 프랙티스: 완성도 높은 Python 개발

[이제와서 시작하는 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)

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

중급편 (8-12)

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

고급편 (13-16)

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

실전편 (17-20)

  1. 웹 프레임워크 입문 (Flask/Django)
  2. 데이터 분석 도구 활용 (Pandas/NumPy)
  3. 머신러닝 기초 (scikit-learn)
  4. 실전 프로젝트와 베스트 프랙티스 ← 현재 글

🚀 다음 단계 추천

  1. 실제 프로젝트 구축: 배운 내용을 활용해 포트폴리오 프로젝트 만들기
  2. 오픈소스 기여: GitHub에서 Python 프로젝트에 기여하기
  3. 전문 분야 심화: 웹 개발, 데이터 분석, AI/ML 중 관심 분야 전문화
  4. 커뮤니티 참여: 파이썬 사용자 모임, 컨퍼런스 참석하기

이전글: 머신러닝 기초 (scikit-learn) ⬅️ 현재글: 실전 프로젝트와 베스트 프랙티스 (시리즈 완결!)


20개의 포스트로 이루어진 Python 마스터하기 시리즈가 모두 완료되었습니다! 여러분의 Python 여정이 계속해서 발전하고 성장하기를 응원합니다. Happy Coding! 🐍✨

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