Namu | 나무 개발자 블로그입니다


[02] 장고 노하우 정리 by namu

django knowhow
image by castlemin in velog.io

목차

시리즈

참조




공부용으로 필요한 내용을 정리합니다.



13장. 장고 템플릿


대부분의 템플릿은 templates/ 에 넣어두자

우리가 선호하는 프로젝트 구성 부분을 참조하면, 프로젝트 내에서 templates/ 디렉토리의 위치를 알 수 있습니다. 이것 하위에 앱 혹은 필요한 분류에 따라 세분화하여 템플릿 파일들을 두면 됩니다.

템플릿 경로는 settings.py 설정파일 내 TEMPLATES 항목에 지정합니다.

간혹 각 앱의 하위에 각각 templates/ 디렉토리를 두는 경우도 있으니 알아둡시다.


템플릿 아키텍쳐 패턴

템플릿 문법을 활용해 템플릿 간 상속이 가능합니다. 따라서 앱별로 재사용 가능한 이중 혹은 삼중의 상속구조를 만들 수 있습니다. 그러나 불필요하게 복잡해진 구조는 유지보수의 복잡성을 야기하니 주의합시다.


템플릿에서 프로세싱 제한하기

장고는 뷰 단에서 템플릿으로 쿼리셋을 전달할 수 있는데, 이는 템플릿 수준에서 이터레이션을 통해 쿼리가 수행될 수 있음을 의미합니다.

이러한 기능은 매우 편리하고 직관적인 활용이 가능하지만 프로세싱이 이루어지는 만큼 다음과 같은 부정적 영향이 존재합니다.

한 마디로 복잡한 비즈니스 로직, 많은 데이터를 다루는 작업은 템플릿 및 자바스크립트 단에서 수행해서는 안 됩니다.

템플릿에서는 결과값을 보여주거나, 소량의 가벼운 프로세싱이 적합합니다.

  • (X, 하면 안됨!) 템플릿상에서 처리하는 aggregation 메서드
    • 대표적으로 count 집계가 있음. 이 때는,
    • 모델 매니저에 집계용 메서드를 작성하여 뷰에서 활용하는 방법을 사용
  • (X, 하면 안됨!) 템플릿상에서 조건문으로 하는 필터링
    • 템플릿에서 쿼리셋으로 반복문을 돌며 조건비교하는건 지양
    • 별도의 모델 매니저 메서드로 처리 후 뷰단에서 context data 로 반환할 것
  • (X, 하면 안됨!) 템플릿상에서 복잡하게 얽힌 쿼리들
    • 템플릿에서 User 객체를 iterating 하며 이에 속한 추가 객체들을 조회하는 경우.
    • {{ user.flavor.scoops_remaining }} 등이 있음
    • 이보다는 파이썬 단에서 select_related 기능을 활용할 것
  • (X, 하면 안됨!) 템플릿에서 생기는 CPU 부하
    • 많은 양의 이미지나 데이터를 처리하는 프로젝트에서는 사이트 성능을 올리기 위해 이러한 이미지 프로세싱 작업을 템플릿에서 분리해 뷰나 모델, 헬퍼 메서드, 또는 셀러리(Celery) 등을 이용한 비동기 메시지 큐 시스템으로 처리해야 함
  • (X, 하면 안됨!) 템플릿에서 숨겨진 REST API 호출
    • 템플릿에서 서드파티 지도 API 호출을 예로 들 수 있음


템플릿의 상속, 강력한 기능의 block.super

장고 템플릿 문법에서 여러 html 파일들이 base.html 을 상속받는 경우,

