Django 좌표 기반 정렬 구현기

Django 좌표 기반 정렬 구현기

Category
Published
July 26, 2024
Last updated
Last updated September 7, 2024
💡
이 포스트는 현재 작성중입니다

웹서버에서 정렬하기

DB 정렬 vs. 서버 정렬

  • DB 정렬
    • 인덱스가 있는 필드에 대한 정렬이라면, DB에서 정렬시 매우 빠르게 정렬이 가능하다
  • 서버 정렬
    • DB에 비해 웹서버가 Scale-out이 더 쉽기에 DB에서의 연산을 최소화하는 것이 좋다
정렬할 대상이 ~100개정도로 많지 않고, 인덱스의 효과를 보기 어려우며, 수학 연산이 복잡하게 들어갈 수도 있는등, DB에서 정렬하는 장점이 크게 없다고 판단하여 서버단에서 정렬을 해보기로 했다.

retrieve()에서 정렬하기

처음에는 def retrieve()에서 정렬을 진행하기로 했다.
  1. mixins.ListModelMixin에서 retrieve()와 같은 역할을 하는 list()에서 self.filter_queryset(self.get_queryset())로 필터링을 할때 OrderingFilter를 통해 정렬도 진행되기 때문
  1. retrieve() 단계에서 get_object()를 통해 lookup_field를 통해 오브젝트 하나를 가져오기에, 오브젝트를 받아서 정렬을 하는것이 좋아 보였다
  1. get() 단계에서 하기에는 get()은 controller에 가깝기에 로직을 넣지 않기로 했다
def retrieve(self, request, *args, **kwargs): instance = self.get_object() serializer = self.get_serializer(instance) return Response(serializer.data)

문제

def retrieve(self, request, *args, **kwargs): instance = self.get_object() print(type(instance)) print(instance.restaurant_list, type(instance.restaurant_list)) # 출력 # <class 'misiklist.models.Misiklist'> # misiklist.MisiklistThrough.None <class 'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager'>
object를 불러왔을 때 일반 필드는 바로 조회가 가능하지만, related field(ForeignKey, OneToOneField, ManyToManyField)는 바로 불러와 지는 것이 아닌, 해당 객체들에 접근 가능한 다른 ReleatedManager를 가지게 된다.
해당 Manager에서 Queryset API인 .all()을 통해 우선 불러온 후, 이를 정렬 후 기존 필드에 덮어 씌우는것을 시도했다.
instance.restaurant_list.set( sorted( instance.restaurant_list.all(), key=lambda x: x.restaurant.restaurant_info.rating, reverse=True, ) )
여러 오류를 해결하며 .set을 통해 재정렬을 시도하였으나, 순서는 바뀌지 않았다.
해당 이슈를 찾아보니 쿼리셋에서 순서를 바꾸더라도 해당 순서를 보장해주지 않기에, 순서를 바꾸려면 쿼리 시점에 정렬을 실행해야 한다는 것을 알 수 있었다.
아니면 아에 필드위에 덮어 씌우는 것 자체가 안되는 느낌도 받았다(추후 확인 필요).

정렬 필드 추가 방식

instance.sorted_restaurant_list = sorted( instance.restaurant_list.all(), key=lambda x: x.restaurant.restaurant_info.rating, reverse=True, ) print(instance.restaurant_list, type(instance.restaurant_list)) print(instance.restaurant_list.all(), type(instance.restaurant_list.all())) print( instance.sorted_restaurant_list, type(instance.sorted_restaurant_list) ) # misiklist.MisiklistThrough.None <class 'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager'> # <QuerySet [<MisiklistThrough: 미식리스트 - 식당1>, <MisiklistThrough: 미식리스트 - 식당2>]> <class 'django.db.models.query.QuerySet'> # [<MisiklistThrough: 미식리스트 - 식당2>, <MisiklistThrough: 미식리스트 - 식당1>] <class 'list'>
이에 쿼리셋을 덮어 쒸우기보다 sorted_restaurant_list 라는 새로운 필드를 추가해 저장하고, serializer에서 변경해주는 것을 시도해보았다.
우선 변경후 값을 출력해보았을 때, QuerySet이 아닌 list 방식으로 나온것을 확인할 수 있었다.
class MisiklistDetailSerializer(serializers.ModelSerializer): restaurant_list = MisiklistThroughSerializer( many=True, required=False, source="sorted_restaurant_list" )
이후 source를 통해서 정렬된 필드를 지정해주니 기능은 정상적으로 작동했다.
 
