(Django) Template 프로세싱의 한계에 따른 이슈

duo2208·2022년 1월 23일
0

Django

목록 보기
13/23
post-thumbnail

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


단순하고 그다지 길지 않은 템플릿 코드라도 상당한 프로세싱을 필요로 하는 객체가 호출될 가능성이 있다. 템플릿이 수많은 중첩문과 복잡한 조건문 그리고 데이터 프로세싱들로 가득 차 있다면 템플릿에서 비즈니스 로직을 재사용하기는 더 어려워질 것이다. 장고 앱을 코드 재사용이 가능하도록 구성하는 것은 특히 요즘처럼 API 개발이 점점 늘어나는 시대에 매우 중요한 요소이다. 그러므로 템플릿 코드에 미니멀리스트 접근법을 이용하기를 권한다.

템플릿에서 처리하는 프로세싱은 적으면 적을수록 좋다. 템플릿 레이어에서의 쿼리 수행과 이터레이션은 특히 문제가 된다. 템플릿상에서 쿼리세트를 가지고 이터레이션을 할 때마다 다음 질문을 해보길 바란다.

(1) 쿼리세트가 얼마나 큰가?
(2) 얼마나 큰 객체가 반환되는가? 모든 필드가 정말로 다 템플릿에서 필요한가?
(3) 각 이터레이션 루프 때마다 얼마나 많은 프로세싱이 벌어지는가?

그러면 이제 효과적으로 쓸 수 있는 템플릿 코드를 예시를 통해 알아보자.
신청하는 첫 100만 명의 고객에게 무료 아이스크림을 주기로 상상해보자.

# vouchers/models.py
from django.core.urlresolvers import reverse
from django.db import models
from .managers import VoucherManger

class Voucher(models.Model):
	""" 무료 아이스크림 상품권 """
	name = models.CharField(max_length=100)
 	email = models.EmailField()
 	address = models.TextField()
	birth_date = models.DateField(blank=True)
 	sent = models.BooleanField(default=False)
	redeemed = models.BooleanField(default=False)
	objects = VoucherManager()

+ [주의1] 템플릿상에서 처리하는 aggreation 메서드

신청자가의 생년월일을 가지고 무료 쿠폰 발매와 이용빈도를 연령대별로 분석해 본다고 가정합시다. 여기서 가장 피해야 할 구성은 바로 템플릿상의 자바스크립트로 이를 해결하려는 것입니다.

  • 자바스크립트 변수에 연령대별 카운트 수를 저장해서 템플릿의 자바스크립트로 무료 쿠론 리스트를 체크(이터레이션을 이용)하지 말자. 자바스크립트는 그 변수에 연령대별 카운트 수를 저장하는 정도로만 쓰자.
  • 무료 쿠폰 개수를 전부 더하기 위해 add 템플릿 필터를 이용하지 말자.

➔ Avoid code : 템플릿상의 자바스크립트로 해결