{% extends "base.html" %}
{% block stylesheets %}
    {# 'block.super' 를 사용하지 않음으로써 dashboard.css 만 사용하도록 stylesheets 재정의 #}
    <link rel="stylesheet" type="text/css" href="{% static 'css/dashboard.css' %}" />
{% endblock stylesheets %}

혹은 아예 stylesheets 블록을 재정의하지 않으면 부모(base.html)의 css 를 그대로 상속받게 됩니다.


그 외 유용한 사항들


템플릿 태그와 필터 (14장 내용)

템플릿 필터| 문자를 활용하여 템플릿 내에서 사용되는 단순명료한(?!) 함수로, 인자를 두 개만 받도록 제한되며 데이터의 외형을 수정(ex. 페이징 처리시)하거나 테스트 시 유용하게 사용됩니다.

템플릿 태그 는 필터보다 좀더 복잡한 기능을 태그를 사용하여 빠르고 쉽게 이용할 수 있도록 합니다. 사실상 프로그래밍적인 그 어떤것도 할 수 있습니다. (ex. url 태그는 form 양식이 제출되는 url 을 지정한 앱 대상으로 생성합니다)

개발자는 필요에 따라 위 내용을 준수하는 커스텀 템플릿 태그 혹은 필터를 생성할 수 있습니다.

커스텀 템플릿 태그와 필터는 주로 화면단에서 사용되기 때문에 (a) 복잡한 로직을 구현해서는 안 되며, (b) 재사용하기가 쉽지 않고, (c) 남용시 디버깅하기 어려워집니다.

템플릿 태그와 필터는 장고 템플릿의 작동 자체를 통제하는 기능을 절대 제공하지 않습니다.



16장. REST API


REST API 를 구현할 때 일반적으로 알아야 할 HTTP 상태 코드는 다음과 같습니다.


간단한 JSON API 구현하기

flavors 앱 예제를 활용합니다.

클래스 기반 뷰에 기초를 둔 패턴으로 REST API 를 효과적으로 구현하기 위해 django-rest-framework 를 이용하겠습니다.

참조: DRF 의 class-based views 와 serializers 상세설명 사이트

# flavors/models.py
from django.urls import reverse
from django.db import models


class Flavor(models.Model):

    title = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    scoops_remaining = models.IntegerField(default=0)
    
    def get_absolute_url(self):
        return reverse('flavors:detail', kwargs={'slug': self.slug})
# flavors/views.py
from rest_framework import serializers

from rest_framework.generics import ListCreateAPIView
from rest_framework.generics import RetrieveUpdateDestroyAPIView

from .models import Flavor


class FlavorSerializer(serializers.ModelSerializer):
    
    class Meta:
        model = Flavor
        fields = ('title', 'slug', 'scoops_remaining')


class FlavorCreateReadView(ListCreateAPIView):

    queryset = Flavor.objects.all()
    serializer_class = FlavorSerializer
    lookup_field = 'slug'


class FlavorReadUpdateDeleteView(RetrieveUpdateDestroyAPIView):
    queryset = Flavor.objects.all()
    serializer_class = FlavorSerializer
    lookup_field = 'slug'

뷰 모듈을 urls.py 로 묶으면 다음과 같습니다.

# flavors/urls.py
from django.conf.urls import url

from .views import FlavorCreateReadView, FlavorReadUpdateDeleteView

app_name = 'flavors'
urlpatterns = [
    url(
        regex=r'^api/$',
        view=FlavorCreateReadView.as_view(),
        name='flavor_rest_api',
    ),
    url(
        regex=r'^api/(?P<slug>[-\w]+)/$',
        view=FlavorReadUpdateDeleteView.as_view(),
        name='flavor_rest_api',
    ),
]


REST API 아키텍쳐

서비스 지향 아키텍쳐(Service-Oriented Architecture, SOA)

  • MSA(Micro-Service Architecture) 는 프로젝트의 각 컴포넌트를 독립된 서버 또는 클러스터에서 구동시켜 상호간에 커뮤니케이션이 이루어지도록 설계하는 것을 의미합니다.
  • 장고 프로젝트의 경우, 이러한 컴포넌트 간 커뮤니케이션에 REST API 를 이용합니다.
  • 여기서 가장 중요한 것은 장고 프로젝트 자체를 독립적으로 운영이 가능한 웹 애플리케이션으로 어떻게 나눌지 고려하는 것입니다.
  • 예제) 아이스크림 트럭 임대 앱
    • trucks, owners, renters, payments, receipts, reservations, scheduling, reviews 앱 단위로 프로젝트 분리하여 각각 REST API 구현



19장. 장고 어드민과 사용자 모델


장고의 어드민은 사이트 관리자를 위한 것이지 최종 사용자를 위한 것이 아니므로, 깊은 수준까지 커스터마이징할 필요가 없습니다.

경우에 따라 고객용 관리 대시보드 화면을 새롭게 구현하는 것이 어드민을 수정하는 것보다 더 효율적일 수 있습니다.


객체의 이름 보여주기

장고 어드민에서 모델의 이름은 <Model> object 형식으로 표기되지만, 모든 모델 클래스에 __str__() 메서드를 구현하면 모델의 name 필드의 값으로 표현됩니다.

...

class IceCreamBar(models.Model):

    ...

    def __str__(self):
        return self.name

또한 어드민에서 객체의 다른 필드들을 보여주기 원한다면, list_display 속성을 활용합니다.

# icecreambars/admin.py
from django.contrib import admin

from .models import IceCreamBar


