170626-0702_TIL

|

7월 2일 (일)

  • 인스타그램 연습 프로젝트 배포를 진행했다. 배포 사이트
    • AWS와 Azure 중에서 고민했는데, 얼마전 참여했던 장고걸스 워크샵에서 배웠던 Azure를 활용하기로 했다.
    • 워크샵에서 실습할때는 수월하게 진행되었는데 개인적으로 연습할 때는 어려움이 많았다.
    • 여러번의 시행착오를 겪고 환경변수 설정, 의존성 패키지 관리 등을 위한 설정 파일 내용을 전부 수정한 이후에 배포를 할 수 있었다.
    • 아직은 발견하지 못한 소소한 버그들이 있겠지만 그래도 뿌듯하다. 공부하면서 진행하다보니 딱 2주 정도가 걸렸는데, 앞으로도 이렇게 만들면서 공부하는 방식으로 공부 해야겠다는 생각이 든다.

7월 1일 (토)

오늘 한 일

  • Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다. 연습내용
    • 주요 화면 마크업을 진행했다. (메인 post 리스트, 팔로우 페이지, 프로필, form 페이지)

6월 29일 (목)

오늘 한 일

  • Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다. 연습내용
    • Profile 모델에 picture, about, gender 필드를 추가하고, 회원정보 변경 기능을 구현했다.
    • 그동안 모르는건 찾아서 공부하면서 진행했는데 주요 기능 구현에만 일주일 정도가 걸렸다. 속도는 느리지만 배우고 느낀 점들이 많았다.
    • 드디어 마크업 작업을 시작했다. 일주일이 지나니 살짝 늘어지는 기분이 들었는데, 꾸미기 시작하니까 새로운 재미가 생긴다.

6월 28일 (수)

오늘 한 일

  • Django에서의 Static Files 관리에 대한 강의를 들었다.
    • StaticFile : 개발 리소스로서의 정적인 파일 (js, css, image etc)
    • Static Files Finders
      • App Directories Finder : “앱/static/” 경로를 검색
      • File System Finder : settings.STATICFILES_DIR = []의 경로를 추가
    • 코드의 재사용을 위해서 script 코드를 별도의 파일로 분리할 필요성이 있었다.
      그래서 공부한 대로 템플릿 내 script 코드를 external javascript로 분리했더니, (static 파일로 분리) 기존의 context variables를 사용하지 못하는 문제가 발생했다.(해당 script 코드는 view 에서 넘겨준 context variables를 활용)
      문제를 해결하기 위해서 script 코드를 포함하는 html 템플릿 파일을 만들고, 이를 원하는 템플릿 내에 include 하는 방식으로 해결했다. (더 좋은 방법이 있을 것도 같은데..) 관련코드
  • Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다. 연습내용
    • view, template 중에서 중복되는 코드를 제거했다. Django도 결국은 python으로 만들어져 있어서, 문법을 잘 활용하면 Django를 더 효율적으로 쓸 수 있는 것 같다. (그런 의미에서 알고리즘 문제 다시 풀어야지..)
    • 팔로우 기능을 구현했다.
    • 팔로우 기능을 Ajax를 활용하여 새로고침 없이 적용되도록 다시 구현했다. (Ajax를 알고나니 모든 기능을 Ajax로 구현하고 있다..)

6월 27일 (화)

오늘 한 일

  • Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다. 연습내용
    • 댓글 등록, 삭제 기능을 Ajax를 활용하여 구현하여 새로고침 없이 적용하였다.
    • 포스트 마다 최대 4개의 댓글을 보여주도록 설정하였다. 템플릿 필터 slice
    • Ajax를 활용하여 댓글 더보기 버튼을 누르면 나머지 댓글을 새로고침 없이 표시하도록 구현하였다.
  • 간단한 알고리즘 문제를 풀었다.

6월 26일 (월)

오늘 한 일

  • Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다. 연습내용
    • 댓글 등록, 삭제 기능을 구현하였다.
    • 댓글 등록 기능을 Ajax를 활용하여 다시 구현하였다. 페이지 새로고침 없이 바로 등록 가능하도록 수정했다.
  • blog 레이아웃을 변경하였다.
    • 최근 글 바로가기 항목 추가하고, 최근 10개 글을 메인화면에서 확인할 수 있도록 수정했다.

Django - Ajax / jQuery를 활용하여 새로고침 없이 좋아요 기능 구현하기

|

개인적인 연습 내용을 정리한 글입니다.
더 좋은 방법이 있거나, 잘못된 부분이 있으면 편하게 의견 주세요. :)

목차

들어가기

요즘 연습중인 인스타그램st 프로젝트 를 진행하며 좋아요 버튼을 눌렀을 때 새로고침 없이 좋아요 숫자가 증감하도록 구현하고 싶었다.

ezgif.com-video-to-gif

