Django template form - http data request, response / sync async / race condition

정현우·2021년 10월 20일
4

Django Basic to Advanced

목록 보기
9/37
post-thumbnail

튜토리얼 이어서

  • 4장을 보고 5장을 이어가길 바란다. 공식 문서 튜토리얼에 내 입맛과 부족한 정보를 담아서 정리하는 것에 가깝다.

  • 5장에 핵심이 꽤 많이 있다. 키워드는 http data request, response / sync async / race condition 정도다.

template 에서 Django Back-end로 http request를 보내보자. Asynchronous JavaScript and XML를 안다면 비동기식과 동기식을 알것이다. Form tag와 같이 submit action을 하는 동기식 이벤트 기반으로 먼저 request를 처리해보자.

Form and Request

  • method와 동기식/비동기식으로 서버(Back-end)에 http request를 보내는 것을 잘 모르겠다면 우선 넘어가면 된다. http통신은 '관행'뿐 아니라 '규칙'이 명백하게 존재하고, 더 깊이있게 바라보면 response code와 get/post/put or patch/delete/option 등의 request method를 기반으로, 불변 그리고 멱등성 등의 웹 개발에서 가장 기본이자 기초 그리고 꼭 알아야할 정보다. 공식문서 체크

Form 만들기

  • polls/templates/polls/detail.html 을 아래와 같이 바꿔주자. form 태그를 추가하는 것이다. form tag에 대한 정보는 필수이자 핵심이다. 잘 모른다면 MDN공식문서를 꼭 참고하자.
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <form action="{% url 'polls:vote' question.id %}" method="post">
        {% csrf_token %}
        <fieldset>
            <legend>
                <h1>{{ question.question_text }}</h1>
            </legend>
            {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
            {% for choice in question.choice_set.all %}
            <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
            <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
            {% endfor %}
        </fieldset>
        <input type="submit" value="Vote">
    </form>
</body>

</html>
  • 위의 템플릿은 각 질문 선택 항목에 대한 라디오 버튼을 표시합니다. 각 라디오 버튼의 value는 연관된 질문 선택 항목의 ID입니다. 각 라디오 버튼의 name은 "choice"입니다. 즉, 누군가가 라디오 버튼 중 하나를 선택하여 폼을 제출하면, POST 데이터 인 choice=#을 보낼 것입니다. 여기서 #은 선택한 항목의 ID입니다. 이것은 HTML 폼의 기본 개념입니다.

  • 폼의 작업을 {% url 'modice:modice' question.id %}(으)로 설정하고 method="post"를 설정했습니다. method="post"(method="get")를 사용하는 것은 매우 중요합니다. 이 양식을 제출하면 데이터 서버 쪽이 변경되기 때문입니다. 데이터 서버 측을 변경하는 양식을 작성할 때마다 method="post"를 사용하십시오. 이 팁은 장고에만 국한된 것이 아니라 일반적으로 좋은 웹 개발 관행입니다.

  • forloop.counter 는 for 태그가 반복을 한 횟수를 나타냅니다.

  • POST 양식을 만들고 있기 때문에(데이터를 수정하는 효과가 있을 수 있음), 사이트 간 위조 요청에 대해 걱정해야 합니다. 다행히도, 여러분은 너무 걱정하지 않아도 됩니다, 왜냐하면 장고는 그것으로부터 보호하는 유용한 시스템을 가지고 있기 때문입니다. 즉, 내부 URL을 대상으로 하는 모든 POST 양식은 {% csrf_token %} 템플릿 태그를 사용해야 합니다.

  • csrf 토큰 값은 쉽게말하면 변조불가능하게, 요청할때 마다 담아서 보내 올바는 것인지 체크하는 것을 도와주는 값이다. 깊게 말하면 Cross-site request forgery 공격을 막기 위해 설정하는 것이다. 위키 참조

form request - reponse

  • form의 요청 도착지(action)을 보면 url 'polls:vote' question.id 이다. 그럼 해당 링크에 매핑되어 있는 views함수에서 비즈니스 로직을 만들어 보자.
# polls/views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, Question

... # 중략 
# question을 가져오고, 선택한 옵션에 대해 vote값을 ++ 해준다 
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
  • request.POST 는 키로 전송된 자료에 접근할 수 있도록 해주는 사전과 같은 객체입니다. 이 경우, request.POST['choice'] 는 선택된 설문의 ID를 문자열로 반환합니다. request.POST 의 값은 항상 문자열들입니다.

  • Django는 같은 방법으로 GET 자료에 접근하기 위해 request.GET 를 제공합니다 – 그러나 POST 요청을 통해서만 자료가 수정되게하기 위해서, 명시적으로 코드에 request.POST 를 사용하고 있습니다.

  • 만약 POST 자료에 choice 가 없으면, request.POST['choice'] 는 KeyError 가 일어납니다. 위의 코드는 KeyError 를 체크하고, choice가 주어지지 않은 경우에는 에러 메시지와 함께 설문조사 폼을 다시보여줍니다.

  • 설문지의 수가 증가한 이후에, 코드는 일반 HttpResponse 가 아닌 HttpResponseRedirect 를 반환하고, HttpResponseRedirect 는 하나의 인수를 받습니다: 그 인수는 사용자가 재전송될 URL 입니다. (이 경우에 우리가 URL을 어떻게 구성하는지 다음 항목을 보세요).

  • 위의 Python 코멘트가 지적했듯이, POST 데이터를 성공적으로 처리한 후에는 항상 HttpResponseRedirect를 반환해야 합니다. 이 팁은 장고에만 국한된 것이 아니라 일반적으로 좋은 웹 개발 관행입니다.

  • 우리는 이 예제에서 HttpResponseRedirect 생성자 안에서 reverse() 함수를 사용하고 있습니다. 이 함수는 뷰 함수에서 URL을 하드코딩하지 않도록 도와줍니다. 제어를 전달하기 원하는 뷰의 이름을, URL패턴의 변수부분을 조합해서 해당 뷰를 가리킵니다. 여기서 우리는 튜토리얼 3장에서 설정했던 URLconf를 사용하였으며, 이 reverse() 호출은 아래와 같은 문자열을 반환할 것입니다.

  • 투표 후 결과 페이지 '/polls/3/results/'로 우리는 redirect를 해준다. 해당 뷰 이벤트와 템플릿을 만들어 주자.

# polls/views.py
... # 중략
# vote의 결과 페이지 
def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})
  • template은 templates/polls/results.html 이다.
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h1>{{ question.question_text }}</h1>

    <ul>
        {% for choice in question.choice_set.all %}
        <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
        {% endfor %}
    </ul>

    <a href="{% url 'polls:detail' question.id %}">Vote again?</a>
