728x90
2.3 고객 주문 등록하기
- Terminal
python manage.py startapp orders
|
- myshop/settings.py
INSTALLED_APPS = [
...
'shop.apps.ShopConfig',
'cart.apps.CartConfig',
'orders.apps.OrdersConfig',
]
|
2.3.1 주문 모델 생성하기
- orders/models.py
from django.db import models
from shop.models import Product
class Order(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField()
address = models.CharField(max_length=250)
postal_code = models.CharField(max_length=20)
city = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
paid = models.BooleanField(default=False)
class Meta:
ordering = ['-created']
indexes = [
models.Index(fields=['-created']),
]
def __str__(self):
return f'Order {self.id}'
def get_total_cost(self):
return sum(item.get_cost() for item in self.items.all())
class OrderItem(models.Model):
order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
product = models.ForeignKey(Product, related_name='order_items', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField(default=1)
def __str__(self):
return str(self.id)
def get_cost(self):
return self.price * self.quantity
|
- Terminal
python manage.py makemigrations
python manage.py migrate
|
2.3.2 관리 사이트에 주문 모델 추가하기
- orders/admin.py
from django.contrib import admin
from .models import Order, OrderItem
class OrderItemInline(admin.TabularInline):
model = OrderItem
raw_id_fields = ['product']
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'first_name', 'last_name', 'email', 'address',
'postal_code', 'city', 'paid', 'created', 'updated']
list_filter = ['paid', 'created', 'updated']
inlines = [OrderItemInline]
|
- Terminal
python manage.py runserver
|
2.3.3 고객 주문 생성하기
- 새로운 주문의 생성 단계
- 사용자에게 데이터를 입력할 주문 폼 제시
- 입력한 데이터로 새로운 Order 인스턴스 생성
- 각 아이템에 대한 해당 OrderItem 인스턴스 생성
- 카트의 모든 내용을 지우고 사용자를 성공 페이지로 Redirection
- orders/forms.py
from django import forms
from .models import Order
class OrderCreateForm(forms.ModelForm):
class Meta:
model = Order
fields = ['first_name', 'last_name', 'email', 'address',
'postal_code', 'city']
|
- orders/views.py
from django.shortcuts import render
from .models import OrderItem
from .forms import OrderCreateForm
from cart.cart import Cart
def order_create(request):
cart = Cart(request)
if request.method == 'POST':
form = OrderCreateForm(request.POST)
if form.is_valid():
order = form.save()
for item in cart:
OrderItem.objects.create(order=order,
product=item['product'],
price=item['price'],
quantity=item['quantity'])
# 카트 비우기
cart.clear()
return render(request, 'orders/order/created.html', {'order': order})
else:
form = OrderCreateForm()
return render(request, 'orders/order/create.html', {'cart': cart, 'form': form})
|
- request.method에 따른 작업 내용
- GET 요청
- OrderCreateForm 폼을 인스턴스화
- orders/order/create.html 템플릿을 렌더링
- POST 요청
- 전송된 데이터의 유효성 검사
- 데이터가 유효하면 order = form.save()를 사용하여 데이터베이스에 새로운 주문을 생성
- 카트의 아이템들을 반복해서 각 아이템에 대한 OrderItem을 생성
- 카트의 콘텐츠 삭제
- orders/order/created.html 템플릿을 렌더링
- GET 요청
- orders/urls.py
from django.urls import path
from . import views
app_name = 'orders'
urlpatterns = [
path('create/', views.order_create, name='order_create'),
]
|
- myshop/urls.py
...
urlpatterns = [
path('admin/', admin.site.urls),
path('cart/', include('cart.urls', namespace='cart')),
path('orders/', include('orders.urls', namespace='orders')),
path('', include('shop.urls', namespace='shop')),
]
...
|
- cart/templates/cart/detail.html
- 다음의 코드를 찾아서 수정
<a href="#" class="button">Checkout</a>
|
<a href="{% url 'orders:order_create' %}" class="button">Checkout</a>
|
- orders/templages/orders/order/create.html
{% extends "shop/base.html" %}
{% block title %}
Checkout
{% endblock %}
{% block content %}
<h1>Checkout</h1>
<div class="order-info">
<h3>Your order</h3>
<ul>
{% for item in cart %}
<li>
{{ item.quantity }}x {{ item.product.name }}
<span>${{ item.total_price }}</span>
</li>
{% endfor %}
</ul>
<p>Total: ${{ cart.get_total_price }}</p>
</div>
<form method="post" class="order-form">
{{ form.as_p }}
<p><input type="submit" value="Place order"></p>
{% csrf_token %}
</form>
{% endblock %}
|
- orders/templages/orders/order/created.html
{% extends "shop/base.html" %}
{% block title %}
Thank you
{% endblock %}
{% block content %}
<h1>Thank you</h1>
<p>Your order has been successfully completed. Your order number is
<strong>{{ order.id }}</strong>.</p>
{% endblock %}
|
- shop/templates/shop/base.html
...
<div class="cart">
{% with total_items=cart|length %}
{% if total_items > 0 %}
Your cart:
<a href="{% url "cart:cart_detail" %}">
{{ total_items }} item{{ total_items|pluralize }},
${{ cart.get_total_price }}
</a>
{% elif not order %}
Your cart is empty.
{% endif %}
{% endwith %}
</div>
...
|
3. 성능 개선
3.1 비동기 작업
- HTTP 요청을 받으면 가능한 한 빨리 사용자에게 응답을 반환해야 함
- 주로 디버그 도구 등을 사용해서 요청/응답 주기의 여러 단계에 걸리는 시간과 수행된 SQL 쿼리의 실행 시간을 확인하면서 총 응답시간을 단축하기 위해 노력함
- 실행 시간이 오래 걸리는 작업은 서버 응답 속도를 심각하게 저하시킬 수 있음
- 시간이 오래 걸리는 작업을 완료하면서 사용자에게 빠른 응답을 반환하기 위한 방법의 하나로 비동기 실행을 활용함
3.1.1 비동기 작업으로 처리하기
동기(Synchronous)와 비동기(Asynchronous)
- 동기
- 직렬적으로 태스크를 수행하는 방식
- 요청을 보낸 후 응답을 받아야 다음 동작이 이루어지는 방식
- 어떠한 태스크를 처리할 동안 나머지 태스크는 대기
- 실제로 CPU가 느려지는 것은 아니지만 시스템의 전체적인 효율이 저하됨
비동기
- 병렬적으로 태스크를 수행하는 방식
- 요청을 보낸 후 응답의 수락 여부와는 상관없이 다음 태스크가 동작하는 방식
- A 테스크가 실행되는 시간 동안 B 태스크를 실행할 수 있으므로 자원을 효율적으로 사용할 수 있음
- 비동기 요청 시 응답 후 처리할 "콜백 함수"를 함께 알려줌으로써 해당 태스크가 완료되었을 때 "콜백 함수"가 호출되도록 함
- 그러나 비동기 처리를 위해 콜백 패턴을 사용하면 처리 순서를 보장하기 위해 여러 개의 콜백 함수가 중첩되어 복잡도가 높아지는 "콜백 헬(Callback Hell)이 발생하는 단점이 있음
- 콜백 헬은 가독성을 나쁘게 하며 실수를 유발하는 원인이 됨
- 콜백 헬이 발생하는 전형적인 사례
step1(function(value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
step5(value4, function(value5) {
// value5를 사용하는 처리
});
});
});
});
});
|
- 요청/응답 주기에서 일부 작업을 백그라운드에서 실행함으로써 작업을 분산시킬 수 있음
- 예시 1:
- 동영상 공유 플랫폼에서 사용자가 동영상을 업로드할 수 있게 할 때
- 업로드된 동영상을 트랜스코딩하는데 오랜 시간이 걸림
- 사용자가 동영상을 업로드하기 시작하면 사이트는 곧 트랜스코딩이 시작될 것이라는 응답을 반환
- 비동기적으로 동영상의 트랜스코딩 시작
- 사용자는 다른 작업을 수행하기 시작
- 동영상의 트랜스코딩이 완료되면 콜백을 통해 다음 순서의 기능을 호출
- 사용자는 트랜스코딩이 완료된 동영상을 사용 가능
- 예시 2:
- 사용자에게 이메일을 보내는 경우
- 사이트의 뷰에서 이메일로 알림을 보낼 경우에는 SMTP(Simple Mail Transfer Protocol) 연결에 실패하거나 응답 속도가 느려질 수 있음
- 이메일을 비동기식으로 보내면 코드 실행이 차단되는 현상을 방지할 수 있음
- 예시 1:
- 비동기 실행은 특히 데이터 집약적, 리소스 집약적, 시간 소모적인 프로세스나 실패할 수 있는 프로세스에 적절하지만 재시도 정책이 필요할 수 있음
3.1.2 워커, 메시지 큐 및 메시지 브로커
- 워커(Worker)
- 웹 서버가 요청을 처리하고 응답을 반환하는 동안 비동기 작업을 처리하려면 워커(Worker)라는 이름의 또 다른 작업 기반 서버가 필요함
- 하나 또는 여러 개의 워커가 백그라운드에서 돌면서 작업을 실행할 수 있음
- 워커는 데이터베이스에 액세스하고, 파일을 처리하고, 이메일을 보내는 등의 작업을 수행할 수 있음
- 워커는 향후 수행할 작업을 대기열에 추가할 수 있음
- 별도의 서버인 워커가 이런 작업을 수행하므로 메인 웹 서버는 HTTP 요청을 여유있게 처리할 수 있음
메시지 큐(Message Queue)와 메시지 브로커(Message Broker)
- 워커에게 어떤 작업을 실행할지 알려주기 위해서는 메시지를 보내야 함
- 이러한 메시지는 주로 선입선출(First In First Out, FIFO) 방식의 데이터 구조인 메시지 큐에 메시지를 넣어서 통신함
- 메시지 큐를 이용한 워커와의 통신은 메시지 브로커라는 모듈을 통해 이루어짐
- 메시지 브로커
- 메시징 미들웨어(Messaging Middleware) 또는 메시지 지향 미들웨어(Message Oriented Middleware) 솔루션 내의 소프트웨어 모듈
- 메시지 큐에 메시지 혹은 이벤트를 넣어주고 중개하는 역할을 하는 주체
- 작업 순서
- 메시지 브로커는 메시지 큐에서 첫 번째 메시지를 가져와 해당 작업을 실행시킴
- 작업이 완료되면 메시지 브로커는 메시지 큐에서 다음 메시지를 가져와 해당 작업을 실행시킴
- 메시지 큐가 비어있으면 메시지 브로커는 유휴상태로 전환
- 여러 브로커를 사용하는 경우, 각 브로커는 큐에서 사용 가능한 첫 번째 메시지부터 순서대로 가져옴
- 메시지 큐는 각 브로커가 한 번에 하나의 작업만 가져가는 것을 보장하며 어떤 작업도 여러 워커에 의해 처리되지 않도록 함
- 메시지 브로커는 메시지를 공식 메시징 프로토콜로 변환하고 여러 수신자의 메시지 큐를 관리하는데 사용됨
- 메시지 브로커는 메시지의 안정적인 저장과 메시지 전달을 보장함
- 메시지 브로커를 사용하면 메시지 큐를 만들고, 메시지를 라우팅하고, 작업자 간에 메시지를 배포하는 등의 작업을 수행할 수 있음
- 메시지 큐의 처리 방식
메시지 브로커의 처리 방식
3.2 Celery 및 RabbitMQ와 함께 장고 사용하기
3.2.1 Celery
- Celery
- 방대한 양의 메시지를 처리할 수 있는 분산 작업 큐
- Celery를 사용하면 비동기 작업을 쉽게 생성하고, 가능한 한 빨리 워커가 실행되도록 할 수 있으며, 특정 시각에 실행되도록 예약을 할 수 있음
- 메시지로 통신하며 클라이언트와 워커 사이를 중개하기 위해 메시지 브로커가 필요함
- Redis와 같은 키/값 저장소
- RabbitMQ와 같은 실제 메시지 브로커 등
3.2.2 RabbitMQ
- 가장 널리 배포된 메시지 브로커
- 고급 메시지 큐 프로토콜(AMQP, Advanced Message Queue Protocal)과 같은 여러 메시징 프로토콜을 지원
- Celery 사용 시 가장 권장되는 메시지 브로커
- 가볍고 배포하기 쉬우며 확장성과 고가용성을 위해 구성할 수 있음
3.2.3 Celery를 이용한 작업 내용
- Celery를 사용해서 비동기 작업을 장고 애플리케이션 내에 파이썬 함수로 정의
- 새로운 메시지를 가져와 비동기 작업을 처리하기 위해 메시지 브로커를 청취하여 새로운 메시지를 가져오는 Celery 워커 실행
3.2.4 Celery를 위한 환경 설정
- Celery 설치
pip install celery
|
- RabbitMQ 설치
docker pull rabbitmq
|
- RabbitMQ 구동
docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management
|
- RabbitMQ 페이지 접속
- 127.0.0.1:15672
- ID/PW: guest/guest
- myshop/celery.py
- 프로젝트에 Celery 추가하기
- Celery 커맨드라인 프로그램에 대해 DJANGO_SETTINGS_MODULE 변수 설정
- app = Celery('myshop')으로 애플리케이션 인스턴스 생성
- config_from_object() 메서드를 사용하여 프로젝트 설정에서 커스텀 구성 로드
- namespace 속성은 settings.py 파일에 Celery 관련 설정이 가질 접두사를 지정
- namespace='CELERY'를 설정하면 모든 Celery 설정의 이름에 'CELERY_' 접두사가 포함되어야 함(예: CELERY_BROKER_URL)
- 애플리케이션의 비동기 작업을 자동으로 검색하도록 Celery에 지시
- Celery는 INSTALLED_APPS에 추가된 애플리케이션의 각 애플리케이션 디렉토리에서 tasks.py 파일을 찾은 후, 이 파일에 정의된 비동기 작업을 로드함
- 프로젝트에 Celery 추가하기
import os
from celery import Celery
# Celery 프로그램에 대한 기본 장고 설정 모듈을 설정함
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myshop.settings')
app = Celery('myshop')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
|
- myshop/__ init__.py
- 장고가 시작될 때 Celery가 로드 되도록 함
# import celery
from .celery import app as celery_app
__all__ = ['celery_app']
|
3.2.5 Celery 워커 실행하기
- Celery 워커
- 전반적인 관리 기능을 처리하는 프로세스
- 대기열 메시지 송수신, 작업 등록, 멈춰있는 작업 종료, 상태 추적 등
- 워커 인스턴스는 여러 개의 메시지 큐에서 소비 가능
- Teminal
- 별도의 쉘을 열고 다음의 명령을 사용해서 프로젝트 디렉토리에서 Celery 워커를 시작함
celery -A myshop worker -l info
|
3.3 애플리케이션에 비동기 작업 추가하기
3.3.1 주문이 접수 시 사용자에게 확인 이메일 보내기
- orders/tasks.py
from celery import shared_task
from django.core.mail import send_mail
from .models import Order
@shared_task
def order_created(order_id):
# 주문이 성공적으로 생성될 때 이메일 알림을 보내는 작업 생성
order = Order.objects.get(id=order_id)
subject = f'Order nr. {order.id}'
message = f'Dear {order.first_name},\n\n' \
f'You have successfully placed an order.' \
f'Your order ID is {order.id}.'
mail_sent = send_mail(subject, message, 'admin@myshop.com', [order.email])
return mail_sent
|
- 장고로 이메일 보내기
- 장고로 이메일을 보내려면 로컬 SMPT(Simple Mail Transfer Protocol) 서버가 있거나, 이메일 서비스 공급자와 같은 외부 SMTP 서버에 액세스해야 함
- 장고 서버에 SMTP 구성
- EMAIL_HOST: SMTP 서버 호스트. 기본 값은 localhost
- EMAIL_PORT: SMTP 포트. 기본 값은 25
- EMAIL_HOST_USER: SMTP 서버의 사용자 이름
- EMAIL_HOST_PASSWORD: SMTP 서버의 비밀번호
- EMAIL_USE_TLS: TLS(Transport Layer Security) 보안 연결 사용 여부
- EMAIL_USE_SSL: SSL(Secure Socket Layer) 연결 사용 여부
- myshop/settings.py
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = "user_account@gmail.com"
EMAIL_HOST_PASSWORD = "****"
EMAIL_USE_TLS = True
# EMAIL_USE_SSL =
|
- myshop/settings.py
- SMTP 서버를 사용할 수 없거나 SMTP를 사용하기 위한 설정을 별도로 하지 않으려면 다음의 설정을 추가하여 장고가 콘솔에 이메일을 출력하도록 지시하도록 함
- 이 설정을 사용하면 장고는 모든 이메일을 보내는 대신 쉘에 출력함(테스트하기 좋음)
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
- orders/views.py
from orders.tasks import order_created
...
def order_create(request):
...
if request.method == "POST":
...
if form.is_valid():
...
cart.clear()
# 비동기 작업 실행
order_created.delay(order.id)
...
|
- Terminal
python manage.py runserver
|
파워쉘 3개를 이용해서 각각 실행해주어야함
728x90
'PYTHON-BACK' 카테고리의 다른 글
#파이썬 24.09.19_Django 기반의 테스트 주도 개발 방법론(TDD) (3) | 2024.09.19 |
---|---|
#파이썬 24.09.02(월) ~ 24.09.13(금) 마켓컬리 클론코딩 프로젝트 (2) | 2024.09.13 |
#파이썬 32일차_온라인 상점 구축 프로젝트2 (0) | 2024.08.26 |
#파이썬 32일차_온라인 상점 구축 프로젝트1 (0) | 2024.08.23 |
#파이썬 34일차_이커머스 클론코딩5_(2) (0) | 2024.08.22 |