내가 원하는 것 (좋아요 숫자가 바로 변경된다)


찾아보니 jQuery와 ajax를 활용하면 페이지 새로고침 없이 서버와 데이터를 주고 받을 수 있다고 하여 적용해 보았다. ajax 통신은 페이스북, 인스타에서 사용하는 무제한 스크롤, 구글의 라이브 검색 등 널리 사용되고 있다. 이번 연습 과정을 기록하여, ajax 개념을 다시 정리하고 유사한 기능 구현시 활용하려고 한다.

좋아요를 구현한 2가지 방법의 전체적인 처리 프로세스를 그려보면 아래와 같다.
각 구현방법에 대해서는 아래에서 하나씩 다루어 보려고 한다.

New Mockup 1

새로고침이 필요한 방식 / Ajax를 활용한 방식


Ajax

위키에 따르면 AJAX는 Asynchronous Javascript and XML의 약자로, 말그대로 Javascript와 XML을 이용한 비동기적 정보 교환 기법이다. 이름에 XML이라고 명시되어있긴 하지만, JSON이나 일반 텍스트 파일과 같은 다른 데이터 오브젝트들도 사용 가능하다.

간단하게, Ajax는 전체 페이지를 새로 고치지 않고도 페이지의 일부만을 위한 데이터를 로드하는 기법 이라고 할 수 있다 jQuery는 Ajax 요청을 생성하고 서버로부터 전달 받은 데이터 처리를 쉽게 만들어 준다.

동기처리모델 (synchronouse processing model) 의 경우,
브라우저는 스크립트가 서버로부터 데이터를 수집하고 이를 처리한 후 페이지의 나머지 부분이 모두 로드될 때 까지 대기한다.

반면에 비동기 처리 모델 (asynchronouse processing model) 의 경우,
브라우저가 서버에 데이터를 요청하면, 작업이 완료되는 것을 기다리지 않고 나머지 페이지를 계속해서 로드하고 사용자와의 상호작용을 처리한다.

Ajax의 장점

  • 페이지 이동없이 고속으로 화면을 전환할 수 있다.
  • 서버 처리를 기다리지 않고, 비동기 요청이 가능하다.
  • 수신하는 데이터 양을 줄일 수 있고, 클라이언트에게 처리를 위임할 수도 있다.

기존의 방식으로 좋아요 기능구현 (새로고침 O)

기존 방식대로 구현하니 당연하게도 새로고침 후에 좋아요 처리정보가 반영되었다.

  • 좋아요 버튼에 url pattern을 연결
  • view로 좋아요 작업 처리
  • template에 context 데이터를 담아서 response
  • 해당 template을 랜더링하여 좋아요 처리정보 반영 (새로고침 필요)

이와 같은 처리방식을 그림으로 표현하면 아래와 같다.

views
페이지 새로고침이 필요한 좋아요 처리과정

좋아요 구현코드1 (새로고침 O)

  • 기존의 방식대로 구현한 전체 코드는 github에서 확인 가능하다.

Model

class Post(models.Model):
  # ...생략...
  like_user_set = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                        blank=True,
                                        related_name='like_user_set',
                                        through='Like')

  @property
  def like_count(self):
      return self.like_user_set.count()


class Like(models.Model):
  # ...생략...
  post = models.ForeignKey(Post)

urls

urlpatterns=[
  # ...생략...
  url(r'^(?P<pk>\d+)/like/$', views.post_like, name='post_like'),
]

view

@login_required
def post_like(request, pk):
  post = get_object_or_404(Post, pk=pk)
  # 중간자 모델 Like 를 사용하여, 현재 post와 request.user에 해당하는 Like 인스턴스를 가져온다.
  post_like, post_like_created = post.like_set.get_or_create(user=request.user)

  if not post_like_created:
    post_like.delete()

    return redirect('post:post_list')

Template


<!-- 생략 -->
<li>
  <a href="{% url "post:post_like" post.pk %}">좋아요 {{ post.like_count }}개</a>
  {% for like_user in post.like_user_set.all %}
    {{ like_user.profile.nickname }}
  {% endfor %}
</li>



Ajax/jQuery를 활용하여 인스타그램 좋아요 구현 (새로고침 X)

Ajax 를 통하여 전체 페이지를 새로 고치지 않고도 서버로 부터 좋아요 데이터만 로드하여 페이지에 적용할 수 있다. jQuery 는 Ajax 요청 생성 및 서버로부터의 데이터 처리를 쉽게 만들어 준다.

이와 같은 처리방식을 그림으로 표현하면 아래와 같다.

views
Ajax/jQuery를 활용한 좋아요 처리과정


좋아요 구현코드2 (새로고침 X)

  • Ajax/jQuery를 할용하여 작성한 전체 코드는 github에서 확인 가능하다.

Model

  • 상기 코드와 동일
