Django01 프로젝트 - 쿼리셋 수정으로 성능 개선하기(Prefetch, Selected Related)

|

문제

친구가 면접준비를 하면서 물었다. 웹 서비스를 하면서 병목이 많이 일어나는 곳이 어디가 있을까? 곰곰이 생각해보다가 답했다. “트래픽이 몰려서 네트워크에서 병목이 일어나지 않을까?” 그것도 정답이 될 수 있지만, DB 쿼리를 보내면서 많은 병목이 발생한다고 한다. 특히 나처럼 Django의 ORM을 이용하면 가장 쉽게 접하는 문제가 django ORM N+1이라고 한다.

먼저 ORM(Object Relation Mapper)는 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 것이다. 즉, 객체를 통해 DB의 데이터를 다룰 수 있는 것이다. 이것을 통해 DB를 사용할 줄 몰라도 우리는 DB를 제어하고 사용할 수 있던 것이다. 하지만 ORM은 Lazy-Loading 방식을 사용하는데, 우리가 명령을 실행하면 바로 데이터를 가져오는 것이 아니라 실제 데이터를 불러와야 할 때 쿼리를 실행하는 방식이다. 이렇게 하다보니 쿼리 1번으로 N건의 데이터를 가져왔는데, 여기서 또 원하는 데이터를 얻기 위해선 N건의 데이터 수 만큼 반복해서 2차적으로 쿼리를 수행하면서 성능 저하의 문제가 생기는 것이다.

이것을 개선하기 위해서 Eager-Loading방식을 사용하면 된다. 이 방식은 사전에 쓸 데이터를 포함하여 쿼리를 날리기 때문에 쿼리를 많이 줄일 수 있다. 이때 사용되는 메소드로 prefetch_relatedselected_related가 있다.

이 두개가 무엇일까

  • selected_related는 ForeignKey(1:N에서 N부분)와 OneToOneField 관계에서 활용할 수 있다. 이 메소드를 활용하면 DB단에서 INNER JOIN으로 쿼리한다.
  • prefetch_related는 ForeignKey(1:N에서 1부분)와 ManyToManyField에서 활용할 수 있다. 이 메소드는 각 관계 별로 DB쿼리를 수행하고, 파이썬 단에서 조인을 수행한다.

적용 전

로컬에서 debug_toolbar를 활성화한채 페이지를 왔다갔다 하면서 몇개의 쿼리가 오고가는지 한 번 봐보았다. 인덱스 페이지에서 가게와 메뉴, 좋아요의 테이블을 쿼리하는것인데 44개의 쿼리와 20개의 중복되는 부분이 있는 것이다. 많은 쿼리

적용 후

해당 view의 get_queryset메소드를 아래와 같이 변경했다.

def get_queryset(self):
    return Store.objects.prefetch_related('menu_set').prefetch_related('like_users').all()

쿼리 수정

아래와 같이 쿼리수는 줄은 것을 볼 수 있다! 하지만 아래를 보면 다른 중복 문제가 발생하여서 템플릿 파일의 코드를 살펴 보았다.


{% if store.menu_set.first.food_image %}
  {% thumbnail store.menu_set.first.food_image "370x250" crop="center" as im %}
    <img class="card-img-top" src="{{ im.url }}" alt="">
  {% endthumbnail %}
{% else %}
    <img class="card-img-top" src="{% static 'img/alt_image.png' %}" style="width:100%; height: 255px;">>
{% endif %}

위의 if의 조건문이 문제였는데, 가게 메뉴에서 첫 번째 레코드를 불러올려다 보니 다시 쿼리가 발생했던 것이다. 이 문제는 아래와 같이 forloop.first를 이용해서 해결해보았다. 이 부분은 데이터가 많이 없을 때는 문제가 없을거 같지만, 많아지면 서버쪽에서 부하가 올 수도 있으니 쿼리셋에 대해 더 이해해서 봐꿔봐야겠다. 어찌됐건 쿼리는 6개로 대폭 줄이는 경험을 할 수 있었다!


{% for menu in store.menu_set.all %}
  {% if forloop.first %}
    {% thumbnail menu.food_image "370x250" crop="center" as im %}
      {% if im.url %}
          <img class="card-img-top" src="{{ im.url }}" alt="">
      {% else %}
          <img class="card-img-top" src="{% static 'img/alt_image.png' %}" style="width:100%; height: 255px;">>
      {% endif %}
    {% endthumbnail %}
  {% endif %}
{% endfor %}

쿼리 수정

200713-200719_TIL

|

7월 15일 (수)

  • 자바스크립트를 계속 봐보고 있다. DOM의 구조와 천천히 이해해보고 있다.

