/ PROJECT, REFACTORING

중고 쇼핑몰 프로젝트 회고 및 리팩토링 후기


6월에 시작한 첫번째 개인 프로젝트였다. 꾸준히 기능적인 부분을 업데이트 시켜왔었지만 정작 짜왔던 코드를 스스로 리뷰해보는 시간을 가져보지 못했다. 이런 생각이 든 후 과거에 짜왔던 로직을 다시한번 보게 되었고 느낀점이 많았다. 😂😂

가장 먼저 가독성이 좋지 않음을 느끼게 되었다. 물론 2-3개월 지났지만 특정 기능의 로직을 파악하는데 시간이 꽤 걸렸다.
굳이 필요하지 않은 코드도 존재해 보였다. (코드가 전체적으로 이뻐보이지 않았다.. :-( )
무엇보다 필터링 기능에 있어 제품을 필터링 한 후 페이지 번호를 선택하면 랜더링이 되면서 필터된 제품이 아닌 모든 제품이 보여지는 것을 파악하게 되었다..(제대로 테스트를 해보지 않은 내 잘못이였다..)

리팩토링이 필요했던 또 다른 이유로 화면의 로딩이 1-2초 지연되는 답답한 상황이 있었다.

이러한 이유로 리팩토링을 진행하게 되었다.


  • 제품 필터링 GET 메소드 요청

리팩토링의 1차적인 목표는 가독성 이다. 코드의 가독성이 좋지 않으면 코드를 짠 나조차도 사실 코드 보기가 싫어진다.
2차적인 목표는 코드의 일관성 이다. 기존의 코드를 보면 어떤건 서비스 레이어 로직으로 작성하고 어떤건 view에 하드코딩을 했다. 한 사람이 짰지만 마치 여러사람이 짠 것처럼 느껴졌다.

초반에 필터링 된 기능을 구현할때 post 메소드를 이용했다. 하지만 제품 필터 기능은 GET방식이 좀 더 알맞은 것 같아 메소드를 GET으로 수정했다. SelectView의 view를 보면 정확히 어떤 기능을 하는지 알 수 없었다. 데이터는 dto를 사용해 데이터를 view에서 유저가 요청한 제품을 필터하는 로직을 따로 분리하고 싶었다.

리팩토링 전 코드보기
리팩토링 후 코드보기

초반의 제품을 필터링 하는 코드를 보면 view안에서 데이터를 get해오고 Q객체를 사용해 요청한 데이터를 기준으로 제품을 필터링 해오고 페이징 처리후 결과값을 반환해주었다. 데이터를 get해오는 로직과 제품을 필터링 하는 로직이 길다보니까 가독성이 좋지 못했다.

SelectView라는 class의 네이밍도 어떤 역할을 하는 기능인지 추축하기 어려웠다.

  • 요청받은 필터된 데이터 불러오기
class SelectView(View):
  def get(self, request, *args, **kwargs):
    categorys = ProductFilterService.find_by_all_category()
    category = self.request.GET.get('sub-menu-pk', None)
    price_from = self.request.GET.get('price-start',0)
    price_to = self.request.GET.get('price-end', 10000000)
    product_sort = self.request.GET.get('sort', None)
    price_from = int(price_from)
    price_to = int(price_to)
    articles = None
  • Q객체 이용해 필터된 제품 객체 필터하기
    q = Q()
    if category:
      q &= Q(category = category)
    q &= Q(price__range =(price_from, price_to))
    if product_sort:
      if product_sort == '1':
        articles = Article.objects.filter(q).order_by('-created_at')
      if product_sort == '2':
        articles = Article.objects.filter(q).order_by('created_at')
      if product_sort == '3':
        articles = Article.objects.filter(q).order_by('price')
      elif product_sort == '4':
        articles = Article.objects.filter(q).order_by('-price')
      elif product_sort == '5':
        articles = Article.objects.filter(q, is_deleted=False).annotate(like_count=Count('like__users')).order_by('-like_count','-created_at')
      elif product_sort == '6':
        articles = Article.objects.filter(q, is_deleted=False).annotate(review_count=Count('comment')).order_by('-review_count','-created_at')
    else:
      articles = Article.objects.filter(q).order_by('-created_at')
  • 필터된 제품 페이징 처리 한 후 값 반환하기
    page = request.GET.get('page', '1')
    articles, page_range = paginator(articles, page, 9)

    context = {'sort':product_sort,'pk':category,'start':price_from,'end':price_to, 'article_list':articles,'category_list':categorys,'is_page':False,'page_range':page_range}
    return render(request, 'article.html', context)



  • dto로 요청받는 데이터를 사전에 정하고 메소드로 데이터를 data라는 변수로 받게 처리했다
  • ProductFilterService 서비스로직의 get_filter_product_infor메소드에서 제품을 필터하는 로직을 따로 빼서 작성했다.
  • 해당 메소드를 거쳐 페이징 처리된 필터링 제품을 view의 context에서 받게된다
class FilterProductView(View):
    def get(self, request, *args, **kwargs):
        data = self._build_product_filter_dto(request)
        context = ProductFilterService.get_filter_product_infor(request, data)

        return render(request, 'article.html', context)

    def _build_product_filter_dto(self, request):
        return ProductFilterDto(
            category_pk = request.GET.get('sub-menu-pk', None),
            price_from = request.GET.get('price-start',0),
            price_to = request.GET.get('price-end', 10000000),
            product_sort = request.GET.get('sort', None)
        )



쿼리 최적화

제품이 렌더링 되어지는 화면의 로딩이 느린 문제가 발생했다. 로직에 문제가 있나 싶어 느려질 수 있는 로직 부분을 찾아보았으나 별 다른 문제가 없어보였다. 이를 해결하기 위해 django-debug-toolbar 를 설치하였고 이를 통해 페이지가 로딩되면서 발생하는 쿼리를 확인해 볼 수 있었다.

그 결과 3개의 데이터를 조회하는데 중복과 비슷한 쿼리가 굉장히 많이 발생하는 것을 볼 수 있었다.

marketsqltoolbar

중복되는 쿼리문을 줄이기 위해 select_related() , prefetch_related() 메소드를 활용하였다. 그 결과 10개의 쿼리문으로 줄일 수 있게 되었고 중복과 비슷한 쿼리문을 모두 제거할 수 있게 되었다. 카테고리별 화면도 대부분 메인 화면에서 발생하는 문제와 비슷하였고 똑같이 해결 할 수 있었다.

  • 장고 템플릿에서 이미지 한장을 화면에 렌더링 시키기 위해 first라는 구문을 사용했는데 첫번째 이미지를 찾기 위해 쿼리가 발생하는 듯 보였다. templatetag폴더에 get_one_image라는 파일을 만들어 photo = photo.all()[0].image 로 첫번째의 이미지를 가져왔다(view에서 prefetch_related(‘photo’)를 작성해 photo에 관련된 모델을 미리 로딩시켰기 때문에 다시 쿼리를 발생 시키지 않는다)

  • article 인스턴스별로 article과 N:M, 1:N 관계인 모델에 접근해야 했기 때문에 많은 중복 쿼리가 발생했지만 Article.objects.filter(is_deleted = False).prefetch_related('photo', 'comment').select_related('writer', 'address', 'category', 'article_price','like') 로 대부분의 중복 쿼리가 해결되었다. 미리 관련 모델을 로딩했기 때문에 추가로 돌아가는 쿼리가 없게 된다.

q = Q()

if dto.category_pk:
   q &= Q(category=dto.category_pk)
   q &= Q(price__range=(price_from, price_to))

 if dto.product_sort:
            articles_filter = Article.objects.filter(q).prefetch_related('photo', 'comment').select_related('writer', 'article_price', 'like' ,'address')
            
            if dto.product_sort == '1':
                articles = articles_filter.order_by('-created_at')
            elif dto.product_sort == '2':
                articles = articles_filter.order_by('created_at')
            elif dto.product_sort == '3':
                articles = articles_filter.order_by('price')
            elif dto.product_sort == '4':
                articles = articles_filter.order_by('-price')
            elif dto.product_sort == '5':
                articles = articles_filter.annotate(like_count=Count('like__users')).order_by('-like_count','-created_at')
            elif dto.product_sort == '6':
                articles = articles_filter.annotate(review_count=Count('comment')).order_by('-review_count','-created_at')

else:
    articles = articles_filter.order_by('-created_at')

그 결과 아래와 같이 쿼리를 중복해서 접근하는 경우가 사라졌고 쿼리 접근 횟수가 줄어든 것을 볼 수 있었다
gorf


Project Introduction

장고의 기본적인 CRUD와 다양한 기능을 구현해보기 위해서 중고 쇼핑몰 사이트를 선택했습니다. 실제 유저가 사용할 수 있는 완성도로 개발하기 위해 여전히 진행중에 있습니다.

초기 프로젝트 진행 인원 수 : 개인

개발 기간 : v1.0 2021.06.03 ~ 2021.06.17

후기 리팩토링 프로젝트 진행 : 개인

개발 기간 : v2.0 2021.08.05 ~ 2021.09.26
리팩토링 된 기능 : 서비스 레이어 로직으로 변경, 필터 기능 POST -> GET 메소드 변경, USER 상속

개발 기간 : v2.1 2021.10.11 ~ 2021.10.25
리팩토링 된 기능 : 여러이미지 S3에 업로드 기능

GitHub Respository
Github Repo