💼 실무 예시: 토스뱅크 같은 핀테크 서비스 개발하기
웹 프레임워크를 배우기 전에, 실제 한국 기업들이 어떻게 Python 웹 프레임워크를 활용하고 있는지 살펴보겠습니다.
토스뱅크 스타일의 간단한 계좌 관리 API를 만들어보며 Flask와 Django의 차이점을 이해해보겠습니다:
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
| # 토스뱅크 스타일 계좌 관리 시스템 개념
"""
요구사항:
1. 사용자 등록/로그인
2. 계좌 잔액 조회
3. 송금 기능
4. 거래 내역 조회
5. 보안 (2단계 인증)
Flask vs Django 선택 기준:
- Flask: 빠른 프로토타입, 마이크로서비스 (토스의 개별 서비스들)
- Django: 전체 백오피스 시스템, 관리자 페이지 필요시
"""
# 실제 핀테크 회사들의 선택
fintech_frameworks = {
"토스": "Spring Boot (Java) + Python 마이크로서비스",
"카카오페이": "Django + Flask 하이브리드",
"네이버페이": "Spring + Python 데이터 분석",
"배달의민족": "Django 기반 주문 시스템",
"당근마켓": "Django + React 풀스택"
}
print("한국 핀테크/스타트업 프레임워크 사용 현황:")
for company, tech in fintech_frameworks.items():
print(f"{company}: {tech}")
|
이제 이런 실제 서비스를 만들 수 있는 웹 프레임워크에 대해 알아보겠습니다.
🌐 웹 프레임워크란?
웹 프레임워크는 웹 애플리케이션을 효율적으로 개발할 수 있도록 도와주는 도구 모음입니다. Python에서는 Flask와 Django가 가장 인기있는 웹 프레임워크입니다.
graph TD
A[웹 프레임워크] --> B[Flask]
A --> C[Django]
B --> B1[마이크로 프레임워크]
B --> B2[유연성]
B --> B3[최소한의 구성]
B --> B4[확장 가능]
C --> C1[풀스택 프레임워크]
C --> C2[배터리 포함]
C --> C3[ORM 내장]
C --> C4[관리자 페이지]
[!TIP] Flask vs Django: 뭘 먼저 배울까요?
- Flask: “내가 직접 하나씩 조립하고 싶다!” → 가볍고 자유도가 높아서 웹의 동작 원리를 배우기 좋습니다.
- Django: “빨리 완성품을 만들고 싶다!” → 로그인, 관리자 페이지, DB 연동 등이 다 들어있어서 생산성이 엄청납니다.
초보자라면 Flask로 가볍게 시작해서 웹의 흐름을 익히고, Django로 넘어가는 것을 추천합니다!
🚀 Flask 시작하기
Flask 설치와 기본 구조
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
| # 설치
# pip install flask
from flask import Flask, render_template, request, jsonify
# Flask 앱 생성
app = Flask(__name__)
# 라우트 정의
@app.route('/')
def home():
"""홈페이지"""
return '<h1>Welcome to Flask!</h1>'
@app.route('/hello/<name>')
def hello(name):
"""URL 매개변수 사용"""
return f'<h1>Hello, {name}!</h1>'
@app.route('/about')
def about():
"""템플릿 렌더링"""
return render_template('about.html', title='About Us')
# 앱 실행
if __name__ == '__main__':
app.run(debug=True, port=5000)
|
[!WARNING] debug=True는 개발할 때만!
app.run(debug=True)를 쓰면 코드를 고칠 때마다 서버가 자동으로 재시작되고, 에러가 나면 웹페이지에 상세한 에러 정보를 보여줍니다. 개발할 땐 정말 편하지만, 실제 서비스(배포)할 때 켜두면 해커에게 서버 내부 정보를 다 보여주는 꼴이 됩니다. 배포 시에는 반드시 꺼야 합니다!
Flask REST 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
| from flask import Flask, jsonify, request
from flask_cors import CORS
import sqlite3
app = Flask(__name__)
CORS(app) # Cross-Origin 허용
# 데이터베이스 초기화
def init_db():
conn = sqlite3.connect('users.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS users
(id INTEGER PRIMARY KEY, name TEXT, email TEXT)''')
conn.commit()
conn.close()
# GET - 모든 사용자 조회
@app.route('/api/users', methods=['GET'])
def get_users():
conn = sqlite3.connect('users.db')
c = conn.cursor()
c.execute("SELECT * FROM users")
users = []
for row in c.fetchall():
users.append({
'id': row[0],
'name': row[1],
'email': row[2]
})
conn.close()
return jsonify(users)
# POST - 새 사용자 생성
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.json
conn = sqlite3.connect('users.db')
c = conn.cursor()
c.execute("INSERT INTO users (name, email) VALUES (?, ?)",
(data['name'], data['email']))
conn.commit()
user_id = c.lastrowid
conn.close()
return jsonify({
'id': user_id,
'name': data['name'],
'email': data['email']
}), 201
# PUT - 사용자 정보 수정
@app.route('/api/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
data = request.json
conn = sqlite3.connect('users.db')
c = conn.cursor()
c.execute("UPDATE users SET name=?, email=? WHERE id=?",
(data['name'], data['email'], user_id))
conn.commit()
conn.close()
return jsonify({'message': 'User updated successfully'})
# DELETE - 사용자 삭제
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
conn = sqlite3.connect('users.db')
c = conn.cursor()
c.execute("DELETE FROM users WHERE id=?", (user_id,))
conn.commit()
conn.close()
return jsonify({'message': 'User deleted successfully'})
# 에러 핸들링
@app.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Not found'}), 404
@app.errorhandler(500)
def internal_error(error):
return jsonify({'error': 'Internal server error'}), 500
if __name__ == '__main__':
init_db()
app.run(debug=True)
|
Flask 블루프린트와 프로젝트 구조
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
| # project_structure/
# ├── app/
# │ ├── __init__.py
# │ ├── models.py
# │ ├── views/
# │ │ ├── __init__.py
# │ │ ├── auth.py
# │ │ └── blog.py
# │ └── templates/
# ├── config.py
# └── run.py
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config.from_object('config.Config')
db.init_app(app)
# 블루프린트 등록
from app.views.auth import auth_bp
from app.views.blog import blog_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(blog_bp, url_prefix='/blog')
return app
# app/views/auth.py
from flask import Blueprint, render_template, request, redirect, url_for
from werkzeug.security import generate_password_hash, check_password_hash
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
# 로그인 로직
return redirect(url_for('blog.index'))
return render_template('login.html')
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
hashed_password = generate_password_hash(password)
# 사용자 등록 로직
return redirect(url_for('auth.login'))
return render_template('register.html')
|
🎯 Django 시작하기
Django 프로젝트 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Django 설치
pip install django
# 프로젝트 생성
django-admin startproject myproject
# 앱 생성
python manage.py startapp myapp
# 마이그레이션
python manage.py makemigrations
python manage.py migrate
# 서버 실행
python manage.py runserver
|
Django 모델과 ORM
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
| # myapp/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class Post(models.Model):
"""블로그 포스트 모델"""
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)
published = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
verbose_name = '포스트'
verbose_name_plural = '포스트들'
def __str__(self):
return self.title
def get_absolute_url(self):
from django.urls import reverse
return reverse('post_detail', kwargs={'slug': self.slug})
class Comment(models.Model):
"""댓글 모델"""
post = models.ForeignKey(Post, on_delete=models.CASCADE,
related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f'Comment by {self.author.username} on {self.post}'
# ORM 사용 예제
# 생성
post = Post.objects.create(
title="Django Tutorial",
slug="django-tutorial",
author=user,
content="Django is awesome!"
)
# 조회
all_posts = Post.objects.all()
published_posts = Post.objects.filter(published=True)
recent_posts = Post.objects.filter(
created_at__gte=timezone.now() - timezone.timedelta(days=7)
)
# 수정
post = Post.objects.get(id=1)
post.title = "Updated Title"
post.save()
# 삭제
Post.objects.filter(id=1).delete()
# 집계
from django.db.models import Count, Avg
post_count = Post.objects.count()
posts_with_comments = Post.objects.annotate(
comment_count=Count('comments')
)
|
Django Views와 Templates
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
| # myapp/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, DetailView, CreateView
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Post, Comment
from .forms import PostForm, CommentForm
# 함수 기반 뷰
def post_list(request):
"""포스트 목록"""
posts = Post.objects.filter(published=True)
return render(request, 'blog/post_list.html', {'posts': posts})
def post_detail(request, slug):
"""포스트 상세"""
post = get_object_or_404(Post, slug=slug, published=True)
comments = post.comments.all()
if request.method == 'POST':
comment_form = CommentForm(request.POST)
if comment_form.is_valid():
comment = comment_form.save(commit=False)
comment.post = post
comment.author = request.user
comment.save()
messages.success(request, '댓글이 작성되었습니다.')
return redirect('post_detail', slug=post.slug)
else:
comment_form = CommentForm()
return render(request, 'blog/post_detail.html', {
'post': post,
'comments': comments,
'comment_form': comment_form
})
@login_required
def post_create(request):
"""포스트 작성"""
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
messages.success(request, '포스트가 작성되었습니다.')
return redirect('post_detail', slug=post.slug)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
# 클래스 기반 뷰
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(published=True)
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['comments'] = self.object.comments.all()
context['comment_form'] = CommentForm()
return context
class PostCreateView(CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
|
Django REST Framework
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
| # 설치
# pip install djangorestframework
# settings.py
INSTALLED_APPS = [
# ...
'rest_framework',
'myapp',
]
# myapp/serializers.py
from rest_framework import serializers
from .models import Post, Comment
class CommentSerializer(serializers.ModelSerializer):
author = serializers.ReadOnlyField(source='author.username')
class Meta:
model = Comment
fields = ['id', 'author', 'content', 'created_at']
class PostSerializer(serializers.ModelSerializer):
author = serializers.ReadOnlyField(source='author.username')
comments = CommentSerializer(many=True, read_only=True)
comment_count = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'author', 'content',
'created_at', 'updated_at', 'published',
'comments', 'comment_count']
def get_comment_count(self, obj):
return obj.comments.count()
# myapp/api_views.py
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Post, Comment
from .serializers import PostSerializer, CommentSerializer
class PostViewSet(viewsets.ModelViewSet):
"""
포스트 API ViewSet
"""
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
lookup_field = 'slug'
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def add_comment(self, request, slug=None):
"""포스트에 댓글 추가"""
post = self.get_object()
serializer = CommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(author=request.user, post=post)
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
@action(detail=False)
def published(self, request):
"""게시된 포스트만 조회"""
published_posts = Post.objects.filter(published=True)
serializer = self.get_serializer(published_posts, many=True)
return Response(serializer.data)
# myapp/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .api_views import PostViewSet
router = DefaultRouter()
router.register('posts', PostViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]
|
🔍 Flask vs Django: 언제 무엇을 사용할까?
Flask를 선택해야 할 때
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
| """
Flask 장점:
1. 가벼움과 유연성
2. 마이크로서비스에 적합
3. 학습 곡선이 낮음
4. 최소한의 구성으로 시작 가능
"""
# Flask 프로젝트 예: 마이크로서비스
from flask import Flask, jsonify
import redis
app = Flask(__name__)
cache = redis.Redis(host='localhost', port=6379)
@app.route('/api/cache/<key>')
def get_cache(key):
value = cache.get(key)
if value:
return jsonify({'key': key, 'value': value.decode()})
return jsonify({'error': 'Key not found'}), 404
@app.route('/api/cache/<key>/<value>', methods=['POST'])
def set_cache(key, value):
cache.set(key, value)
return jsonify({'message': 'Cache set successfully'})
if __name__ == '__main__':
app.run(port=5001)
|
Django를 선택해야 할 때
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
| """
Django 장점:
1. 풀스택 기능 내장
2. 강력한 ORM
3. 관리자 페이지 자동 생성
4. 대규모 프로젝트에 적합
5. 보안 기능 내장
"""
# Django 프로젝트 예: 전자상거래 사이트
# models.py
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.IntegerField()
category = models.ForeignKey(Category, on_delete=models.CASCADE)
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
products = models.ManyToManyField(Product, through='OrderItem')
total = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
# admin.py
from django.contrib import admin
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ['name', 'price', 'stock', 'category']
list_filter = ['category', 'price']
search_fields = ['name', 'description']
|
🚀 실전 프로젝트: Todo 애플리케이션
Flask 버전
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
| # flask_todo.py
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
db = SQLAlchemy(app)
class Todo(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
completed = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f'<Todo {self.title}>'
@app.route('/')
def index():
todos = Todo.query.order_by(Todo.created_at.desc()).all()
return render_template('index.html', todos=todos)
@app.route('/add', methods=['POST'])
def add_todo():
title = request.form.get('title')
description = request.form.get('description')
new_todo = Todo(title=title, description=description)
db.session.add(new_todo)
db.session.commit()
return redirect(url_for('index'))
@app.route('/complete/<int:id>')
def complete_todo(id):
todo = Todo.query.get_or_404(id)
todo.completed = not todo.completed
db.session.commit()
return redirect(url_for('index'))
@app.route('/delete/<int:id>')
def delete_todo(id):
todo = Todo.query.get_or_404(id)
db.session.delete(todo)
db.session.commit()
return redirect(url_for('index'))
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
|
Django 버전
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
| # Django Todo 앱
# models.py
from django.db import models
from django.contrib.auth.models import User
class Todo(models.Model):
PRIORITY_CHOICES = [
('L', 'Low'),
('M', 'Medium'),
('H', 'High'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
priority = models.CharField(max_length=1, choices=PRIORITY_CHOICES,
default='M')
completed = models.BooleanField(default=False)
due_date = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-priority', '-created_at']
def __str__(self):
return self.title
# views.py
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.views.generic import ListView
from .models import Todo
from .forms import TodoForm
@login_required
def todo_list(request):
todos = Todo.objects.filter(user=request.user)
return render(request, 'todo/list.html', {'todos': todos})
@login_required
def todo_create(request):
if request.method == 'POST':
form = TodoForm(request.POST)
if form.is_valid():
todo = form.save(commit=False)
todo.user = request.user
todo.save()
return redirect('todo_list')
else:
form = TodoForm()
return render(request, 'todo/form.html', {'form': form})
class TodoListView(ListView):
model = Todo
template_name = 'todo/list.html'
context_object_name = 'todos'
def get_queryset(self):
return Todo.objects.filter(user=self.request.user)
|
💡 웹 프레임워크 Best Practices
보안
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # Flask 보안
from flask import Flask
from flask_cors import CORS
from werkzeug.security import generate_password_hash
import secrets
app = Flask(__name__)
app.config['SECRET_KEY'] = secrets.token_hex(16)
CORS(app, origins=['http://localhost:3000'])
# Django 보안 설정
# settings.py
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com']
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
|
[!IMPORTANT] SECRET_KEY는 절대 노출 금지!
Flask나 Django 설정에 있는 SECRET_KEY는 암호화 서명에 쓰이는 매우 중요한 열쇠입니다. 이 키가 유출되면 해커가 사용자 세션을 위조할 수 있습니다. 깃허브(GitHub)에 코드를 올릴 때 절대 이 키를 코드에 직접 적어서 올리지 마세요! 환경 변수(os.environ)나 별도의 설정 파일로 관리해야 합니다.
성능 최적화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # 캐싱
from flask_caching import Cache
cache = Cache(app, config={'CACHE_TYPE': 'simple'})
@app.route('/expensive')
@cache.cached(timeout=300)
def expensive_operation():
# 비용이 많이 드는 작업
return result
# Django 캐싱
from django.core.cache import cache
def get_popular_posts():
posts = cache.get('popular_posts')
if posts is None:
posts = Post.objects.filter(
published=True
).order_by('-views')[:10]
cache.set('popular_posts', posts, 3600)
return posts
|
테스트
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
| # Flask 테스트
import unittest
from app import app
class FlaskTestCase(unittest.TestCase):
def setUp(self):
self.app = app.test_client()
def test_home_page(self):
response = self.app.get('/')
self.assertEqual(response.status_code, 200)
def test_api_endpoint(self):
response = self.app.get('/api/users')
self.assertEqual(response.status_code, 200)
data = response.get_json()
self.assertIsInstance(data, list)
# Django 테스트
from django.test import TestCase, Client
from django.contrib.auth.models import User
from .models import Post
class PostTestCase(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass'
)
def test_post_creation(self):
post = Post.objects.create(
title='Test Post',
author=self.user,
content='Test content'
)
self.assertEqual(post.title, 'Test Post')
def test_post_list_view(self):
response = self.client.get('/posts/')
self.assertEqual(response.status_code, 200)
|
🎯 배포하기
Flask 배포 (Gunicorn + Nginx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # requirements.txt
Flask==2.3.0
gunicorn==21.2.0
# Gunicorn 실행
gunicorn -w 4 -b 0.0.0.0:8000 app:app
# Nginx 설정
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
|
Django 배포 (uWSGI + Nginx)
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 정적 파일 수집
python manage.py collectstatic
# uWSGI 실행
uwsgi --http :8000 --module myproject.wsgi
# Docker 배포
FROM python:3.11
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
|
⚠️ 초보자들이 자주 하는 실수
웹 프레임워크를 처음 배울 때 자주 발생하는 실수들을 정리했습니다. 이런 실수들을 미리 알고 피해가세요!
1. Flask에서 app.run(debug=True)를 프로덕션에서 사용
1
2
3
4
5
6
7
8
9
| # ❌ 잘못된 예: 프로덕션에서 디버그 모드
if __name__ == '__main__':
app.run(debug=True) # 보안 취약점!
# ✅ 올바른 예: 환경별 설정 분리
import os
if __name__ == '__main__':
debug_mode = os.environ.get('FLASK_ENV') == 'development'
app.run(debug=debug_mode, host='0.0.0.0', port=5000)
|
2. Django에서 settings.py에 SECRET_KEY 하드코딩
1
2
3
4
5
6
| # ❌ 잘못된 예: 민감한 정보 노출
SECRET_KEY = 'django-insecure-actual-secret-key-here'
# ✅ 올바른 예: 환경 변수 사용
import os
SECRET_KEY = os.environ.get('SECRET_KEY', 'default-for-dev-only')
|
3. SQL 쿼리에서 사용자 입력 직접 삽입
1
2
3
4
5
6
7
8
| # ❌ 잘못된 예: SQL Injection 취약점
query = f"SELECT * FROM users WHERE username = '{username}'"
# ✅ 올바른 예: 파라미터화된 쿼리
# Flask-SQLAlchemy
users = User.query.filter_by(username=username).all()
# Django ORM
users = User.objects.filter(username=username)
|
4. CORS 설정을 ‘*‘로 열어두기
1
2
3
4
5
6
| # ❌ 잘못된 예: 모든 도메인 허용
from flask_cors import CORS
CORS(app, origins='*') # 보안 위험!
# ✅ 올바른 예: 특정 도메인만 허용
CORS(app, origins=['https://yourdomain.com', 'https://app.yourdomain.com'])
|
5. 에러 처리 없이 API 응답 반환
1
2
3
4
5
6
7
8
9
10
11
12
13
| # ❌ 잘못된 예: 에러 처리 누락
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
user = User.query.get(user_id) # None일 수 있음
return jsonify(user.to_dict()) # AttributeError 발생 가능
# ✅ 올바른 예: 적절한 에러 처리
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
user = User.query.get(user_id)
if not user:
return jsonify({'error': 'User not found'}), 404
return jsonify(user.to_dict())
|
6. 데이터베이스 연결을 매번 새로 생성
1
2
3
4
5
6
7
8
9
| # ❌ 잘못된 예: 매번 새 연결
def get_users():
conn = sqlite3.connect('database.db') # 비효율적
# ... 쿼리 실행
conn.close()
# ✅ 올바른 예: 연결 풀 사용
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app) # 연결 풀 자동 관리
|
7. 환경별 설정 파일 분리하지 않기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # ❌ 잘못된 예: 하나의 설정 파일
DEBUG = True
DATABASE_URL = 'postgresql://localhost/myapp'
# ✅ 올바른 예: 환경별 설정
# config.py
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY')
class DevelopmentConfig(Config):
DEBUG = True
DATABASE_URL = 'sqlite:///dev.db'
class ProductionConfig(Config):
DEBUG = False
DATABASE_URL = os.environ.get('DATABASE_URL')
|
8. 프론트엔드와 백엔드 세션/인증 동기화 실패
1
2
3
4
5
6
7
8
9
10
11
12
| # ❌ 잘못된 예: 세션 불일치
# 프론트엔드는 JWT를 사용하는데 백엔드는 세션 쿠키 사용
# ✅ 올바른 예: 일관된 인증 방식
from flask_jwt_extended import JWTManager, create_access_token
jwt = JWTManager(app)
@app.route('/api/login', methods=['POST'])
def login():
# 로그인 검증 후
access_token = create_access_token(identity=user.id)
return jsonify({'access_token': access_token})
|
이런 실수들을 피하면 더 안전하고 효율적인 웹 애플리케이션을 만들 수 있습니다!
🎓 파이썬 마스터하기 시리즈
📚 기초편 (1-7)
- Python 소개와 개발 환경 설정 완벽 가이드
- 변수, 자료형, 연산자 완벽 정리
- 조건문과 반복문 마스터하기
- 함수와 람다 완벽 가이드
- 리스트, 튜플, 딕셔너리 정복하기
- 문자열 처리와 정규표현식
- 파일 입출력과 예외 처리
🚀 중급편 (8-12)
- 클래스와 객체지향 프로그래밍
- 모듈과 패키지 관리
- 데코레이터와 제너레이터
- 비동기 프로그래밍 (async/await)
- 데이터베이스 연동하기
💼 고급편 (13-16)
- 웹 스크래핑과 API 활용
- 테스트와 디버깅 전략
- 성능 최적화 기법
- 멀티프로세싱과 병렬 처리
🏆 실전편 (17-20)
- 웹 프레임워크 입문 (Flask/Django) ← 현재 글
- 데이터 분석 도구 활용 (Pandas/NumPy)
- 머신러닝 기초 (scikit-learn)
- 실전 프로젝트와 베스트 프랙티스
이전글: 멀티프로세싱과 병렬 처리 ⬅️ 현재글: 웹 프레임워크 입문 (Flask/Django) 다음글: 데이터 분석 도구 활용 (Pandas/NumPy) ➡️
이번 포스트에서는 Python의 대표적인 웹 프레임워크인 Flask와 Django를 완벽히 마스터했습니다. 다음 포스트에서는 데이터 분석 도구인 Pandas와 NumPy에 대해 자세히 알아보겠습니다. Happy Coding! 🐍✨