7월 16일 (목)

  • mysqldump -uroot -p비밀번호 dongguk_food > ~/Desktop/dongguk_food.sql

7월 17일 (금)

  • 인생 첫 개발자 면접을 보았다. 실력이 미천하지만, 좋은 친구를 두어서인지 좋은 회사에 면접을 볼 수 있는 기회를 받았다. 3대1 면접으로 이력서를 토대로 질문을 해주셨다. 일부는 답했지만, 일부는 답을 못했다. 또한, 답했던 것도 지금 생각해보면 시원치 않았다. 이런건 항상 끝나고 나서 ‘아~ 이렇게 답해 볼 수 있었는데…’ 하는 아쉬움을 남기는 거 같다. 좀만 더 시간을 갖고 답해볼걸 그랬다. 그래도 5년만에 본 면접치고는 크게 긴장하지 않았고, 면접관 님들도 내 부족한 실력에 맞춰 계속 질문을 리드해주셨다. 끝나고 질문타임도 받았는데, 더 많이 질문해볼거 그랬다. 첫 면접이기에 한술에 배부를 수 없는 노릇인가 보다. 하지만, 이 첫 면접을 통해 나에게 많은 생각과 기회를 주었다 생각한다. 내가 아직 더 노력해야하는 것과 더 해봐야 할것이 많음을 알게 해주었다. 하지만 2차의 면접기회도 주셨으면 좋겠다ㅎㅎ. 내 자신이여 더 노력하자!

7월 19일 (일)

  • 간만에 일찍 자고 오래 잤다. 사용중인 노션을 정리해보았다.

달팽이 배열(Python)

|

달팽이 배열

달팽이 배열은 주어진 수(n)에 맞춰 nxn의 이중 배열을 생성하여서, 시계 방향으로 숫자가 늘어나는 배열이다. 이 시계 방향이 마치 달팽이 껍질 형태기 때문에 그리 부르는 듯 하다. 아래의 사진처럼 4x4 배열을 보면 더 이해가 갈 것이다. 달팽이 배열

규칙

n은 4로 주어졌다 가정하고, 규칙을 한 번 살펴보도록 하자.

  1. 0행 0열부터 시작해서 1열, 2열, 3열로 증가하는 것을 볼 수 있다. (n 만큼 열 증가)
  2. 이번엔 3열을 유지하고, 1행, 2행, 3행으로 증가하는 것을 볼 수 있다. 0행은 이미 채워졌으니 1행부터 채우는 것이다. (n-1만큼 행 증가)
  3. 3행 3열에서 다시 열 감소가 이루어져 3행 0열까지 돌아온다. (n-1만큼 열 감소)
  4. 3행 0열에서 다시 행 감소로 위로 올라간다. (n-2만큼 행 감소)

위의 과정들이 반복되어서 n이 0보다 작으면 종료되면 되는 것이다.

위의 규칙들을 살펴보면 처음 두 번은 열과 행이 증가하고, 두 번은 열과 행이 감소하는 것이다. 계속해서 열 -> 행 순서로 반복되는 것이다. 그리고 열일 때는 n인 상태고, 행일 때는 n-1형태로 n이 줄어드는 것을 볼 수 있다.

코드

코드를 통해 한 번 살펴보면 이해하기 좋을 것이다.

파이썬

def snail(n):
    arr = [[0 for j in range(n)] for i in range(n)] #1
    row = 0 #2
    col = -1 #2
    cnt = 1 #3
    trans = 1 #4
    while n > 0: #5
        for i in range(n): #6
            col += trans
            arr[row][col] = cnt
            cnt += 1
        n -= 1 #7
        for j in range(n): #8
            row += trans
            arr[row][col] = cnt
            cnt += 1
        trans *= -1 #9
    return arr


arr = snail(4)
for i in arr:
    for j in i:
        print('%5d' %j , end=' ')
    print()
  1. 먼저 이중 배열을 만들어 준다. 각 자리는 0으로 초기화한다.
  2. 인덱스에 접근할 row와 col을 초기화해준다. 이때, col을 -1로 초기화 해준 이유는 아래서 선언한 trans 변수를 통해 행열의 증가, 감소시킬건데, col을 0부터 작으면 index 초과 오류가 나기 때문이다.
  3. cnt=1로 초기화해 인덱스마다 증가시켜 값을 넣어준다.
  4. trans=1 을 통해 행열의 증가와 감소를 바꿔준다. 이때, 곱하기 -1을 해줘서 스위칭 해주면 된다.
  5. while문을 선언해서 n이 0보다 클 때까지만 반복시킨다.
  6. 먼저 열부터 제어해볼 것이다. for문에 col을 넣지 않은 이유는, col은 trans변수를 통해 증가, 감소할 것이기 때문이고, 위의 for문은 단순히 횟수를 반복시키기 위해 존재하는 것이다.
  7. 위의 규칙을 봤듯이, 열은 n, 행은 n-1형태이기에 n을 1 감소시킨다.
  8. 행을 제어하는 부분이다. 위의 열과 마찬가지로 trans로 증가 감소해주면 된다.
  9. 이 부분이 중요하다. 규칙을 보면, (열, 행) 증가 (열, 행) 감소 형태이다. 이것을 위해 trans를 곱하기 -1을 해주어서 스위칭을 해주는 것이다.

