3월 06, 2022

[Django] 1차 프로젝트 필터링 기능 구현

 술담화 페이지를 클론코딩하며, 상품 리스트 페이지에 대한 필터링 및 정렬 기능 구현을 맡게 됐습니다.

저희의 기획은 이렇게 카테고리, 카페인 유무(차담화라 도수 말고 카페인 여부로 변경) , 가격으로 필터링을 한 뒤, 이에 대하여 최신순 또는 평점순으로 정렬하는 것이었습니다. 

처음에는 급한 마음에 기획의도와 코드 방향성에 대해 깊게 생각해보지 않고 각각의 개별 필터링 및 정렬 만이 가능하게 하는 코드를 짰습니다.

밑의 코드가 처음으로 짠 코드입니다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
import json

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

from drinks.models import Drink
# Create your views here.

class FilteringView(View):  
    def get(self, request):
        data     = request.GET #쿼리 스트링 전체를 가져옴 
        
        def compute_reviews(drink):
            drink_reviews = drink.review_set.all() 
            review_count  = drink_reviews.count() 
            sum_rating    = 0
            for review in drink_reviews:       
                sum_rating += review.rating
            if review_count == 0:              
                drink_average_review = 0
            elif review_count != 0:                 
                drink_average_review = sum_rating / review_count
            return review_count, drink_average_review


        if data.get("ordering", None) == "-updated_at":
            recently_ordered_queryset = Drink.objects.all().order_by('-updated_at')
            data_ordered_list = []
            for drink in recently_ordered_queryset:
                data_dict = {}
                data_dict["name"] = drink.name
                data_dict["price"] = drink.price
                review_count , drink_average_review = compute_reviews(drink)
                data_dict["average_rating"] = drink_average_review
                data_dict["review_count"] = review_count
                data_ordered_list.append(data_dict)
            return JsonResponse({'message':data_ordered_list}, status=200) 

        elif data.get("Min_price", None) and data.get("max_price",None):
            price_range                = [int(data.get("Min_price", None)), int(data.get("max_price",None))]
            qualified_drinks           = Drink.objects.filter(price__range=(price_range[0],price_range[1]+1))
            qualified_drinks_in_order = qualified_drinks.order_by('price')
            data_ordered_list = []
            for drink in qualified_drinks_in_order:
                data_dict = {}
                data_dict["name"] = drink.name
                data_dict["price"]  = drink.price
                review_count , drink_average_review = compute_reviews(drink)
                data_dict["average_rating"] = drink_average_review
                data_dict["review_count"] = review_count
                data_ordered_list.append(data_dict)
            return JsonResponse({'message':data_ordered_list}, status=200)



        elif data.get("filtering", None) == "caffeinated":
            drinks = Drink.objects.all()
            caffein_drinks  = drinks.filter(caffeine__range =(1,10000)).order_by('caffeine')
            data_ordered_list = []
            for drink in caffein_drinks:
                data_dict = {}
                data_dict["name"] = drink.name
                data_dict["price"]  = drink.price
                review_count , drink_average_review = compute_reviews(drink)
                data_dict["average_rating"] = drink_average_review
                data_dict["review_count"] = review_count
                data_ordered_list.append(data_dict)
            return JsonResponse({'message':data_ordered_list}, status=200)
            
        elif data.get("filtering", None) == "decaffeinated":
            drinks = Drink.objects.all()
            decaffein_drinks    = drinks.filter(caffeine = 0)
            data_ordered_list = []
            for drink in decaffein_drinks:
                data_dict = {}
                data_dict["name"] = drink.name
                data_dict["price"]  = drink.price
                review_count , drink_average_review = compute_reviews(drink)
                data_dict["average_rating"] = drink_average_review
                data_dict["review_count"] = review_count
                data_ordered_list.append(data_dict)
            return JsonResponse({'message':data_ordered_list}, status=200)

        elif data.get("ordering", None) == "-ratings":
            drinks = Drink.objects.all()
            drink_and_average_rating = {}
            for drink in drinks:
                review_count , drink_average_review = compute_reviews(drink)
                drink_and_average_rating[drink.name] = drink_average_review
            sorted_dict = sorted(drink_and_average_rating.items(), key=lambda x: x[1], reverse=True) 

            sorted_key_list= []
            for i in sorted_dict:
                sorted_key_list.append(i[0])
            data_ordered_list = []
            for name in sorted_key_list:
                data_dict = {}
                drink = Drink.objects.get(name=name)
                data_dict["name"] = drink.name
                data_dict["price"] = drink.price
                data_dict["average_rating"] = drink_and_average_rating[name]
                data_dict["review_count"] = drink.review_set.all().count()
                data_ordered_list.append(data_dict)
            return JsonResponse({'message':data_ordered_list}, status=200)


이렇게 코드를 짜고, 개별 필터링 및 정렬이 가능한 것을 보고, 나름의 안도감(?) 을 느끼며 멘토님께 리뷰를 받으러 갔는데, 리뷰를 받고 나서야 코드를 처음부터 다시 짜야 한다는 것을 깨달았습니다. 
심지어 처음에는 각 필터링 및 정렬 조건을 프론트로 부터 받아야 하는 것 아닌가? 라고 생각하며 post를 사용했습니다( GET을 사용한 것은 한번 수정을 거친 이후의 버전이기 때문입니다)