@admin.register(IceCreamBar)
class IceCreamBarAdmin(admin.ModelAdmin):

    """ IceCreamBar Admin """

    list_display = (
        'name',
        'shell',
        'filling',
    )


장고의 사용자 모델 다루기 (20장 내용)

장고에서는 django.contrib.auth.models.User 클래스의 기본 사용자 모델이 존재합니다.

또한 이 모델은 클래스 상속을 통해 커스터마이징이 가능합니다. 이때는 username 과 password 필드가 기본적으로 정의되어 있는 django.contrib.auth.models.AbstractUser 추상화 클래스를 상속받습니다.

커스텀 사용자 모델은 필요에 따라 추가적인 필드를 구성할 수 있습니다.

AbstractBaseUser 의 서브클래스를 생성하는 경우

password, last_login, is_active 필드만 가진 기본 형태의 옵션

  1. User 모델이 기본으로 제공하는 필드(first_name, last_name)에 그리 만족하지 못할 때
  2. 기본 형태만 가진 가볍고 깨끗한 상태로부터 새로 서브클래스를 생성하기 원하면서 패스워드를 저장하기 위해 AbstractBaseUser 의 기본 환경의 장점을 이용하고 싶을 때



22장. 테스트, 문서화에 집착하자


먼저 장고 앱에 자동으로 생성되는 tests.py 를 모두 삭제하고 각 모듈에 맞는 test_<module name>.py 형식의 테스트 파일을 생성합니다.

테스트용 파일은 ‘test_‘ 접두어를 붙여야 장고의 테스트 러너가 인식함

  • test_models.py, test_forms.py, test_views.py, test_middleware.py, …


단위 테스트 작성하기

테스트 코드는 최소한의 시간을 들여 가장 의미 있게 만들어야 합니다.


통합 테스트(Integration Tests)란?

통합 테스트는 개별적인 소프트웨어 모듈이 하나의 그룹으로 조합되어 테스트되는 것을 말하는데, 단위 테스트 이후에 시행됩니다.

이것은 서드 파티 API 와의 연동을 포함하여 어플리케이션의 모든 부분이 잘 작동하는지 확인하는 훌륭한 방법입니다.

하지만 통합 테스트는 시스템 전체에 대한 확인과정을 거치기 때문에 단위 테스트에 비해 더 많은 주의를 요합니다.

예를 들어, 데이터베이스 레벨에서 벌어진 유니코드 변환 문제가 브라우저상의 문제로 보일 수도 있습니다.


지속적 통합

프로젝트 규모에 관계없이 프로젝트 저장소에 새로운 코드가 커밋될 때마다 테스트를 실행하는 지속적 통합 서버를 세팅하는 것을 추천합니다. 본 내용은 ‘32장. 지속적 통합’ 에서 자세히 다룹니다.


테스트 범위 게임

테스트를 최대한 많은 범위로 확장하는 것을 테스트 범위 게임이라고 합니다.

  1. 테스트 작성 시작하기
  2. 테스트 실행하기($ manage.py test) 그리고 커버리지 리포트(coverage.py) 작성하기
    • <project_root> 에서 다음과 같은 명령을 입력합니다
    • $ coverage run manage.py test -settings-twoscoops.settings.test
  3. 리포트 생성하기
    • <project_root> 에서 다음과 같은 명령을 입력합니다
    • $ coverage html -omit="admin.py"

처음에는 당연히 매우 낮은 테스트 커버리지를 가지고 있을 것입니다. 이 커버리지를 높여나가는 게임입니다.

새로운 기능을 추가하거나 버그 수정을 시작했을 때 테스트 커버리지가 이전보다 조금이라도 높아지지 않는다면 절대 commit and merge 하지 않습니다.

한 번에 급격히 증가하는 테스트 커버리지보다 점진적으로 발전하는 테스트 커버리지가 좋습니다. 개발자들이 의미 없는 가짜 테스트를 넣고 있지 않다는 의미이기 때문입니다.


unittest 대안

장고의 unit test 모듈 은 파이썬 표준 라이브러리인 unittest 를 사용합니다.

unittest 는 클래스 기반(class-based) 으로 코드를 작성하게 되는데, 그만큼 확장 면에서 강력하고 유용하지만 너무 많은 코드를 요한다는 불편함도 존재합니다.

따라서 대안으로 pytest, nose 라이브러리를 사용하기도 합니다.

# test_models.py
"""Using pytest"""
from pytest import raises

from cones.models import Cone


def test_good_choice():
    assert Cone.objects.filter(type='sugar').count() == 1


