현재 백오피스(관리자) 기능의 페이지를 구현중이다.
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
'✨Framework+Library > 🔵Django' 카테고리의 다른 글
[Django] 가상환경으로 프로젝트 세팅하기2 (pipenv) (0) | 2024.09.19 |
---|---|
[Django] 파이썬 버전 바꾸고 가상환경으로 프로젝트 세팅하기(venv) (1) | 2024.06.15 |
[Django] 장고 게시판 구현하기 - ckeditor5 적용하는 방법 (0) | 2024.05.24 |
[Django] @action과 @api_view : View 하나에 같은 HTTP 요청 메서드가 두개일 때 API 구현하기 (0) | 2024.04.25 |
[Django] Django:장고 웹 프레임워크 구조 (0) | 2024.04.01 |