Intern | class F (쿼리 표현식)

김민철·2021년 2월 26일
1

인턴

목록 보기
3/3
post-thumbnail

Intro

인턴 기간 동안 기프티콘을 구입하는 로직을 작성할 때가 있었습니다.
이때, 쿼리 표현식을 사용해 성능을 개선한 적이 있었습니다.

먼저 처음 작성한 구매로직의 일부입니다.
구입이 성공적으로 처리되면 , 유저의 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 ?
처음들어봤는데 이름이 멋있습니다. ㅎㅎ
이제 어떤 역할을 하는지 알아보겠습니다.

공식문서를 찾아보니 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 SET point = 1010 WHERE logs.user = 'mincheol'; args=(1010, 'mincheol')

클래스 F 를 적용했을 때의 쿼리입니다.

UPDATE logs SET user = 'mincheol', point = (logs.point + 1000) WHERE logs.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개의 댓글

comment-user-thumbnail
2022년 10월 17일

1

답글 달기