class Post(models.Model):
  # ...생략...
  like_user_set = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                        blank=True,
                                        related_name='like_user_set',
                                        through='Like')

  @property
  def like_count(self):
      return self.like_user_set.count()


class Like(models.Model):
  # ...생략...
  post = models.ForeignKey(Post)

urls

urlpatterns=[
  # ...생략...
  url(r'^like/$', views.post_like, name='post_like'),
]

View

@login_required
@require_POST # 해당 뷰는 POST method 만 받는다.
def post_like(request):
    pk = request.POST.get('pk', None) # ajax 통신을 통해서 template에서 POST방식으로 전달
    post = get_object_or_404(Post, pk=pk)
    post_like, post_like_created = post.like_set.get_or_create(user=request.user)

    if not post_like_created:
        post_like.delete()
        message = "좋아요 취소"
    else:
        message = "좋아요"

    context = {'like_count': post.like_count,
               'message': message,
               'nickname': request.user.profile.nickname }

    return HttpResponse(json.dumps(context), content_type="application/json")
    # context를 json 타입으로

Template

  • jQuery 를 사용하여 Ajax 통신을 수행한다.

<!-- 생략 -->
<li>
  <input type="button" class="like" name="{{ post.id }}" value="Like">
  <p id="count-{{ post.id }}">{{ post.like_count }}개</p>
  <p id="like-user-{{post.id}}">
  {% for like_user in post.like_user_set.all %}
    {{ like_user.profile.nickname }}
  {% endfor %}
  </p>
</li>

<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript">
  $(".like").click(function(){
    var pk = $(this).attr('name')
    $.ajax({ // .like 버튼을 클릭하면 <새로고침> 없이 ajax 서버와 통신하겠다.
      type: "POST", // 데이터를 전송하는 방법을 지정
      url: "{% url 'post:post_like' %}", // 통신할 url을 지정
      data: {'pk': pk, 'csrfmiddlewaretoken': '{{ csrf_token }}'}, // 서버로 데이터 전송시 옵션
      dataType: "json", // 서버측에서 전송한 데이터를 어떤 형식의 데이터로서 해석할 것인가를 지정, 없으면 알아서 판단
      // 서버측에서 전송한 Response 데이터 형식 (json)
      // {'likes_count': post.like_count, 'message': message }
      success: function(response){ // 통신 성공시 - 동적으로 좋아요 갯수 변경, 유저 목록 변경
        alert(response.message);
        $("#count-"+pk).html(response.like_count+"개");
        var users = $("#like-user-"+pk).text();
        if(users.indexOf(response.nickname) != -1){
          $("#like-user-"+pk).text(users.replace(response.nickname, ""));
        }else{
          $("#like-user-"+pk).text(response.nickname+users);
        }
      },
      error: function(request, status, error){ // 통신 실패시 - 로그인 페이지 리다이렉트
        alert("로그인이 필요합니다.")
        window.location.replace("/accounts/login/")
        //  alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error);
      },
    });
  })
</script>

느낀점

  • 생각보다 많은 곳에서 Ajax가 활용되고 있다는 것을 알았다. (좋아요, 무한스크롤, 라이브검색, 계정 정보 표시 등)
  • 이걸 간단하게 만들어주는 jQuery가 새삼 더 고맙게 느껴진다.
  • 앞으로 Ajax를 잘 활용할 수 있도록 연습해야겠다.
  • (추가) 이 글을 작성한 이후에 Ajax를 활용하여 무한스크롤 기능, 댓글 추가, 팔로우 기능을 구현해보았다. 알고나니 계속 쓰게 된다. (이왕이면 새로고침 없는게 좋으니..!)코드상세

reference

Django 사용자 정의 필터 (Custom Template Filter)를 활용하여 인스타그램 해시태그 링크 구현하기

|

개인적인 연습 내용을 정리한 글입니다.
더 좋은 방법이 있거나, 잘못된 부분이 있으면 편하게 의견 주세요. :)

장고 Custom Template Filter 활용하기

목차

들어가기

요즘 연습중인 인스타그램st 프로젝트 를 진행하며
실제 인스타그램 처럼 본문의 해시태그 문자열을 링크 처리하고,
해당 링크를 클릭하면 태그 내용이 포함된 모든 post list를 검색 결과로 보여주는 기능을 구현하고 싶었다.

스크린샷 2017-06-22 오후 10.38.36

내가 원하는 것


여러가지 방법을 고민하고 시도해보았는데, 생각도 못했던 오류들이 다양하게 발생했다.
(편집 화면에서 html tag가 그대로 노출되는 등)
그냥 다른 기능구현으로 넘어갈까 싶었지만 꼭 필요한 기능이라고 생각해서 열심히 찾아보았다.
결과적으로 custom template filter를 활용하여 DB 내용에 직접적인 영향을 미치지 않고, 깔끔하게 해결할 수 있었다!

