Tiny Star

✨Framework+Library/🔵Django

[Django] Q 객체 : 쿼리의 효율 올리기

청크 2024. 7. 16. 21:32

현재 백오피스(관리자) 기능의 페이지를 구현중이다.

 

DEV-DB에서 개발 중이지만, 운영 DB를 가지고 로컬에 복붙해놓은거라

그동안 개발공부를 하면서 더미로 집어넣은 소소한 데이터와는 양 자체가 다르다.

 

앞서 php로 구현된 구 코드들은 구현을 오라클 패키지를 사용해서 데이터를 호출했지만

python으로  전면 개편하게 되면서 패키지는 생각하지 못하고 그냥 쿼리셋을 날린 결과 속도저하가 발생했다.

 

회원의 그룹과 계정을 각각 조회할 수 있는 페이지를 만들었고

그룹을 조회하는 페이지는 잘 로드되지만 계정을 조회하는 페이지는 로드되는데 시간이 너무 오래걸리면서

아래와 같은 ConnectionAbortedError가 나왔다. (시간이 지나면 로드가 되긴 됨)

 

어차피 이대로는 실서버 반영을 할 수 없으니 쿼리 속도를 개선해보기로 했고

다양한 방법으로 구글링하다가 발견한 방법이 Q객체였다.

 

Q 객체를 공부할 겸 코드 리팩토링 했던걸 남겨보려고 한다.


Q 객체

Django ORM에서 복잡한 쿼리 조건을 작성할 때 유용하게 사용할 수 있는 도구로 생각하면 쉽다.

 

개발을 하다보면 때때로 복잡한 조건이 많이 추가된 쿼리가 필요할 때가 있고이 테이블 저 테이블에 여러 조건을 설정해서 데이터를 받아오는 경우도 있다.

 

example = ModelName.objects.fiter(조건).order_by('-cr_time')

 

뭐 보통 이렇게 filter로 조건을 걸어 데이터를 가져온다.

저 filter 괄호 안에  and/or 등등을 사용해 내가 원하는 조건을 걸게 되고, 

또 다른 여러 조건이 생기면 쿼리셋을 추가로 작성해야한다.

 

이러한 방식이 안되는건 아니지만 개발하면서 쿼리셋을 조건만큼 분기하기 어렵기 때문에 filter내의 조건들을

AND, OR와 같은 논리 연산자를 사용하여 조건을 결합, 분기하거나 조건을 동적으로 구성할  수 있도록 해주는게 바로 Q 객체이다. 


Q 객체의 기본 사용

Q 객체는 django.db.models 모듈에 포함되어 있는 객체이기 때문에

모듈을 아래와 같이 import해주기만 하면 사용이 가능하다.

from django.db.models import Q

Q 객체의 주요 기능

1. 기본 조건 만들기

위에도 적어놓은 기본 조건을 거는 쿼리셋의 형태를 Q객체를 이용해서 기본 조건을 걸 수 있다.

from myapp.models import MyModel
from djsngo.db.models import Q

# 기본 쿼리셋 
queryset = MyModel.objects.filter(name__icontains='john')

# Q객체를 사용한 쿼리셋
queryset = MyModel.objects.filter(Q(name__icontains='john'))

두 코드 예제 모두 동일한 결과를 반환하고, 이런 간단한 조건에서는 차이가 없지만

그냥 이렇게 쓴다라는 것을 익히기 위해 내용을 넣어봤다.

 

2. AND / OR / NOT 연산자 사용

기본연산에 더해서 두 개 이상의 조건을 걸 때 AND/OR 연산자를 사용하는데, 이때도 Q객체를 사용할 수 있다.

 

AND

from myapp.models import MyModel
from djsngo.db.models import Q

# 기본 쿼리셋 
queryset = MyModel.objects.filter(name__icontains='john', age=30)

# Q객체를 사용한 쿼리셋
queryset = MyModel.objects.filter(Q(name__icontains='john') & Q(age=30))

 