def test_bad_cone_choice():
    with raises(Cone.DoesNotExist):
        Cone.objects.get(type='spaghetti')

위와 같이 pytest 를 활용하면 짧은 코드의 함수 기반 테스트 코드를 작성할 수 있습니다.

함수는 간편한 대신 클래스의 공통 테스트 기능 확장에 한계가 있기도 하다는 사실을 기억합시다.


문서화에 집착하자 (23장 내용)

문서 작성을 위해 reStructuredTextSphinx 같은 도구를 사용합니다.

  1. 파이썬 프로젝트 문서화에 일반적으로 사용되는 reStructuredText (RST) 마크업 언어
  2. reStructuredText 로부터 스핑크스를 이용하여 문서 생성하기
    • Sphinx.rst 파일을 보기 좋게 꾸며진 문서로 변환해주는 도구입니다.
    • 스핑크스 문서 생성에 대한 방법은 reStructuredText 공식 문서를 살펴봅시다.

적어도 매주마다(주기적으로) 스핑크스 문서를 빌드합시다.

프로젝트에서 작성해야 하는 문서들은 다음과 같습니다.



24장. 장고 성능 향상시키기


이 장에서는 병목 현상을 찾아내 장고 프로젝트를 좀 더 빠르게 만드는 방법을 다룹니다.


쿼리로 무거워진 페이지의 속도 개선

장고 공식: DB optimization

성능 디버깅을 위해 유용한 툴


(1) 쿼리 수 줄이기


(2) 일반 쿼리 빠르게 하기

다음은 개별 쿼리 속도를 높이는 데 시작점으로 삼기 좋은 팁입니다.

쿼리를 다시 작성할 때는 다음과 같이 진행합니다.

  1. 가능한 작은 크기의 결과가 반환되도록 재구성
  2. 인덱스가 좀더 효과적으로 작동할 수 있도록 모델 재구성
  3. ORM 보다 효율적이라면, 직접 SQL 이용해보기


(3) ATOMIC_REQUESTS 비활성화하기

절대 다수의 사이트에서는 한 request 에서 발생하는 모든 쿼리를 하나의 트랜잭션 처리하도록 ATOMIC_REQUESTS 값을 True 로 설정해도 문제가 없습니다.

하지만 특정 트랜잭션에서 병목 현상이 일어나는 경우, 아예 이 설정값을 비활성화(False 로 변경)하는 것을 고려할 수 있습니다.

혹은 해당 트랜잭션 뷰(view)에 @transaction.non_atomic_requests 데코레이터를 적용하고, 필요한 쿼리수행 코드만 with transaction.atomic(): ... 구문으로 감싸 별도 처리할 수 있습니다.


데이터베이스의 성능 최대한 이용하기

데이터베이스 접근 최적화에 대해 세부적으로 다뤄봅니다.


Memcached 나 레디스(Redis)를 이용하여 쿼리 캐시하기

Memcached레디스(Redis) 를 바인딩할 수 있는 파이썬 패키지를 통해 이용이 가능합니다.

사이트 전반에 적용되는 캐시를 설정할 수도 있고 각 뷰나 템플릿별로 할 수도 있습니다.


캐시를 이용할 곳 정하기

메모리 캐시를 어느 곳에 우선적으로 적용할지 결정해야 합니다.


서드 파티 캐시 패키지

서드 파티 캐시 패키지는 다음과 같은 기능을 제공합니다.

몇몇 인기 있는 장고 캐시 패키지는 다음과 같습니다.


HTML, CSS, Javascript 압축과 최소화하기

책 참조(p290)


업스트림 캐시나 CDN 이용하기

책 참조(p290)



25장. 비동기 태스크 큐


비동기 태스크 큐(asynchronous task queue) 란 태스크가 실행되는 시점이 태스크가 생성되는 시점과 다르고 태스크의 생성 순서와도 연관 없이 실행되는 작업을 의미합니다.

이와 관련된 용어는 다음과 같습니다.

다시 말해 비동기 태스크 큐는 사전에 지정된 순서에 따라 원하는 작업이 순차적으로 수행되는 것을 의미합니다. 이때 다음 작업은 이전 작업이 완료되고 하드웨어 자원이 가용한 시점에 호출됩니다.

이것이 정말 필요한지 가늠할 수 있는 기준은 다음과 같습니다.

사이트의 트래픽 규모에 따라 예외가 있을 수 있습니다.


태스크 큐 소프트웨어 선택하기

Celery, Redis Queue, django-background-tasks 가 있습니다.


태스크 큐에 대한 실전 방법론

