디스 프로그래머 (This Programmer)

django에서 object queryset을 여러 기준으로 ordering하기 + object filter의 order_by 기준을 동적으로 구성하기. 본문

Python/Django

django에서 object queryset을 여러 기준으로 ordering하기 + object filter의 order_by 기준을 동적으로 구성하기.

디스 프로그래머 2020. 1. 9. 18:56

django에서 object queryset을 여러 기준으로 ordering하기 + object filter의 order_by 기준을 동적으로 구성하기.

이런 저런 작업을 하다가 Big aha moment를 만나서 적어두려 한다. 다른 부분은 거두절미 하겠다.

 

이러한 모델이 만들어져있다는 가정 하에 시작할 것이다.

class Post(models.Model):
	title = models.CharField() #제목
	content = models.TextFiled() #내용
	timestamp = models.DateTimeField(default=timezone.now) #만들어진 시간
	like_count = models.IntegerField() #좋아요 눌린 횟수
    view_count = models.IntegerField() #뷰 횟수

 

보통 이런 게시물류의 객체는 만들어진 시간을 기준으로 정렬하기 마련이다. 그래서 views.py단에서 이런 식으로 정리해서 뿌려주게 된다.

post = Post.objects.all().order_by('-timestamp')
# 만들어진시간을 기준으로 내림차순 -> 가장 최근의 게시물이 위에오게

(오름차순으로 하고 싶다면 -timestamp앞의 -를 없애주면 된다.)

 

만약에 게시판에 정렬기능을 추가해서 좋아요가 많이 눌리게 된 순으로 정렬하고 싶다면? 답은 간단하다.

post = Post.objects.all().order_by('-like_count')
# 좋아요 갯수의 내림차순 -> 가장 많은 좋아요 갯수가 달린 게시물이 위에오게

로 정렬하면 된다.

 

그렇다면 복합적으로, 좋아요가 가장 많이 눌린 순서대로 정렬하되, 같은 갯수의 좋아요가 눌린 게시물이 있다면 더 예전에 써진 글부터 위에 노출하고 싶다. 하면 어떻게 해야할까. 이것 역시 간단하다.

post = Post.objects.all().order_by('-like_count', '-timestamp')
# 좋아요 갯수의 내림차순으로 먼저 

이런식으로 하면 된다.

 

들어오는 요청값에 따라 order_by안에 들어가는 문자열을 컨버팅해준다면 이정도의 처리는 어렵지 않을 것이다. 하지만 문제는 "상식적인 사이트가 작동하는 방식"을 구현하는 데에서 튀어나온다.

 

자, 한 유저가 게시판에 들어왔다고 치자. 가장 익숙한 모습인 시간의 내림차순으로 정렬돼있을 것이며 쿼리는

post = Post.objects.all().order_by('-timestamp')

이런 식으로 구성이 돼있을 것이다. (페이지 네이션과 같은 요소는 제하였다.)

이 유저는 이 게시판에 어떤 게시물이 제일 인기있었는지 궁금해서 좋아요의 내림차순으로 게시물들을 조회하려 한다. 그러면

post = Post.objects.all().order_by('-like_count', '-timestamp')

이런 모습의 쿼리가 돼야한다.

 

그렇다. 들어오는 요청에 맞춰서 유저가 원하는 방식의 게시물 정렬을 위해선 order_by()안에 들어가는 인수의 형태나 갯수, 순서마저 바뀌어야한다. 물론

post = Post.objects.all().order_by(like_count_order, timestamp_order)

같은 식으로 변수만 받아서 뿌려주면서 빈 변수는 생략되는 식이라면 얼마나 편할까, 하지만 order_by()는 그런식으로 작동하지 않는다. 빈 칸이 있거나 이상한 정렬값이 들어오면 여지없이 Invalid order_by arguments 에러를 내뿜는다. 그리고 심지어 이런식이면 timestamp와 like_count의 우선순위가 뒤바뀌었을 때의 처리는 거의 불가능에 가깝다.

 

물론 조건에 따라 모든 경우의 수에 맞춰 if문으로 일일히 작성하면 되겠지만... 프로그래머는 그런식의 프로그래밍은 유지보수와 디버깅, 기능확장에 커다란 장애가 된다는 점 모두 잘 알고 있을 것이다.

