Django 필터링 refactoring

김주현·2021년 12월 8일
0

[Django]

목록 보기
7/8

장고에서 필터링 하는 방법은 총 3가지가 있다.

  • if문을 이용하는 방법
  • 딕셔너리를 이용하는 방법
  • Q객체를 이용하는 방법

아워플레이스 홈페이지를 클론 프로젝트 주제로 삼아 진행중인데, 필터링 구현에 있어 처음 if문으로 적용하는 것에는 어려움이 없었으나 점점 가독성을 위해 코드를 발전시키면서 여러 난관들을 겪었다.

내가 맡은 기능은 상품필터링, 상품검색, 상품정렬 3가지이다.
패스파라미터가 아닌 쿼리파라미터로 하나의 뷰안에서 기능구현을 하였고, 가독성을 높이기 위해 리펙토링 단계에 있어 최종단계에는 dictionary comprehension을 적용해보았다.

여기서 쿼리파라미터로 정한 이유는?
: 패스파라미터를 사용하면 주소창에 어디어디로 이동하는지 한눈에 보여서 보기 편하지만 인자로 확실한 구분이 가능한 경우( 특정 상품의 정보 등 ) 에 주로 사용하기 때문에, 특정 상품의 필터링 및 검색과 같이 여러개의 조건이 결합될 때는 쿼리파라미터를 사용해야한다.

아워플레이스 홈페이지의 대대분류(모든카테고리, 둘러보기,...)는 모양만 나오도록 하기로 하였고, 대분류(가정집, 스튜디오 ...) 및 중분류(아파트, 주택, ...)를 DB에 저장된 데이터를 요청에 따라 보내주기로 하였다.

여기서 대분류를 menu, 중분류를 category, 검색은 keyword, 상품정렬은 order로 구분하였다.

리펙토링 수정 전

1단계 : 상품 필터링을 위한 if문 사용

딕셔너리형태로 데이터를 변수에 담아주기 위해 request.GET.get 사용하였고, 다중 if문으로 조건 하나를 만족할때마다 if문을 추가해주었다.

하지만 if문이 많아지면 가독성 뿐만 아니라 로직이 갈라지게 된다.
로직이 갈라지면 갈라질수록 나중에 해당 함수에 버그가 발생하여 디버깅할때 여러 경우를 따져봐야 하기 때문에 골치가 아파진다.

2단계 : dictionary comprehension 적용

if문을 사용하는 것보다 key-value의 형태를 활용하는 것이 보다 효율적이라고 하여, dictionary comprehension으로 적용해보았다.

하지만 이 방식의 단점은 하나만 들어와도 list에 담긴다는 것인데,
value에서 튜플형식의 데이터를 리스트형태로 담아주기 때문에 __ in을 사용하여 데이터를 읽어와야 한다.

3단계 : dictionary comprehension 두가지 조건 적용

import json
from json.encoder import JSONEncoder

from django.http.response import JsonResponse
from django.views         import View

from places.models        import Place

class PlaceListView(View):    
    def get(self, request):
        try: 
            order_keyword = request.GET.get('keyword',None) # :8000/place?keyword=price
            
            order_prefixes = {
                'price' : 'price', # 가격낮은순
                'recent' : '-created_at' # 최신순
            }                      
            
            order_set = {order_prefixes.get(key) : value[0] for (key, value) in dict(request.GET).items()}
        
            

            filter_prefixes = {
                'menu'     : 'category__menu_id', 
                'category' : 'category_id',
                'search'   : 'name__contains',
            }
            
            #value는 리스트로 담겨져나오기 때문에 리스트의 값을 읽기 위해 [0]을 설정한 것
            #dic(~)부분부터 찍어보면서 이해하자
            
            filter_set = {filter_prefixes.get(key) : value[0] for (key, value) in dict(request.GET).items()}
            
            if not order_keyword :
            #     places = Place.objects.filter(**filter_set)
                
            # places = Place.objects.all().order_by(order_prefixes[order_keyword])
        
            if order_keyword :
                places = Place.objects.all().order_by(order_prefixes[order_keyword])
            
            places = Place.objects.filter(**filter_set)
        
            result = [{
                'id'        : place.id, 
                'place_name': place.name,
                'category'  : place.category.name,
                'price'     : place.price,
                'capacity'  : place.capacity,
                'city'      : place.city.name,
                'parking'   : place.parking,
                'url'       : [image.url for image in place.image_set.all()],
                } for place in places] 
            
            return JsonResponse({'result' : result}, status=200)
        
        except Place.DoesNotExist:
            return JsonResponse({'message' : 'DOES_NOT_FOUND'}, status=400)
        except TypeError:
            return JsonResponse({'message' : 'TYPE_ERROR'}, status=400)
        
        except ValueError:
            return JsonResponse({'message' : 'VALUE_ERROR'}, status=400)

상품정렬기능을 추가하기 위해 prefixes를 두가지의 경우로 나누었다.

order_prefixes에 price(가격 낮은순), recent(최신순)의 키값을 담아주었고,
filter_prefixes에는 menu(메뉴별 상품목록), category(메뉴의 카테고리별 상품목록), search(상품 검색)의 키값을 담아주었다.

문제는 각자의 딕셔너리에 있는 기능이 조건이 성립할때마다 동작하도록 설정해주어야 하는건데, 멘토분께 조언을 구한결과 몇없는 조건을 위해 prefix를 적용하기엔 코드복잡성이 올라갈 수도 있다는 말씀을 남겨주셨다.

결국 시간적인 문제와 코드복잡성으로 인해 추가 구현예정으로 미루기로 하고, 이전 로직으로 돌아가기로 결정하였다.

리펙토링 수정후

Query parameter Q 객체 사용

class PlaceListView(View):    
    def get(self, request):
        try: 
            menu     = request.GET.get('menu', None)
            category = request.GET.get('category', None)
            keyword  = request.GET.get('keyword', None)
            order    = request.GET.get('order', 'id')
        
            q=Q()
            
            if menu:
                q &= Q(category__menu__id = menu)
            
            if category:
                q &= Q(category__id = category)
                
            if keyword:
                q &= Q(name__contains=keyword)
            
            places = Place.objects.filter(q).order_by(order)

            result = [{
                'id'        : place.id,
                'place_name': place.name,
                'category'  : place.category.name,
                'price'     : place.price,
                'capacity'  : place.capacity,
                'city'      : place.city.name,
                'parking'   : place.parking,
                'url'       : [image.url for image in place.image_set.all()],
                } for place in places]
            
            return JsonResponse({'result' : result}, status=200)
        
        except Place.DoesNotExist:
            return JsonResponse({'message' : 'DOES_NOT_FOUND'}, status=400)
        
        except TypeError:
            return JsonResponse({'message' : 'TYPE_ERROR'}, status=400)
        
        except ValueError:
            return JsonResponse({'message' : 'VALUE_ERROR'}, status=400)

다시 다중 if문으로 구현하여 PR을 요청하였는데,
Q()객체를 사용하면 and,or조건을 유연하게 대응할 수 있다고 하여 로직을 다시 수정하였다.

다음번엔 dictionary comprehension에 대해 제대로 공부하여 prefix를 적용해봐야겠다는 생각이 들었다.

0개의 댓글