Model

  • Post와 Tag 모델은 ManyToMany 관계를 갖고 있다.
  • content에 입력된 문자열 중에서 해시태그 형태 (#태그명)를 가진 문자열을 따로 추출하여 Tag 모델에 저장하도록 구현하였다.
  • 자세한 코드는 github 에서 확인 가능하다.
class Post(models.Model):
  #...생략...
  content = models.CharField(max_length=140, help_text="최대 140자 입력 가능")
  tag_set = models.ManyToManyField('Tag', blank=True)


class Tag(models.Model):
    name = models.CharField(max_length=140, unique=True)

view

  • queryset을 활용하여 Post 모델의 모든 인스턴스를 가져온다. (post_list)
  • 이를 contenxt 객체로 템플릿에 넘겨준다.
def post_list(request):
    post_list = Post.objects.prefetch_related('tag_set').select_related('author__profile').all()

    #...생략...

    return render(request, 'post/post_list.html', {
        'post_list': post_list,
    })

template

  • 현재는 post.content의 문자열이 그대로 출력되고 있는 상태
  • post.content 내부의 문자열에서 특정 문자열을 추출하여 원하는대로 수정하려면 ‘custom filter’가 필요하다.

<!-- ...생략... -->
{% for post in post_list %}
<ul>
  <li>{{ post.author.profile.nickname }}</li>
  <li><img src="{{ post.photo.url }}" alt="{{ post.author }}'s photo"></li>
  <li>{{ post.content }}</li>
  <!-- post.content 내부의 문자열에서 특정 문자열을 추출하여 원하는대로 수정하려면 'custom filter'가 필요하다-->
</ul>
{% endfor %}



custom template filter 적용하기

Django 공식문서 를 찾아보면, 사용자 정의 템플릿 필터를 만드는 과정은 크게 3가지로 나뉜다.

  1. 장고 프로젝트 app 내에 templatetags 폴더 (패키지) 만들기
  2. 사용자 정의 템플릿 필터 (모듈) 작성하기
  3. template 내에 해당 모듈을 load 하고, 원하는 field에 필터 적용하기

1. 장고 프로젝트 app 내에 templatetags 폴더 (패키지) 만들기

  • 일반적으로 custom template tags, filters 를 정의하는 곳이 Django app 디렉토리 안이다.
  • 작성하려는 사용자 정의 필터가 진행중인 프로젝트 앱과 연관성이 있다면, 해당 앱 디렉토리 하단에 templatetags 폴더를 추가한다.
    (modles.py, views.py 와 동일한 level에 추가한다.)
  • 폴더 작성 후 해당 폴더 내에 __init__.py 라는 이름의 빈 파일을 추가한다. (내용은 없어도 괜찮다.)
  • 이 파일의 역할은 해당 폴더가 파이썬 패키지 라는 것을 명시하는 것이다.
  • templatetags 폴더(패키지)을 추가하고 나서는 터미널로 돌아가 서버를 재시작 해야 정상적으로 적용된다.
# 폴더구조 예시
post/
    __init__.py
    models.py
    templatetags/
        __init__.py
        post_extras.py # 앞으로 작성할 사용자 정의 템플릿 필터 파일예시
    views.py

2. 사용자 정의 템플릿 필터 (모듈) 작성하기

  • templatetags 폴더 아래에 원하는 이름으로 .py 파일을 추가한다.
  • 여기서는 post_extras.py 라는 이름을 사용하였다. 해당 파일에는 사용자 정의 필터 함수를 작성할 것이다.
  • 우선은 상단에 아래와 같은 코드를 추가한다. register는 유효한 tag library를 만들기 위한 모듈 레벨의 인스턴스 객체이다.
# post_extras.py
from django import template

register = template.Library()
  • 그리고 add_link 라는 이름의 함수를 정의한다. (이것이 바로 template에서 사용할 사용자 정의 필터의 이름이다.)
  • 해당 필터 함수는 post|add_link 와 같이 활용될 수 있다. 이때 post 객체는 add_link 함수의 파라미터로 전달된다.
# post_extras.py
@register.filter
def add_link(value):
    content = value.content # 전달된 value 객체의 content 멤버변수를 가져온다.
    tags = value.tag_set.all() # 전달된 value 객체의 tag_set 전체를 가져오는 queryset을 리턴한다.

    # tags의 각각의 인스턴를(tag)를 순회하며, content 내에서 해당 문자열을 => 링크를 포함한 문자열로 replace 한다.
    for tag in tags:
        content = re.sub(r'\#'+tag.name+r'\b', '<a href="/post/explore/tags/'+tag.name+'">#'+tag.name+'</a>', content)
    return content # 원하는 문자열로 치환이 완료된 content를 리턴한다.

3. template 내에 해당 모듈을 load 하고, 원하는 field에 필터 적용하기

  • 우선 템플릿 상단에 사용할 모듈을 load한다.
  • 여기서는 사용자 정의 필터가 포함된 모듈인 post_extras.py 를 load 한다.

<!-- post_list.html -->
{% load post_extras %}  <!-- custom filter 추가 -->

  • post|add_link 와 같이, 원하는 부분에 사용자 정의 필드인 add_link 를 적용할 수 있다.
  • 여기서는 post 인스턴스 객체를 add_link 함수의 파라미터로 받아서 정의된 작업을 진행한 이후에 content를 리턴한다.
<!-- post_list.html -->

{% for post in post_list %}
<ul>
  <!-- ...생략... -->
  <li>{{ post.author.profile.nickname }}</li>
  <li><img src="{{ post.photo.url }}" alt="{{ post.author }}'s photo"></li>
  <li>{{ post|add_link|safe }}</li>
  <!-- post 인스턴스 객체에 'add_link' 사용자정의 필터를 적용하였다 -->
  <!-- 참고) 내장 필터인 safe 필터를 사용하여, tag escape를 방지할 수 있다. -->