{# templates/vouchers/ages.html #}
{% extends 'base.html' %}

{% block content %}
<table>
  <thead>
    <tr>
    	<th>Age Bracket</th>
    	<th>Number of Vouchers Issued</th>
    </tr>
  </thead>
  
  <tbody>
  	{% for age_bracket in age_brackets %}
 	<tr>
		<td>{{ age_bracket.title }}</td>
  		<td>{{ age_bracket.count }}</td>
 	</tr>
 	{% end for}
  </tbody>
</table>
{% end content %}

➔ Good Code : 장고 ORM의 aggreation 메서드를 이용하여 모델 매니저로 처리

이 메서드는 뷰에서부터 호출될 것이고 해당 결과는 템플릿에 콘텍스트 변수(context variable) 형태로 전달된다.

# vouchers/managers.py
from django.utils import timezone
from dateutil.relativedelta import relativedelta
from django.db import models

class VoucherManage(models.Manager):
	def age_breakdown(self):
	""" 연령대별 카운트 수를 저장한 딕셔너리 변환 """
	age_brackets = []
	now = timezone.now()

	delta = now - relativedelta(years=18)
	count = self.models.objects.filter(birth_date__gt=delta).count()
	age_brackets.append(
 		{"title": "0-17", "count": count}
	)
	count = self.models.objects.filter(birth_date__lte=delta).count()
	age_brackets.append(
 		{"title": "18+", "count": count}
 	)
	return age_brackets

+ [주의2] 템플릿상에서 조건문으로 하는 필터링

무료 쿠폰을 신청한 사람들 중에서 성이 'Greenfeld' 인 사람과 'Roy' 인 사람들만을 따로 추려본다고 하자. 이를 위해 우선 이름의 '성' 필드를 가지고 필터링 해야 한다. 피해야 할 방법은 템플릿상에서 거대한 루프문과 if문을 돌려서 찾아내는 것이다.

➔ Avoid code : 루프를 돌면서 여러 if문으로 조건 검색

이러한 큰 리스트의 루프는 템플릿상에서 처리되기 위해 만들어진 것이 아니다. 따라서 성능이 심각하게 저하될 것이다. 동시에 PostgreSQL이나 MySQL은 데이터를 필터링하는데 상당히 최적화된 기능을 가지고 있다. 장고의 ORM은 이런 기능을 이용하는데 유용하게 쓰일 수 있다.

<h2>Greenfelds Who Want Ice Cream</h2>
<ul>
{% for voucher in voucher_list %}
	{# 절대 따라하지 말 것 : 템플릿에서 조건을 이용한 필터링 #}
	{% if 'greenfeld' in voucher.name.lower %}
		<li>{{ voucher.name }}</li>
	{% endif %}
{% endfor %}
</ul>

<h2>Roys Who Want Ice Cream</h2>
<ul>
{% for voucher in voucher_list %}
	{# 절대 따라하지 말 것 : 템플릿에서 조건을 이용한 필터링 #}
	{% if 'roy' in voucher.name.lower %}
		<li>{{ voucher.name }}</li>
	{% endif %}
{% endfor %}
</ul>
  

➔ Good code : 장고의 ORM을 이용하여 로직 이전

모델 매니저로 필터링 로직을 이전함으로써 쉽게 속도 향상을 꾀할 수 있다. 이렇게 함으로써 템플릿을 가지고 미리 필터링된 결과를 쉽게 표현할 수 있다.

# vouchers/views.py
from django.views.generic import TemplateView
from .models import Voucher

class GreenfeldRoyView(TemplateView):
	template_name = "vouchers/views_conditional.html"
	
 	def get_context_data(self, **kwargs):
		context = super(GreenfeldRoyView, self).get_context_data(**kwargs)
 		context["greenfelds"] = \
 			Voucher.objects.filter(name__icontains="greenfeld")
 		context["roys] = Voucher.objects.filter(name__icontains="roys")
 		return context
<h2>Greenfelds Who Want Ice Cream</h2>
<ul>
{% for voucher in greenfelds %}
	<li>{{ voucher.name }}</li>
{% endfor %}
</ul>

<h2>Roys Who Want Ice Cream</h2>
<ul>
{% for voucher in roys %}
	<li>{{ voucher.name }}</li>
{% endfor %}
</ul>

+ [주의3] 템플릿상에서 복잡하게 얽힌 쿼리들

장고 템플릿에서 로직을 구현하는 것을 자제해야 하는데도, 뷰에서 불필요한 쿼리를 자주 호출하는 것을 종종 본다. 예시로 우리 사이트의 이용자들과 그들이 선호하는 아이스크림을 다음과 같이 나타낸다고 해보자.

➔ Avoid code : 생성된 암묵적인 쿼리

이렇게 추출된 각 사용자는 또 다른 쿼리를 호출하게 된다. 그렇게 쿼리가 많아 보이지는 않지만 사용자가 많아지고 이런 구현이 늘어날때마다 결국 심각한 문제에 봉착할 것이다.

{# list genereataed via User.objects.all() #}
<h1>Ice Cream Fans and their favorite flavors.</h1>

<ul>
{% for user in user_list %}
	<li>
		{{ user.name }}
 		{{ user.flavor.title }}
		{{ user.flavor.scoops_reamaining }}
	</li>
{% endfor %}
</ul>

➔ Good Code : 장고 ORM의 selected_related() 메서드를 이용

장고 ORM의 selected_related() 메서드를 이용하여 고쳐보면 다음과 같다.

{% comment %}
List generated via User.objects.all().select_related("flavors")
{% endcomment %}

<h1>Ice Cream Fans and their favorite flavors.</h1>

<ul>
{% for user in user_list %}
	<li>
		{{ user.name }}
 		{{ user.flavor.title }}
		{{ user.flavor.scoops_reamaining }}
	</li>
{% endfor %}
</ul>

+ [주의4] 템플릿에서 생기는 CPU 부하

템플릿상에서 무심코 구현된 로직이 상당한 CPU 부하를 일으키는 경우를 주의하라. 단순하고 그다지 길지 않은 템플릿 코드라도 상당한 프로세싱을 필요로하는 객체가 호출될 가능성이 있다.

흔한 예를 들면 sorl-thumbnail 과 같은 라이브러리에서 제공되는 이미지 처리를 하는 템플릿 태그를 들 수 있다. 대부분은 문제가 되지 않지만 간혹 문제가 발생하는 경우가 있는데, 파일 시스템(때론 네트워크)에서 이미지를 처리하고 저장하는 작업이 템플릿 안에 존재할 경우가 그렇다.

따라서 많은 양의 이미지나 데이터를 처리하는 프로젝트에서는 사이트 성능을 올리기 위해 이러한 이미지 프로세싱 작업을 템플릿에서 분리해 뷰나 모델, 헬퍼 메섣, 셀러리 등을 이용한 비동기 메시지 큐 시스템으로 처리해야한다.

+ [주의5] 템플릿에서 숨겨진 REST API 호출

템플릿에서 객체 메서드를 호출함으로써 로딩 시간이 늘어나기도 한다. 이는 비단 로딩하는데 자원이 많이 드는 메서드뿐만 아니라 REST API를 호출하는 메서드에서도 마찬가지이다. 일례로 프로젝트에 반드시 필요하지만 매우 느린 서드 파티 서비스의 지도 API를 들 수 있다. 뷰로 전달될 객체가 포함된 메서드를 템플릿에서 호출하는 일은 자제하기 바란다.
그럼 어디서 API를 호출해야 할까? 다음의 방법을 추천한다.

  • 자바스크립트 코드 : 페이지 내용이 다 제공된 다음에 클라이언트 브라우저에서 자바스크립트로 처리한다. 이럴 경우 데이터가 로딩되기를 기다리는 중에 사용자의 이목을 다른 곳으로 끌거나 여러 재미있는 기능을 제공할 수도 있다.
  • 느린 프로세스를 메시지 큐, 스레드, 멀티프로세스 등의 방법으로 처리하는 뷰의 파이썬코드



템플릿 태그의 성능 문제


템플릿 태그와 필터에 너무 많은 로직을 구겨넣을 경우 심각한 성능 문제를 야기할 수 있다. 템플릿 태그는 디버깅과 재사용이 어렵다는 문제가 있으므로 새로운 템플릿을 추가하기 앞서 다음사항을 반드시 고려해 보자.

  • 데이터를 읽고 쓰는 작업을 할 것이라면 모델이나 객체 메서드(object method)가 더 나은 장소일 것이다.
  • 프로젝트 전반에서 일관된 작명법을 이용하고 있기 때문에 추상화 기반의 클래스 모델을 core.models 모듈에 추가할 수 있다. 프로젝트 추상화 기반 클래스 모델에서 어떤 메서드나 프로퍼티가 우리가 작성하려는 커스텀 템플릿 태그와 같은 일을 하는가?

언제 새로운 템플릿 태그를 작성하는 것이 좋겠냐고 물어본다면 HTML을 렌더링하는 작업이 필요할 때라고 조언하고 싶다. 예를 들면 각자 다른 다양한 모델이나 데이터 타입을 필요로 하는 매우 복잡한 HTML 레이아웃을 가진 프로젝트에서 템플릿 태그를 이용하면 좀 더 유연하고 이해하기 쉬운 템플릿 아키텍쳐를 구현할 수 있는 경우가 이에 해당할 것이다.


📌 참고 출처

0개의 댓글