</body>

</html>
  • 이제, 웹 브라우저에서 /polls/1/ 페이지로 가서, 투표를 해보세요. 당신이 투표를 할 때마다 값이 반영된 결과 페이지를 볼 수 있을 것입니다. 만약 당신이 설문지를 선택하지 않고 폼을 전송했다면, 오류 메시지를 보게 될 것입니다.

  • 여기서 흥미롭게 살펴볼 부분이 있다. 지금은 우리만 투표를 하지만, 동시에 2유저가 투표를 진행하면 어떻게 될까?

  • 우리의 vote() 뷰에는 작은 문제가 있습니다. 먼저 데이터베이스에서 selected_choice 객체를 가져온 다음, votes 의 새 값을 계산하고 나서, 데이터베이스에 다시 저장합니다. 만약 여러분의 웹사이트에 두 명의 사용자가 정확하게 같은 시간 에 투표를 할려고 시도할 경우, 잘못될 수 있습니다. votes 의 조회값이 42라고 할 경우, 두 명의 사용자에게 새로운 값인 43이 계산 되고, 저장됩니다. 그러나 44가 되야되겠죠. 이를 경쟁 상태(race condition) 라 합니다. 이 문제를 해결할 수 있는 방법을 알아보려면 Avoiding race conditions using F() 를 참고하세요.

  • 사실 'race condition'은 'OS'(운영체제)에서 많이 사용하는 단어다. 대표적인 경우가 위와 동일하다. 예를 들어 멀티 쓰레드 환경에서 공유 자원(shared data)에 여러 쓰레드가 접근해 값을 수정하면 데이터의 일관성(consistency)이 깨지게 되는데, 이를 경쟁 상태(Race Condition)라고 한다. 이 경쟁 상태를 해결하기 위한 것이 동기화(Synchronization)다. 보통 예시는 java기반으로 많이 든다. CS지식 얻기

request 경쟁상태 해결

  • 사실 race condition에 대한 핸들링은 시리즈를 하나 팔 정도로 방대하다. 하지만 django에서 어떻게 '회피'하는지 간단하게 살펴보자!

  • 우선 F 표현식을 알아야 한다. 공식문서에서는 아래와 같이 표현한다.

  • F() 객체는 모델의 필드 혹은 어노테이트된 열의 값을 나타낸다. 실제로 데이터베이스에서 Python 메모리로 가져오지 않고, 모델 필드 값을 참조하고 이를 데이터베이스에서 사용하여 작업할 수 있다.

reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed += 1
reporter.save()

# 위 코드를 F를 사용하면
from django.db.models import F

reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed = F('stories_filed') + 1
reporter.save()
  • 위의 예제의 reporter.stories_filed = F('stories_filed') + 1 이 라인에서 Python 연산자를 사용한 것처럼 보이지만 아니다. Django에서는 F() 객체를 만나면, Python 연산자를 오버라이딩하여 캡슐화된 SQL문을 생성한다.

  • 위의 작업은 전적으로 데이터베이스에서 처리하므로, Python에서는 reporter.stories_filed에 대해 알 수 없다.

빠르게 장점

  1. Python이 아닌 데이터베이스에서 해당 연산을 처리한다. -> DB에게(정확하겐 DBMS) 연산을 맡김
  2. 몇몇 작업에 필요한 쿼리 수를 줄일 수 있다.
  3. 그리고 F() 객체의 또 다른 이점은 경쟁 조건 (race condition)을 피할 수 있다는 점.

출처 : https://blog.myungseokang.dev/posts/django-f-class/


제너릭 뷰 사용하기

  • 확실하게 명시할 수 있다면, 코드는 짧으면 관리하고 코딩하기 편해진다. 하지만 '무조건 숏코딩이 최고다' 는 절대 아니다. 오히려 변수명과 코드가 길고 양이 많을 수 록 협업과 빠르게 공유하고 작업하기 용이한 경우가 다반사다.

url, views 코드 리펙토링

  • 뷰는 URL에서 전달 된 매개 변수에 따라 데이터베이스에서 데이터를 가져 오는 것과 템플릿을 로드하고 렌더링 된 템플릿을 리턴하는 기본 웹 개발의 일반적인 경우를 나타냅니다. Django는 이런 매우 일반적인 경우를 위해 《제너릭 뷰》시스템이라는 지름길을 제공합니다.

  • 제너릭 뷰는 일반적인 패턴을 추상화하여 앱을 작성하기 위해 Python 코드를 작성하지 않아도됩니다. 그러기 위해 아래와 같은 단계를 밟읍시다!

  1. URLconf를 변환하십시오.
  2. 불필요한 오래된보기 중 일부를 삭제하십시오.
  3. Django의 제너릭 뷰를 기반으로 새로운 뷰를 도입하십시오.

URLconf 수정 - urls.py

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
  • 두 번째와 세 번째 패턴의 경로 문자열에서 일치하는 패턴들의 이름이 <question_id> 에서 <pk> 로 변경되었습니다.

Views 수정 - views.py

  • vote는 request에서 준 data기반으로 DBMS와 소통하여 실제 비즈니스 로직을 다루고 redirect하기 때문에 리팩토링 대상이 아니다.
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'

... # 하략
  • ListView와 DetailView의 두 가지 제너릭 뷰를 사용하고 있습니다. 이 두보기는 각각 《개체 목록 표시》 및 《특정 개체 유형에 대한 세부 정보 페이지 표시》 개념을 추상화합니다.

  • 각 제너릭 뷰는 어떤 모델이 적용될 것인지를 알아야합니다. 이것은 model 속성을 사용하여 제공됩니다.

  • DetailView 제너릭 뷰는 URL에서 캡처된 기본 키 값이 "pk"라고 기대하기 때문에 question_id를 제너릭 뷰를 위해 pk로 변경합니다.

  • 기본적으로 DetailView 제너릭 뷰는 <app name>/<model name>_detail.html 템플릿을 사용합니다. 우리의 경우에는 "polls/question_detail.html"템플릿을 사용할 것입니다. template_name 속성은 Django에게 자동 생성 된 기본 템플릿 이름 대신에 특정 템플릿 이름을 사용하도록 알려주기 위해 사용됩니다.

  • results리스트 뷰에 대해서 template_name을 지정합니다 - 결과 뷰와 상세 뷰가 렌더링 될 때 서로 다른 모습을 갖도록합니다. 이들이 둘다 동일한 DetailView를 사용하고 있더라도 말이지요.

  • 마찬가지로, ListView 제네릭 뷰는 <app name>/<model name>_list.html 템플릿을 기본으로 사용합니다; 이미 있는 "polls/index.html" 템플릿을 사용하기 위해 ListView 에 template_name 를 전달했습니다.

  • 제네릭 뷰에 대한 자세한 내용은 공식 문서를 참조

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

0개의 댓글