</ul>
{% endfor %}

결과물

  • 사용자 정의 템플릿 필터를 사용하면 템플릿 랜더링 시에 원하는 대로 DB를 조작할 수 있다. (줄바꿈 추가하기, 필터링, html escape 처리 등)
  • 좋은 점은 DB의 원본 데이터에는 영향을 미치지 않는다는 점이다.
  • css 작업 전이라 아직 수수하지만 결과물은 아래와 같다. 해시태그 링크를 클릭하면 해당 해시태그를 가진 모든 post list가 화면에 출력 되도록 구현하였다. (이 부분에 대한 코드는 github 에서 확인 가능하다.)


스크린샷 2017-06-23 오전 12.47.21

아직 아름답지는 않지만.. 원하는건 구현했다!


결론, 느낀점

  • 처음에는 다른 방법으로 고민을 많이해서 문제 해결에 시간이 많이 걸렸다.
  • Post 모델에 메소드를 구현하고, 직접 content 필드 내의 문자열을 수정했더니 DB 자체에 a 태그가 포함되는 문제가 발생했다.
  • 이걸 포기하지 않고 추가적으로 발생하는 문제를 계속 해결하려다 보니까 점점 시간만 흐르고 구렁텅이에 빠지는 기분이 들었다.
  • 너무 하나를 고집하기 보다, 다른 방법을 생각해보는 자세도 필요한 것 같다.
  • 혼자서 조금이라도 고민하고 나서 검색하면 해결책을 찾을 가능성이 높아진다.
  • 다른 일을 하고 있을 때 (ex. 식사중), 의외로 좋은 아이디어가 떠오르는 경우가 있다.

django 쿼리셋 수정을 통한 웹서비스 성능 개선 - select_related, prefetch_related

|

개인적인 연습 내용을 정리한 글입니다.
더 좋은 방법이 있거나, 잘못된 부분이 있으면 편하게 의견 주세요. :)

쿼리셋 수정을 통한 웹서비스 성능 개선

들어가기

웹서비스에 있어서 데이터페이스는 성능에 많은 영향을 미친다.
절대적으로 SQL 갯수를 줄이고, 각 SQL의 성능 및 처리속도 최적화가 필요하다.

리스트 조회 페이지를 만들때 Post.objects.all() 과 같은 queryset을 자주 활용했었다. 이번에 인스타그램st 프로젝트를 진행하며 데이터 조회시 몇개의 SQL 쿼리가 발생할까? 중복은 없을까? 궁금해졌다.
django-debug-toolbar 를 활용해서 페이지 로딩시 발생하는 쿼리를 확인해보았는데, 결과가 충격적이었다.
고작 글 9개를 조회하고 화면에 출력하는데, 28개의 쿼리문이 발생하고 그 중에 26개는 중복이었다. (부들부들..)

이를 해결하기 위해서 다음과 같은 메소드를 활용하였다.

  • select_related()
    • ForeignKey, OneToOneField 관계에서 활용
    • ForeignKey/OneToOneField 관계에서 Lazy하게 쿼리하지 않고, DB단에서 INNER JOIN 으로 쿼리할 수 있다.
  • prefetch_related()
    • ManyToManyField, ForeignKey의 reverse relation 에서 활용
    • 각 관계 별로 DB 쿼리를 수행하고, 파이썬 단에서 조인을 수행한다.

수정 결과는 대만족! 28개의 쿼리문이 5개로 줄어들고 중복은 모두 삭제되었다.
아래는 내가 겪었던 문제와, 이를 해결하는 과정을 정리한 내용이다.


models

  • 각 모델은 다양한 필드 (1:N 관계, 1:1 관계, M:N 관계) 갖고 있다.