예를 들어 정렬기준이 이 두가지라면 그래도 또 if문으로 해결이 가능하겠지만 만약에 view_count까지 정렬 기준에 합세하고 거기에 오름차순 내림차순 기능까지 추가된다면 말이다.

 

아무튼 이러한 요소들을 수월하게 해결하려면 결국엔 if문으로 가지를 치는 게 아니라 oder_by안에 들어가는 요소들을 동적으로 바꿔줘야 한다는 결론에 이른다. 실제 내가 작업하고 있는 플랫폼의 코드를 빌려오자면 이렇다. (내가 만들고 있는 내 플랫폼이니 문제가 될 소지는 없을 것이다.)

# order는 오름차순인지 내림차순인지 정의하는 변수이고
# sort는 그 기준이 어떤 컬럼인지 정의하는 변수이다.

ordering_priority = []

# 이곳에서 오름차순인지 내림차순인지 알아낸 뒤에 명령어를 변수에 입력해둔다.
order = '' if request.GET.get('order') == 'asc' else '-'

# 종합해서 ordering query를 생성한다.
if request.GET.get('sort') and request.GET.get('sort') != 'timestamp':
    # 정렬 기준이 시간이 아닐 때만 해당 값을 ordering_priority에 추가한다.
    # 시나리오대로라면 이곳에 like_count라는 값이 들어갔을 것이다.
    # 다른 기준으로 정렬하면 무조건 시간의 내림차순으로 정렬하기 때문에 이런식으로 구성이 된다.
    sort = request.GET.get('sort')
    ordering_priority.append(order + sort)
    ordering_priority.append('-timestamp')
else:
    sort = 'timestamp'
    ordering_priority.append(order + sort)
            
posts = Post.objects.all().order_by(*ordering_priority)

이런식으로 하면 어떤 정렬기준을 들이밀어도 수월하게 해낼 수 있을 것이다.

 

p.s: 사실 동적으로 ordering query를 뱉어주는 로직은 다 만들어놓고 order_by의 뒤어 들어가는 인수의 자료형 이리저리 해봐도(list, tuple, set, dict등...) 제대로 실행도 안되고 오류만 내뱉길래 골치아팠는데 갑자기 옛날에 알고리즘 문제를 풀다가 *iterable식으로 하면 안의 요소가 전부 풀려서 쓰여진다는 사실이 기억나서 해봤더니 잘 돼서 기쁜 마음에 글까지 남긴다. 이렇게 하면 수많은 컬럼이 덕지덕지 달려서 엑셀처럼 수많은 정렬기준과 오름차순 내림차순의 우선순위까지 요구하는 어플을 만든다고 할지라도 수월하게 작성할 수 있을 것이다.

2 Comments
  • 프로필사진 rumbarum 2020.01.27 21:09 * 으로 풀어 넣는 것도 orderby로 쓸수 있네요. 좋은팁 얻어 갑니다.

    보다보니, order_by는 그냥 스트링으로 넣어도 되는것 같네요, 여러 쿼리를 처리하기 위해 다음처럼 작성해보면 어떨까요?

    # 다중 sort, query_string으로 받을시,
    # mainURL/post?sort=view_count,like_count,-timestamp
    sort = request.GET.get('sort','-timestamp')
    sort = sort.split(',')
    posts = Post.objects.all().order_by(*sort)

    #다중 sort body로 받을시,
    # data = {
    "order_by":
    [
    {"order":"", "field":"timestamp"},
    {"order": "", "field":"view_count"},
    {"order":"asc","field":"like_count"},
    ]
    }
    post = Post.objcects.all()
    if 'order_by' in data:
    for order in data['order_by']:
    field = order['field']
    asc = '-' if not order['asc'] else ""
    post = post.order_by(f"{asc}{field}")
    좀 길어지지만, 이런식으로 다중 처리도 가능 할 것 같아요.
  • 프로필사진 Favicon of https://this-programmer.com 디스 프로그래머 2020.01.28 17:37 신고 저도
    sort = request.GET.get('sort','-timestamp')
    부분에서 GET KEY에 sort가 넘어오지 않으면 -timestamp라는 문자열로 대체된다는 사실을 처음 알게 됐네요. 이걸 알고 있었더라면 코드가 훨씬 깔끔해졌을 텐데...

    긴 답변 남겨주셔서 감사합니다. 말씀주신 방법도 좋아보여요. 저도 좋은 팁 얻어갑니다!
댓글쓰기 폼
Prev 1 2 3 4 5 6 7 Next