보시면 아시겠지만, 기능 구현 과정에 있어서 절차적으로 모든 경우의 수를 if elif문 등을 이용하여 분기해준 뒤, 각각 경우의 수에 맞는 처리를 해주었습니다.
멘토님게서 이러한 코드는 매우 절차지향적 코드이고, 객체 지향적으로 코드를 재구성 해야겠다는 피드백을 주셨습니다. 

또한, 술담화의 페이지를 보면 하나의 URI안에서 "쿼리 파라미터"를 통해서 모든 요청을 처리하고 있지만, 저는 메인 리스트뷰 페이지와 필터링과 오더링을 담당하는 URI를 새로 생성할 생각을 하고, urls.py를 통해 URI를 분리해주었습니다.

그러나 백엔드로써, 이렇게 요청에 따라 페이지를 분기해주는 페이지적 관점이 아닌 , 데이터를 한 API안에서 끌고와 , 요청에 따라 분리해줄 필요가 있다는 피드백을 주셨습니다.

정리하자면,

1. 상품 데이터를 전체 끌고오는 메인 리스트에 대한 클래스를 views.py에 작성하고
2. 그 안에서 get메소드를 통해 쿼리 파라미터를 통해 들어오는 요청을 받아 필터링과 정렬을 수행해야만 했습니다.

그래서 다시 짰습니다. 
아래는 수정한 코드입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
from django.http      import JsonResponse
from django.views     import View
from django.db.models import Q

from drinks.models import Drink
# Create your views here.

class ProductsView(View):
    def get(self, request):

        drinks = Drink.objects.all()

        q = Q()

        category    = request.GET.get("category", None)  
        caffeine    = request.GET.get("caffeine", None)
        price_upper = request.GET.get("price_upper", 200000) 
        price_lower = request.GET.get("price_lower", 0) 
    
        if category:
            categories = category.split(',')
            q.add(Q(category__name__in = categories), Q.AND) 

        if caffeine:
            if caffeine   == "yes":
                q.add(Q(caffeine__gt=0), Q.AND)  
            elif caffeine == "no":
                q.add(Q(caffeine__exact=0), Q.AND) 
        
        q.add(Q(price__range = (price_lower, price_upper)),Q.AND)

        filtered_drinks = drinks.filter(q) 


        recently = request.GET.get("recently", None)
        rating   = request.GET.get("rating", None)    

        def compute_reviews(drink):
            drink_reviews = drink.review_set.all() 
            review_count  = drink_reviews.count() 
            sum_rating    = 0
            for review in drink_reviews:       
                sum_rating += review.rating
            if review_count == 0:              
                drink_average_review = 0
            elif review_count != 0:                 
                drink_average_review = sum_rating / review_count    
            return review_count, drink_average_review
        
        def make_whole_data_list(filtered_drinks):
            drink_and_average_rating = {}
            for drink in filtered_drinks:      
                review_count , drink_average_review = compute_reviews(drink)
                drink_and_average_rating[drink.name] = drink_average_review
            whole_data_list = [{
                "name"           : drink.name,
                "price"          : drink.price,
                "average_rating" : drink_average_review,
                "review_count"   : review_count
            } for drink in filtered_drinks]
            return whole_data_list

        if recently:
            filtered_drinks = filtered_drinks.order_by('-updated_at')
            whole_data_list = make_whole_data_list(filtered_drinks)


        elif rating:
            drink_and_average_rating = {}
            for drink in filtered_drinks:      
                review_count , drink_average_review = compute_reviews(drink)
                drink_and_average_rating[drink.name] = drink_average_review
            sorted_dict = sorted(drink_and_average_rating.items(), key=lambda x: x[1], reverse=True)
            sorted_key_list = []
            for i in sorted_dict:
                sorted_key_list.append(i[0])
            whole_data_list = []
            for name in sorted_key_list:
                data_dict = {}
                drink = Drink.objects.get(name=name)
                data_dict["name"]           = drink.name
                data_dict["price"]          = drink.price
                data_dict["average_rating"] = drink_and_average_rating[name]
                data_dict["review_count"]   = drink.review_set.all().count()
                whole_data_list.append(data_dict)

        else: 
            whole_data_list = make_whole_data_list(filtered_drinks)

        return JsonResponse({'message':whole_data_list}, status = 200)
처음에는 Q객체를 사용하고, 객체지향형으로 짜라는 말이 잘 이해가 안됐습니다. 

코드를 다 짜고 나서 보니, Q객체를 이용하여 요청(쿼리파라미터로 들어온) 과 메소드(field lookup)을 캡슐화하여 코드를 구현하라는 뜻인듯 싶습니다.

이렇게 수정을 하고보니 요청에 따른 경우의 수를 일일이 나눠줄 필요가 없을 뿐더러, Q객체를 이용하여 들어온 요청과 메소드를 간단하게 융합할 수 있기에 훨씬 간결하고 객체지향적인 코드가 바뀐게 아닌가 싶습니다.

Q객체를 이용하여 필터링을 모두 진행해준 뒤, 이 필터링이 완료된 쿼리셋에 대하여 ordering은 이전 코드와 유사하게 진행했습니다.