# models.py
class Post(models.Model):
    #...생략...
    author = models.ForeignKey(settings.AUTH_USER_MODEL) # auth.User
    tag_set = models.ManyToManyField('Tag', blank=True)


class Tag(models.Model):
    # Tag:Post = M:N
    name = models.CharField(max_length=140, unique=True)


class User(AbstractUser):
    #...생략...
    # User:Post = 1:N
    # User:Profile = 1:1


class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    nickname = models.CharField(max_length=30, unique=True)

views

수정 전

  • 평소처럼 Post.objects.all() 를 활용하여 post_list 를 가져오고 이를 contenxt 객체로 template에 전달했다.
# views.py

def post_list(request):
    post_list = Post.objects.all()

    return render(request, 'post/post_list.html', {
        'post_list': post_list,
    })

template

  • 전달한 post_list는 다음과 같이 템플릿에서 활용하였다.
  • django-debug-toolbar 를 활용해서 페이지 로딩시 발생하는 쿼리를 확인하니, Post 모델에서 User 모델/Tag 모델로 접근하면서 중복되는 DB 작업이 발생하는 것을 확인할 수 있었다.

{% for post in post_list %}
<!-- post_list가 10개라고 가정하면 -->
<ul>
  <li>{{ post.author.profile.nickname }}</li>
  <!-- 1. post 인스턴스별로 post와 N:1 관계인 author 모델에 접근 -->
  <!-- 2. author 모델과 1:1 관계인 profile 모델에 접근하여 중복 작업이 발생한다 -->
  <!-- 총 20번의 중복 발생 -->
  <li>{{ post.content }}</li>
  <li>{% for tag in post.tag_set.all %} {{ tag.name }} {% endfor %}</li>
  <!-- 3. post 인스턴스별로 post와 N:M 관계인 tag 모델에 접근 -->
  <!-- 추가적으로 10번의 중복 발생 -->
</ul>
{% endfor %}

문제상황

1

post 갯수가 늘어날수록 중복도 어마어마하게 늘어난다

쿼리셋 수정 및 성능 개선과정

수정 1

  • Post.objects.select_related(‘author’).all() 사용
  • 28개-> 20개로 쿼리문으로 줄어들었다 (17개 중복)
  • (참고) post 인스턴스 갯수 : 9개
# views.py

def post_list(request):
    post_list = Post.objects.select_related('author').all()
    # 첫 DB 쿼리시 author record까지 로딩

    return render(request, 'post/post_list.html', {
        'post_list': post_list,
    })

{% for post in post_list %}
<!-- post_list가 10개라고 가정하면 -->
<ul>
  <li>{{ post.author.profile.nickname }}</li>
  <!-- 1. (개선 전) post 인스턴스별로 post와 N:1 관계인 author 모델에 접근 -->
  <!-- 1. (개선 후) 첫 DB 쿼리시에 author record까지 로딩했기 때문에 추가 DB 없음 -->
  <!-- 2. author 모델과 1:1 관계인 profile 모델에 접근하여 중복 작업이 발생한다 -->
  <!-- 총 10번의 중복 발생 -->
  <li>{{ post.content }}</li>
  <li>{% for tag in post.tag_set.all %} {{ tag.name }} {% endfor %}</li>
  <!-- 3. post 인스턴스별로 post와 N:M 관계인 tag 모델에 접근 -->
  <!-- 추가적으로 10번의 중복 발생 -->
</ul>
{% endfor %}

2

첫 DB 쿼리시 .select_related('author')를 통해 author record를 함께 로딩한다.

수정 2

  • Post.objects.prefetch_related(‘tag_set’).select_related(‘author’).all()사용
  • 28->20->13개로 쿼리문으로 줄어들었다 (9개 중복)
  • (참고) post 인스턴스 갯수 : 9개
# views.py

def post_list(request):
    post_list = Post.objects.prefetch_related('tag_set').select_related('author').all()
    # 첫 DB 쿼리시 tag_set, author record 까지 로딩

    return render(request, 'post/post_list.html', {
        'post_list': post_list,
    })

{% for post in post_list %}
<!-- post_list가 10개라고 가정하면 -->
<ul>
  <li>{{ post.author.profile.nickname }}</li>
  <!-- 1. (개선 전) post 인스턴스별로 post와 N:1 관계인 author 모델에 접근 -->
  <!-- 1. (개선 후) 첫 DB 쿼리시에 author record까지 로딩했기 때문에 추가 DB 없음 -->
  <!-- 2. author 모델과 1:1 관계인 profile 모델에 접근하여 중복 작업이 발생한다 -->
  <!-- 총 10번의 중복 발생 -->
  <li>{{ post.content }}</li>
  <li>{% for tag in post.tag_set.all %} {{ tag.name }} {% endfor %}</li>
  <!-- 3. (개선 전) post 인스턴스별로 post와 N:M 관계인 tag 모델에 접근 -->
  <!-- 3. (개선 후) 첫 DB 쿼리시에 tag_set record까지 로딩했기 때문에 추가 DB 없음 -->