이 과정들을 거치고 나면 달팽이 배열이 예쁘게 표시될 것이다.

JavaScript

function snail(n){
  var array = Array.from(Array(n),()=> Array());
  var row = 0
  var col = -1
  var count = 1
  var trans = 1
  while (n>0){
    for(var i=0; i<n; i++){
      col+=trans;
      console.log("row:", row, "col:", col)
      array[row][col] = count;
      count++;
    }
    n--;
    for(var j=0; j<n; j++){
      row+=trans;
      console.log("row:", row, "col:", col)
      array[row][col] = count;
      count++;
    }
    trans *= -1
  }
  console.log(array);
}

snail(5)

느낀점

처음에 이 문제를 보았을 때, 어떤식으로 접근해야할지 감이 안왔다. 풀어왔던 이중배열 문제들은 첫 번째 행의 열에 대해 제어하는 그런 류의 문제(삼각형 문제)들만 풀어봤기 때문이다. 결국에는 계속 인터넷을 보고 풀었는데, 계속 잊어먹게 되는 것이다. 그러다 이번에는 필기도 해보면서 풀어보았고, 결국 풀 수 있었다. 예전에 참고했던것과 완전 판반이긴 했지만, 혼자 생각해보며 풀었다는 것에 큰 의의를 두고 싶다. 참 알고리즘이란 것은 어렵지만, 프로그래밍에 있어 사고력을 키워줄 수 있는 거 같다. 앞으로도 열심히 풀어봐야겠다.

Django01 프로젝트 - 패스워드 리셋하기

|

Django01 프로젝트 - 패스워드 리셋하기

패스워드 리셋 참고 auth

from django.contrib.auth import views
  7 from django.urls import path
  8
  9 urlpatterns = [
 10     path('login/', views.LoginView.as_view(), name='login'),
 11     path('logout/', views.LogoutView.as_view(), name='logout'),
 12
 13     path('password_change/', views.PasswordChangeView.as_view(), name='password_change'),
 14     path('password_change/done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
 15
 16     path('password_reset/', views.PasswordResetView.as_view(), name='password_reset'),
 17     path('password_reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
 18     path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
 19     path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
 20 ]

200706-200712_TIL

|

7월 6일(월)

  • 닉네임과 전화번호를 통해 아이디를 찾는 것을 구현해 보았다.
  • auth앱의 기본 기능인 password_reset을 사용해 보았다. 메일 인증을 위해 나의 지메일을 이용해서 실습해보았다.

7월 7일(화)

  • 프로젝트가 거의 다 완성되었다. 피드백도도 받고 하니 예전보다 많이 발전했음을 느꼈다.
  • pythonanywhere에 올려서 비밀번호 초기화를 시도했더니 지메일 보안에 의해 차단되는거 같다. 다음의 링크 참조해서 해결햇다. 참고링크

7월 8일(수)

  • 프로젝트를 해보면서 부족함을 느껴 CSS에 대해 다시 공부해보았다.
  • iterm으로 디렉토리들은 보는데, 자음모음이 분리되어 출력되고 있었다. 다음 링크로 해결했다. 자소분리 해결

7월 9일(목)

  • 코딩도장을 보면서 파이썬을 다시 이해해보면서 Django도 다시 이해해보는 중인데, 어렵다..
  • 블로그 디자인을 수정하려고 jekyll serve를 해서 수정중인데, 이 블로그가 어떻게 돌아가는지 구조부터 다시 이해해야겠다.

7월 10일(금)

  • 프로그래머스 문제들을 풀어보는데, 연습문제들을 풀면서도 아직 역량이 많이 부족함을 느낀다. 더욱 공부해야겠다.

7월 11일(토)

  • 제로초님의 javascript을 읽어보면서 이해중인데, 공부할게 많다…! 새로 장고를 해보면서 적용해보아야겠다.

7월 12일(일)

  • 인천 바다에 다녀왔다. 집에만 있다가 머리도 식히고 다시 의욕을 되찾기로 다짐했다. 요즘 새벽까지 하다가 늦게일어나는 경향이 생겼는데, 다시 돌릴 수 있도록 해야겠다.