다음은 태스크 큐 패키지에 공통으로 적용 가능한 일반적인 방법론으로써, 각 태스크 기능이 이식성독립성을 갖게 하는데 큰 도움을 줍니다.

따라서 사이트가 성장하여 패키지를 전환할 필요가 발생할 때 이전이 쉬워집니다.


(1) 태스크를 뷰처럼 다루자

앞서 뷰(view) 를 가능한 한 작고 단일 기능으로 구성하도록 권장했는데, 이와 유사하게 태스크 로직을 구성해야 합니다.

복잡해지거나 중복 사용되는 코드는 헬퍼 함수로 별도 모듈화하도록 합시다.


(2) 태스크 또한 리소스를 이용한다

태스크 실행을 위한 메모리와 리소스에도 한계가 있기 때문에 최대한 간결하고 리소스를 낭비하지 않는 방향으로 코드를 작성해야 합니다.


(3) JSON 화 가능한 값들만 태스크 함수에 전달하라

뷰와 같은 태스크 함수의 인자는 JSON 화 가능한 값으로만 제한해야 합니다. (정수, 부동 소수점, 문자열, 리스트, 튜플, 딕셔너리 타입만 허용하도록)

복잡하게 얽힌 객체를 인자로 이용하지 말아야 합니다.


(4) 태스크와 워커를 모니터링하는 방법을 익혀 두라

태스크와 워커의 상태를 시각적으로 확인할 수 있는 방법을 반드시 익혀 두어야 합니다.


(5) 로깅!

태스크 큐 작업은 기본적으로 백그라운드에서 진행되므로, 에러가 일어나기 쉬운 태스크의 경우 디버깅을 위해 각 태스크 함수 내에 로그를 남겨야 합니다.


(6) 백로그 모니터링하기

트래픽이 점점 증가하는데 충분한 수의 워커가 제공되지 못한다면 태스크가 점점 쌓일 수밖에 없습니다.

이 경우 워커 수를 늘리는 것을 고려해야 합니다. 하나의 워커만 이용이 가능한 경우(django-background-tasks), 샐러리나 레디스 큐로 업그레이드를 고려해야 합니다.


(7) 죽은 태스크들 주기적으로 지우기

태스크가 큐로 전달되었음에도 모종의 이유로 아무런 반응(리턴)이 돌아오지 않는 경우, 이러한 죽은 태스크를 주기적으로 지우는 방법을 고심해야 합니다.

원인은 사이트의 버그 등으로 아무 반응이 없거나, 리소스가 더 이상 존재하지 않는 등 여러가지가 있을 수 있습니다.


(8) 불필요한 데이터 무시하기

태스크가 완료되면 브로커는 태스크 성공과 실패를 기록하게 설계되어 있습니다. 이는 통계 정보 추출에 유용한 정보이나, exit 결과값 자체는 태스크의 결과물이 아닙니다.

exit 결과물이 불필요한 저장소를 차지하지 않도록 보통은 이런 기능을 비활성화 시킵니다.


(9) 큐의 에러 핸들링 이용하기

네트워크나 서드파티 API 문제 등 예상치 못한 이유로 태스크가 실패하는 경우가 있습니다.

태스크 큐 소프트웨어는 이러한 에러에 대한 핸들링 기능을 제공합니다.

재시도 전 지연 시간은 적어도 10초 이상 기다리도록 하는 것을 권장합니다. 또한 재시도 할때마다 이 간격이 커지게 설정해 주면 좋습니다.


(10) 태스크 큐 소프트웨어의 기능 익히기

각 태스크 큐 소프트웨어의 특성을 인지하고 적절한 기능을 판단해 사용하시기 바랍니다.


태스크 큐 참고자료

  • http://www.fullstackpython.com/task-queues.html
  • http://www.2scoops.co/why-task-queues
  • https://pypi.python.org/pypi/django-transaction-hooks (트랜잭션 후 커밋 훅을 지원하는 장고 데이터베이스 백엔드)

    샐러리 관련

  • http://celeryproject.com, https://denibertovic.com/posts/celery-best-practices/ (셀러리 홈페이지, 필독서)
  • https://pypi.python.org/pypi/flower (셀러리 클러스터 관리를 위한 웹 기반 도구)
  • http://wiredcraft.com/blog/3-gotchas-for-celery/
  • https://www.caktusgroup.com/blog/2014/06/23/scheduling-tasks-celery/

    레디스 큐 관련

  • http://python.rq.org, http://racingtadpole.com/blog/redis-queue-with-django

    django-background-tasks 관련

  • https://pypi.python.org/pypi/django-background-tasks


[이전글] [다음글]