</ul>
{% endfor %}

3

초기 DB 쿼리시 tag_set, author record를 함께 로딩한다.

수정 3

  • Post.objects.prefetch_related(‘tag_set’).select_related(‘author__profile’).all() 사용
  • 28->20->13->5개로 쿼리문으로 줄어들었다 (중복없음)
  • (참고) post 인스턴스 갯수 : 9개

4

초기 DB 쿼리시 tag_set, author, author과 1:1 관계인 profile record를 함께 로딩한다.
def post_list(request):
    post_list = Post.objects.prefetch_related('tag_set').select_related('author__profile').all()
    # 첫 DB 쿼리시 tag_set, author, author과 1:1 관계인 profile record까지 로딩

    return render(request, 'post/post_list.html', {
        'post_list': post_list,
    })

{% for post in post_list %}
<!-- post_list가 10개라고 가정하면 -->
<ul>
  <li>{{ post.author.profile.nickname }}</li>
  <!-- 1. (개선 전) post 인스턴스별로 post와 N:1 관계인 author 모델에 접근 -->
  <!-- 1. (개선 후) 첫 DB 쿼리시에 author record까지 로딩했기 때문에 추가 DB 없음 -->
  <!-- 2. (개선 전) author 모델과 1:1 관계인 profile 모델에 접근하여 중복 작업이 발생한다 -->
  <!-- 2. (개선 후) 첫 DB 쿼리시에 author 과 1:1 관계인 profile record 로딩했기 때문에 추가 DB 없음-->
  <li>{{ post.content }}</li>
  <li>{% for tag in post.tag_set.all %} {{ tag.name }} {% endfor %}</li>
  <!-- 3. (개선 전) post 인스턴스별로 post와 N:M 관계인 tag 모델에 접근 -->
  <!-- 3. (개선 후)  첫 DB 쿼리시에 tag_set record까지 로딩했기 때문에 추가 DB 없음 -->
</ul>
{% endfor %}

결론

그동안 너무 당연하게 모델.objects.all()를 사용하여 전체 DB를 조회하고 활용했었다.
이번 기회를 통해서 상황에 맞는 queryset을 사용하지 않으면 엄청난 중복이 발생하고, 이는 속도저하로 연결된다는 것을 알게되었다.
사실 AskDjango에서 관련된 강의를 들었을 때는 ‘아 이런게 있구나’ 하고 넘어갔던 내용이다. 역시 필요성이 생기니 정보에 대해 접근하는 태도가 달라지는 것 같다.
그리고 덤으로 django-debug-toolbar 활용 방법을 찾은 것 같다. 이렇게 유용할 줄이야!

reference

170619-0625_TIL (다시 시작)

|

6월 25일 (일)

오늘 한 일


6월 24일 (토)

오늘 한 일

  • Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다.
    • Ajax를 활용하여 새로고침 없이 좋아요 정보가 업데이트 되도록 구현하였다.
    • 과거에 남겨놓은 메모가 많은 도움이 되었다.
  • 장고걸스 워크샵 둘째 날 행사에 참여하였다.
    • 같은 조의 Jay Jin 님께 여러가지 꿀팁을 얻었다.
    • Azure 를 사용하여 배포를 진행해보았다. AWS 보다 간단하다는 인상을 받았다.
    • 내년에도 계속 장고를 쓰고 있었으면 좋겠다. 그리고 장고걸스 행사에는 코치로 참여할 수 있었으면 좋겠다.

6월 23일 (금)

오늘 한 일

  • Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다.
    • 좋아요 기능을 구현하였다. (ManyToManyField 활용)
    • select_related, prefetch_related 를 활용하여 좋아요 정보를 가져오는 부분의 중복 쿼리를 제거하였다.
  • 장고걸스 워크샵 첫째 날 행사에 참여하였다.

6월 22일 (목)

오늘 한 일

  • Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다.
    • Tag 검색 기능을 추가했다.
    • 검색기능은 post의 tag_set 필드에서 name 값으로 특정 입력 값을 가진 post list만 필터링하여 구현하였다.
    • Queryset 메소드 중에 icontains 를 활용하여 부분일치 문자열도 쉽게 필터링이 가능했다. 그 외에 다른 유용한 Queryset API 에 대해서도 읽어보았다.
    • content 내 태그 문자열만 추출하여 검색 링크를 추가하였다.
    • 원하는 기능을 구현하기 위해서 여러가지 고민과 시도를 해보았다. 생각하지 못했던 문제점들이 다양하게 발생하여 기능 구현에 시간이 많이 걸렸다.
    • 결과적으로 custom filter를 활용하여 DB 내용에 직접적인 영향을 미치지 않고, 검색 링크가 추가된 상태로 템플릿이 랜더링 되도록 구현할 수 있었다.
    • 문제 해결과정 을 블로그에 정리했다.