OR

from myapp.models import MyModel
from djsngo.db.models import Q

# 기본 쿼리셋 
queryset = MyModel.objects.filter(name__icontains='john') 
         | MyModel.objects.filter(age=30)


# Q객체를 사용한 쿼리셋
queryset = MyModel.objects.filter(Q(name__icontains='john') | Q(age=30))

 

Q객체를 사용했을 때 편리한게 AND 조건까지는 비슷해보이지만 OR조건에서 그 차이가 명확하게 드러난다.

 

OR 조건에서 기본 쿼리셋을 쓴다면 My.Model.objects.filter를 두번 작성하여 조건별로 filter를 만들어줘야하지만

Q 객체를 사용한다면 filter는 한번만 명시하고 Q로 조건을 감싸서 표현할 수 있다.

 

NOT

from myapp.models import MyModel
from djsngo.db.models import Q

# 기본 쿼리셋 
queryset = MyModel.objects.exclude(name__icontains='john')


# Q객체를 사용한 쿼리셋
queryset = MyModel.objects.filter(~Q(name__icontains='john'))

NOT의 조건을 사용하는 경우 기본 쿼리셋에서는 NOT대신 exclude를,

Q객체에서는 Q앞에 ~를 붙여 NOT 연산자를 대신할 수 있다.

 

 

 

조금 더 복합적인 예제인 경우는 어떻게 사용할까

 

AND, OR, NOT의 조건이 모두 충족하는 경우

-> name 필드에 'john'이 포함되거나, age 필드가 30이며, is_active 필드가 True인 조건

SELECT * FROM myapp_mymodel WHERE (name LIKE '%john%' OR age = 30) AND is_active = True;

 

만약 이러한 복합조건을 Q객체 없이 사용한다면 여러 개의 filter와 exclude를 중첩하여 사용해야하기 때문에

코드의 복잡도가 올라간다.

 

기본 조건만을 사용해서 복합 조건을 처리하는 경우

 

1. OR 연산자를 처리 : OR 조건을 위해 두 개의 쿼리셋을 생성하여 합침

queryset1 = MyModel.objects.filter(name__icontains='john')
queryset2 = MyModel.objects.filter(age=30)
combined_queryset = queryset1 | queryset2

2. AND 연산자를 처리 : AND 조건을 위해 추가 필터링

combined_queryset = combined_queryset.filter(is_active=True)

3. NOT 연산자를 처리 : NOT 조건을 위해 exclude 사용

final_queryset = combined_queryset.exclude(name__icontains='john')

 

이렇게 각각 처리해서 데이터를 받아와야한다. 

 

name 필드에 'john'이 포함되거나 age 필드가 30인 레코드를 필터링하고, 

그 결과에서 is_active 필드가 True인 레코드를 필터링한 후, 

다시 그 결과에서 name 필드에 'john'이 포함되지 않는 레코드를 제외하는 복잡한 과정을 거쳐야만 원하는 값을

받아올 수 있게 된다.

 

이처럼 Q 객체 없이 복잡한 조건을 기본 조건으로 처리하는 것은 가능하지만, 

코드가 복잡해지고 가독성이 떨어질 수 있다.

 

반면 Q 객체를 사용하면 이러한 복잡한 조건을 어떻게 처리하게 될까.

 

Q 객체를 사용해서 복합 조건을 처리하는 경우

from myapp.models import MyModel
from django.db.models import Q

queryset = MyModel.objects.filter((Q(name__icontains='john') 
              | Q(age=30)) & Q(is_active=True))

단 한줄로 복잡 조건을 모두 처리했다.


REFACTORING

 

그럼 이제 성능저하가 된 내 코드를 뚝딱 고쳐보자.

 

우선 내 코드는 검색 기능을 구현했기 때문에 사용자 입력에 따라 결과가 바뀌는 동적 조건으로,

AND, OR, NOT 등의 조건은 포함되지 않았지만 두가지 로직이 있다.

 

