본문 바로가기

PYTHON-BACK

#파이썬 33일차_온라인 상점 구축 프로젝트3

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 고객 주문 생성하기

  • 새로운 주문의 생성 단계
    1. 사용자에게 데이터를 입력할 주문 폼 제시
    2. 입력한 데이터로 새로운 Order 인스턴스 생성
    3. 각 아이템에 대한 해당 OrderItem 인스턴스 생성
    4. 카트의 모든 내용을 지우고 사용자를 성공 페이지로 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 요청
      1. OrderCreateForm 폼을 인스턴스화
      2. orders/order/create.html 템플릿을 렌더링
    • POST 요청
      1. 전송된 데이터의 유효성 검사
      2. 데이터가 유효하면 order = form.save()를 사용하여 데이터베이스에 새로운 주문을 생성
      3. 카트의 아이템들을 반복해서 각 아이템에 대한 OrderItem을 생성
      4. 카트의 콘텐츠 삭제
      5. orders/order/created.html 템플릿을 렌더링
  • 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가 느려지는 것은 아니지만 시스템의 전체적인 효율이 저하됨

(출처: https://poiemaweb.com/es6-promise)

비동기

  • 병렬적으로 태스크를 수행하는 방식
  • 요청을 보낸 후 응답의 수락 여부와는 상관없이 다음 태스크가 동작하는 방식
  • A 테스크가 실행되는 시간 동안 B 태스크를 실행할 수 있으므로 자원을 효율적으로 사용할 수 있음
  • 비동기 요청 시 응답 후 처리할 "콜백 함수"를 함께 알려줌으로써 해당 태스크가 완료되었을 때 "콜백 함수"가 호출되도록 함

 

(출처: https://poiemaweb.com/es6-promise)

  • 그러나 비동기 처리를 위해 콜백 패턴을 사용하면 처리 순서를 보장하기 위해 여러 개의 콜백 함수가 중첩되어 복잡도가 높아지는 "콜백 헬(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) 연결에 실패하거나 응답 속도가 느려질 수 있음
      • 이메일을 비동기식으로 보내면 코드 실행이 차단되는 현상을 방지할 수 있음
  • 비동기 실행은 특히 데이터 집약적, 리소스 집약적, 시간 소모적인 프로세스나 실패할 수 있는 프로세스에 적절하지만 재시도 정책이 필요할 수 있음

 

3.1.2 워커, 메시지 큐 및 메시지 브로커

  • 워커(Worker)
    • 웹 서버가 요청을 처리하고 응답을 반환하는 동안 비동기 작업을 처리하려면 워커(Worker)라는 이름의 또 다른 작업 기반 서버가 필요함
    • 하나 또는 여러 개의 워커가 백그라운드에서 돌면서 작업을 실행할 수 있음
    • 워커는 데이터베이스에 액세스하고, 파일을 처리하고, 이메일을 보내는 등의 작업을 수행할 수 있음
    • 워커는 향후 수행할 작업을 대기열에 추가할 수 있음
    • 별도의 서버인 워커가 이런 작업을 수행하므로 메인 웹 서버는 HTTP 요청을 여유있게 처리할 수 있음

메시지 큐(Message Queue)와 메시지 브로커(Message Broker)

  • 워커에게 어떤 작업을 실행할지 알려주기 위해서는 메시지를 보내야 함
  • 이러한 메시지는 주로 선입선출(First In First Out, FIFO) 방식의 데이터 구조인 메시지 큐에 메시지를 넣어서 통신함
  • 메시지 큐를 이용한 워커와의 통신은 메시지 브로커라는 모듈을 통해 이루어짐

  • 메시지 브로커
    • 메시징 미들웨어(Messaging Middleware) 또는 메시지 지향 미들웨어(Message Oriented Middleware) 솔루션 내의 소프트웨어 모듈
    • 메시지 큐에 메시지 혹은 이벤트를 넣어주고 중개하는 역할을 하는 주체

  • 작업 순서
    1. 메시지 브로커는 메시지 큐에서 첫 번째 메시지를 가져와 해당 작업을 실행시킴
    2. 작업이 완료되면 메시지 브로커는 메시지 큐에서 다음 메시지를 가져와 해당 작업을 실행시킴
    3. 메시지 큐가 비어있으면 메시지 브로커는 유휴상태로 전환

  • 여러 브로커를 사용하는 경우, 각 브로커는 큐에서 사용 가능한 첫 번째 메시지부터 순서대로 가져옴
  • 메시지 큐는 각 브로커가 한 번에 하나의 작업만 가져가는 것을 보장하며 어떤 작업도 여러 워커에 의해 처리되지 않도록 함
  • 메시지 브로커는 메시지를 공식 메시징 프로토콜로 변환하고 여러 수신자의 메시지 큐를 관리하는데 사용됨
  • 메시지 브로커는 메시지의 안정적인 저장과 메시지 전달을 보장함
  • 메시지 브로커를 사용하면 메시지 큐를 만들고, 메시지를 라우팅하고, 작업자 간에 메시지를 배포하는 등의 작업을 수행할 수 있음

  • 메시지 큐의 처리 방식

메시지 브로커의 처리 방식

 

 

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 파일을 찾은 후, 이 파일에 정의된 비동기 작업을 로드함
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