def sort_key(x): distance = getattr(x.restaurant, "distance", None) if distance is None: return ( 1, inf, ) # 튜플의 첫 번째 요소로 1을 사용하여 distance가 없는 항목을 뒤로 보냄 return (0, distance) # distance가 있는 항목은 앞쪽에 정렬 instance.sorted_restaurant_list = sorted( instance.restaurant_list.all(), key=sort_key )
해당 방식을 사용했을 때 DB에서 생성한 distance 필드도 문제없이 접근 가능했고, 기능도 정상 동작했다.

Serializer에서 정렬

기능 구현은 무사히 했으나, retrieve() 에서 정렬을 구현하는 것에 대해 의문이 들었다.
우선 sorted_restaurant_list를 따로 생성해야 하는 것이 불만이였는데, 이렇게 된다면 Serializer단에서 정렬을 구현하는 것이 가장 깔끔한 방식이라는 생각이 들었다.

문제

쿼리셋을 불러와 리스트형식으로 변경했을 때 distance 필드가 누락되는 문제가 생겼다.
def get_restaurant_list(self, obj): request = self.context.get("request") if not request: return [] ordering = request.query_params.get("ordering", "order") latitude = request.query_params.get("latitude") longitude = request.query_params.get("longitude") restaurant_list = list( obj.restaurant_list.all().select_related( "restaurant", "restaurant__restaurant_info", ) ) if ordering == "distance" and latitude and longitude: def sort_key(x): distance = getattr(x.restaurant, "distance", None) if distance is None: return (1, inf) # 거리 정보가 없는 항목을 뒤로 보냄 return (0, distance) sorted_restaurants = sorted(restaurant_list, key=sort_key) elif ordering == "rating": sorted_restaurants = sorted( restaurant_list, key=lambda x: x.restaurant.restaurant_info.rating, reverse=True, ) else: # 기본 정렬 (order 필드 기준) sorted_restaurants = sorted(restaurant_list, key=lambda x: x.order) return MisiklistThroughSerializer( sorted_restaurants, many=True, context=self.context ).data
이는 새롭게 obj.restaurant_list.all()로 쿼리를 했을 때 기존에 distance annotate가 포함된 쿼리가 아닌 새로운 쿼리가 생성되는 문제였다.
이때 select_related를 prefetch_related로 전환해서 문제를 해결했다. 이는 prefetch_related를 사용하면 별개의 쿼리를 날리고, 무언가 캐싱을 하는 효과로 인해서 distance를 정상적으로 가져올 수 있는 것으로 보인다(확인필요).
이때 실행되는 쿼리를 확인해보면 기존에 실행되던 5개의 쿼리 외에도,
SELECT "misiklist_misiklistthrough"."id", "misiklist_misiklistthrough"."misiklist_id", "misiklist_misiklistthrough"."restaurant_id", "misiklist_misiklistthrough"."order", "misiklist_misiklistthrough"."memo" FROM "misiklist_misiklistthrough" WHERE "misiklist_misiklistthrough"."misiklist_id" = 26051 ORDER BY "misiklist_misiklistthrough"."misiklist_id" ASC, "misiklist_misiklistthrough"."order" ASC; SELECT "restaurant_restaurant"."id", "restaurant_restaurant"."uuid", "restaurant_restaurant"."name_native", "restaurant_restaurant"."name_korean", "restaurant_restaurant"."name_english", "restaurant_restaurant"."latitude", "restaurant_restaurant"."longitude", "restaurant_restaurant"."thumbnail", (SQRT((POWER(("restaurant_restaurant"."latitude" - 35.3), 2) + POWER(("restaurant_restaurant"."longitude" - 139.5), 2))) * 111319.9) AS "distance", "restaurant_restaurantinfo"."restaurant_id", "restaurant_restaurantinfo"."summary", "restaurant_restaurantinfo"."rating", "restaurant_restaurantinfo"."rating_taste", "restaurant_restaurantinfo"."rating_price", "restaurant_restaurantinfo"."rating_service", "restaurant_restaurantinfo"."daytime_price", "restaurant_restaurantinfo"."evening_price" FROM "restaurant_restaurant" LEFT OUTER JOIN "restaurant_restaurantinfo" ON ("restaurant_restaurant"."id" = "restaurant_restaurantinfo"."restaurant_id") WHERE "restaurant_restaurant"."id" IN (290, 271, 115, 91, 188, 189);
위와 같은 두번의 쿼리가 serialize 단계에서 추가적으로 실행되는데, 이는 기존에 이미 진행된 쿼리와 똑같은 쿼리가 실행되는 것이다. 그렇기에 성능적으로 손해가 있다.
 
 

View단게에서 필터링으로 정렬

filter_backends = [filters.OrderingFilter]와 ordering_fields = [~]를 통해 정렬을 하듯이, 커스터마이징된 필터를 추가해 정렬을 해볼 계획. 추후 수정
 

PostGIS, GeoDjango 이용

성능 테스트

 
 
 

Reference