1. search_option과 search_query로 사용자 입력 값을 받아서 해당 조건에 맞는 데이터들을 가져오는 로직

    if search_option and search_query:
        if search_option == 'user_nm':
            accounts = accounts.filter(user_nm__icontains=search_query)
        elif search_option == 'user_id':
            accounts = accounts.filter(user_id__icontains=search_query)
        elif search_option == 'group_nm':
            accounts = accounts.filter(
            group_cd__in=GroupManager.objects.filter(group_nm__icontains=search_query).values('group_cd'))

 

 

2.  드롭다운 메뉴에서 조건을 받아 해당 데이터를 가져오는 로직

    if function_limit:
        accounts = accounts.filter(function_limit=function_limit)

    if use_yn:
        accounts = accounts.filter(use_yn=use_yn)

 

사실 다른 로직들은 모델에서 filter로 단일 조건을 가지고오는 로직이라서 쿼리 속도에 영향을 주지는 않는 것 같았고

의심가는 부분은 group_nm을 불러오는 로직때문에 전체적인 속도저하가 일어난 것 같았다.

 

group_nm 은 그룹테이블에 존재하고 계정테이블에는 group_ id만 존재하고 있어서

group_ id 로 매칭해서 group_nm을 받아와야하는 상황이라 이 조건을 의심했다.

search_option == 'group_nm':
            accounts = accounts.filter(
            group_cd__in=GroupManager.objects.filter(group_nm__icontains=search_query).values('group_cd'))

 

이 로직은 search_option이 'group_nm'인지 확인하고 만약에 search_option이 'group_nm'이라면,

group_nm을 기준으로 group_cd를 찾고, 이를 AccountManager에서 필터링해서 검색을 수행한다.

 

Q객체를 사용하지 않은 기본 조건의 쿼리셋은 아래와 단계를 거치게 된다.

1. search_option과 search_query가 제공되면, 해당 조건에 맞는 필터를 추가
2. user_nm, user_id, group_nm에 대한 필터링 조건을 추가
3. group_nm의 경우, GroupManager에서 group_nm이 search_query를 포함하는 그룹의 group_cd를 찾아서 필터링

 

이 부분을 Q 객체를 사용한 코드로 바꿔봤다.

 

icontains를 사용해서 대소문자 구분없이  group_nm에 search_query가 포함되어 있는지 확인하고
.values_list('group_cd', flat=True)으로 필터링된 결과에서 group_cd 값만 추출하여 리스트로 반환한다. 

group_cd__in=group_code로 group_cd 필드가 group_codes 리스트에 포함된 계정만 필터링을 한번 더 거쳐

기존의 Q 객체 accounts에 이 조건을 추가하여, 최종적으로 모든 조건을 결합한 후 데이터를 반환해주는 로직으로 변경했다.

search_option == 'group_nm':
            group_codes = GroupManager.objects.filter(group_nm__icontains=search_query).values_list('group_cd', flat=True)
            accounts &= Q(group_cd__in=group_codes)

·

·

·

뭐...암튼 이렇게 코드 리팩토링을 했는데 결론적으로는 Q객체를 쓰지않아서 발생한 속도저하는 아니었다.

 

처음 페이지 로드 시 테이블에 표시 될 group_nm을 for으로 하나하나 찾아왔기 때문.. 이었다^^

    # for account in accounts:
    #     group = GroupManager.objects.get(group_cd=account.group_cd)
    #     account.group_nm = group.group_nm

 

페이지네이션도 직접 view에서 계산해서 던져주는 로직이었는데페이지네이션 for문이랑 group_nm찾는 for문이 같이 돌면서 생긴 문제같았음..

 

group_nm찾는 for문은 지우고 페이지네이션 fot문에 group_nm 찾는 쿼리셋 추가해주니까..해결됨^;;

약간 삽질이었지만 Q객체에 대해 알아보는 시간이었당...ㅎ

group_nm = GroupManager.objects.get(group_cd=account.group_cd).group_nm