인턴 기간 동안 기프티콘을 구입하는 로직을 작성할 때가 있었습니다.
이때, 쿼리 표현식을 사용해 성능을 개선한 적이 있었습니다.
먼저 처음 작성한 구매로직의 일부입니다.
구입이 성공적으로 처리되면 , 유저의 Log 테이블을 기록하고 유저가 가지고 있던 포인트를 변경해줘야합니다.
...
username = data['user']
...
user, _ = User.objects.get_or_create(
name=username,
)
point = user.point
...
# 구입이 성공하면 로그 테이블을 기록합니다
if response.status_code == 200:
Log.objects.create(
user_id=user.id,
point_before=point,
point_after=point - item_price,
point_delta=-item_price,
...
)
#유저 테이블의 포인트도 변경해줍니다
User.objects.filter(name=user).update(point=log.point_after)
이렇게 작성한 코드는 문제없이 작동했습니다. 하지만 리드개발자님께서 class F 를 알아보고 변경해보라는 미션을 받았습니다.
class F ?
처음들어봤는데 이름이 멋있습니다. ㅎㅎ
이제 어떤 역할을 하는지 알아보겠습니다.
공식문서를 찾아보니 class F 의 내용은 Query Expressions 챕터에 속해 있습니다. 그 중 Built - in Expressions 부분에 F() expressions 로 속해있습니다.
공식문서에 쓰여있는 정의들을 보겠습니다.
Query Expressions(쿼리 표현식) :
update
,create
,filter
,annotation
,aggregate
의 일부분으로 사용할 수 있는 값이나 계산.
update할 때 사용하니, 포인트를 변경해야 하는 제 현재 상황에 적합합니다.
그럼 F 클래스를 마저 알아보겠습니다.
class F :
F() 객체는 모델 필드 또는 annotated column 의 값을 나타냅니다. 파이썬 메모리를 사용하지 않고 , 이 값들을 참고하여 데이터 베이스에서 작업을 가능하게 해줌.
쿼리 표현식의 F 클래스를 사용하면 파이썬으로 데이터를 처리하는 것이 아니라 데이터 베이스 내에서 처리하게 해줍니다. 데이터 베이스 내에서 처리하기에 속도가 빠릅니다.
공식문서의 예제를 보면서 이해를 넓혀 보겠습니다.
# Tintin filed a news story!
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed += 1
reporter.save()
reporter.stories_filed 의 값은 데이터 베이스를 참조한 후 파이썬 메모리에 로딩됩니다. 메모리 안에서, 파이썬 연산자를 통해 데이터가 처리되고 결과 값을 다시 데이터 베이스에 저장합니다.
그러면 F 클래스를 적용 시켜보겠습니다.
from django.db.models import F
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed = F('stories_filed') + 1
reporter.save()
F 클래스는 파이썬 연산자를 담아서 캡슐화된 SQL 문을 생성합니다. 데이터베이스 자체에서 연산을 해, 결과 값을 저장합니다. 이 작업은 전적으로 데이터 베이스에서 일어나는 일이기에 파이썬은 알지 못합니다. 그렇기에 새로운 값에 접근하기 위해선 코드를 추가해 다시 불러와야합니다.
reporter = Reporters.objects.get(pk=reporter.pk)
# Or, more succinctly:
reporter.refresh_from_db()
그렇다면 간단히 코드를 짜서 쿼리문을 비교해보겠습니다.
import json
from django.db.models import F
class ChangeView(View):
def post(self, request):
data = json.loads(request.body)
user = data['user']
point = data['point']
writer = Log.objects.create(
user = user,
point = point,
)
#기존에 쓰던 update
Log.objects.filter(user=user).update(point=writer.point+1000)
# 클래스 F 를 사용한 로직
# writer.point = F('point') + 1000
# writer.save()
return HttpResponse(user)
이 로직에 user=mincheol, point=10 으로 요청을 보냈습니다.
(이에 대한 쿼리문 : INSERT INTO logs
(user
, point
) VALUES ('mincheol', 10); args=['mincheol', 10])
기존 방식으로 update 시켰을 때의 쿼리입니다.
UPDATE
logs
SETpoint
= 1010 WHERElogs
.user
= 'mincheol'; args=(1010, 'mincheol')
클래스 F 를 적용했을 때의 쿼리입니다.
UPDATE
logs
SETuser
= 'mincheol',point
= (logs
.point
+ 1000) WHERElogs
.id
= 22; args=('mincheol', 1000, 22)
확실히 두 코드는 다르게 작동합니다.
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed = F('stories_filed') + 1
reporter.save()
reporter.name = 'Tintin Jr.'
reporter.save()
만약 reporter.stories_filed 의 초기값이 1 이였다면 위 코드를 실행한 후에는 3이 됩니다.
F 클래스는 save() 를 또 호출하면 해당 속성에 저장된 표현식이 다시 평가됩니다. 그렇기에 사용시 주의가 필요합니다.
User.objects.filter(name=user).update(point=log.point_after)
이제 위 로직을, 클래스 F 를 사용한 아래의 로직으로 바꿔줍니다.
user.point = F('point') - item_price
user.save()
장고 ORM 훌륭하지만, 모든 경우를 다 완벽할 수는 없습니다.
쿼리 표현식을 사용하면 성능을 개선할 수 있습니다.
참고
책 - two scoop of django
공식문서
1