6월 20일 (화)

오늘 한 일

  • Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다.
    • messages.framework 를 활용하여 post 등록/수정/삭제시 유저 안내문구를 alert로 구현하였다.
      이전에 messages.framework 를 몰랐을 때는 안내 메시지를 javascript로 하나하나 구현했었다.
      강의에서 messages.framework를 접했을 때는 ‘아 이런게 있구나’ 정도로 끝났다.
      필요성이 발생해서 사용하는 지금 messages.framework의 편리함이 드디어 실감으로 다가온다.
      필요한 부분에 코드중복 없이 일괄 적용할 수 있다니..너무나 편하다.
    • select_related, prefetch_related를 활용해서 post list를 출력하는 페이지의 SQL 쿼리갯수를 28개에서 5개로 줄였다! 덕분에 소요 시간이 3.81ms에서 1.51ms로 줄어들어 성능을 개선할 수 있었다. 정말 유용한 것 같아서 블로그에 문제 해결과정을 정리해두었다.

6월 19일 (월)

오늘 한 일

  • 생각도 못했던 시기에, 생각도 못했던 곳에서 면접 기회가 주어졌다.
    Java와 Spring을 사용하는 곳이지만, 배울 수 있는 사람들이 많고 동기부여가 되는 업무환경이라고 생각했다.
    1번의 필기시험과 2번의 기술면접을 진행했다. 처음 경험하는 개발자 채용 과정은 하나하나가 놀랍고 재미있었다. (맙소사 필기시험을 통과하다니!)

    그렇게 최근 3주정도 집중해서 준비했던 일이 마무리 되었다.
    바라던 결과는 얻지 못했지만 Java를 학습하며 새롭게 알게 된 점도 많았고, 자료구조 알고리즘도 다시 한번 정리할 수 있었다. 동기부여가 컸기에 그만큼 효율도 높았던 것 같다.
    대신 3주만에 만난 django는 낯설어서 조금 충격이었다. 그렇게 열심히 했는데 이렇게 금방 낯설어 지다니!
    잠시 한눈을 팔았으니 이제 애정 2배를 갖고 열심히 해야겠다 :)

  • Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다.
    • 이번 연습의 목표는 아래와 같다.
    • 1) 능동적으로 고민한다. (그동안 수동적으로 강의를 듣고, 같은 것을 반복 연습하는 부분에 집중했었다.)
    • 2) 필요한건 공식 문서, stackoverflow를 찾아보고 문제를 해결한다.
    • 3) 궁금한 코드는 Django 소스코드 를 열어서 직접 읽어보자.
    • 4) 가능한 스스로 코드를 짜본다. 비효율적이라도 직접 해보고, 다른 사람의 코드를 참고한다.
  • 오늘 구현한 기능
    • post 생성/수정/삭제 구현
    • django-imagekit를 활용하여 유저가 업로드한 이미지를 수정
    • post모델의 content 필드에서 #해시태그 부분을 정규표현식으로 추출
    • 추출한 태그는 Tag 모델에 별도로 저장

이고잉님의 적정수준의 공부를 재미있게 읽은 적이 있다. 크게 공감을 하면서도 내가 갖고 있는 것이 너무 적어보여서 계속 채우는 부분에만 집중 했던 것 같다. (그리고 수동적으로 강의를 듣는게 편하기도 하다.)

적정수준의 공부방법

  1. 프로그램을 만드는데 필요한 최소한의 도구를 익힌다. (문자, 숫자, 변수, 비교, 조건문, 반복문, 함수)
  2. 최소한의 도구로 다양한 문제를 해결해 본다.
  3. 가지고 있는 도구로 문제를 해결하는 것이 점점 어려운 일이 되었을 때 선배 개발자들의 성취를 찾아본다. 이 또한 최소한으로.
  4. 2번과 3번 반복

의식적으로 불편해지려는 시도를 해야겠다는 생각이 든다. 아는게 적은데 그 안에서 해결하려고 고민하는건 힘든 일이다. 그러다 다른 사람의 간단한 해결책을 보면 신기하기도 하면서 자괴감이 들기도 하고.. 그래서 요즘에는 문제를 만나면 ‘이걸 분명히 쉽게 푸는 방법이 있을텐데’ 하면서 다른 사람의 쉬운 해결책을 바로 찾아보려는 생각이 먼저 든다. 이게 좋은건지 나쁜건지 잘 모르겠는데, 고민을 적게 하는건 분명 좋지 않을 것 같다.

적절한 양만큼 고민하고 찾아보는 것, 적절한 양만큼 공부하고 만들어 보는 것 그걸 잘 할 수 있었으면 좋겠다.