🧶 서두
프로젝트를 진행하며 동시성 문제를 해결하기 위해 F 클래스를 사용했지만 정확한 원리에 대해 알지 못해서 정리하는 시간을 가져봤다.
🧶 F( )표현식
🔋 F( ) 정의
F() 개체는 모델 필드의 값, 모델 필드의 변환된 값 또는 주석이 달린 열을 나타냅니다. 실제로 데이터베이스에서 Python 메모리로 가져올 필요 없이 모델 필드 값을 참조하고 이를 사용하여 데이터베이스 작업을 수행할 수 있습니다.
나름의 해석을 해보자면 "F() 클래스를 사용해서 만든 객체는 모델의 필드의 값, 모델 필드의 변화된 값, 주석이 달린 열을 나타내는데 Python 메모리로 데이터를 가져오지 않고 데이터베이스에서 작업을 수행한다"라고 해석할 수 있다.
🔋 F( ) 활용법
장고 공식 문서에 나와있는 예제를 살펴보자.
# Tintin filed a news story!
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed += 1
reporter.save()
이 방법은 DB Reporters 테이블에서 name 필드 값이 "Tintin"인 object를 python 메모리로 가져와서 reporter 변수에 담아주고 stories_filed 값에 1 더하는 연산을 해준 뒤 다시 DB에 쿼리를 보내서 수정된 값을 업데이트해준다.
코드만 보면 문제가 없는 코드지만, 동시성 문제를 안고 있다.
stories_filed의 default 값이 0일 때 위 코드가 1번 실행이 되면 1이 되고 다시 1번 실행이 되면 2가 되어야 한다.
하지만 동시에 2번 실행이 되면 두 코드 전부 DB에서 0 값을 python 메모리로 가져와서 1 더하는 연산을 하기 때문에 결과는 1이 된다.
이런 현상은 경쟁 조건(race condition)이라고 하는데 F( )표현식을 사용해서 이런 문제를 해결할 수 있다.
from django.db.models import F
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed = F('stories_filed') + 1
reporter.save()
reporter.stories_filed = F('stories_filed') + 1 코드는 일반적인 Python 구문처럼 보이지만 실제로는 DB작업을 설명하는 SQL 구문이다.
Django가 F( )인스턴스를 만나면 캡슐화된 SQL 표현식을 생성하고 save()가 호출되면 DB로 쿼리문이 보내져 DB내에서 작업이 이루어지는 구조인 거 같다. 이런 구조를 가지고 있다 보니 python은 DB에서 어떤 일이 일어났는지 알지 못해서 새로 저장된 값을 알고 싶다면 해당 인스턴스를 다시 가져와야 한다.
reporter = Reporters.objects.get(pk=reporter.pk)
# Or, more succinctly:
reporter.refresh_from_db()
F( )는 이런 단일 인스턴스에 대한 작업뿐 아니라 쿼리셋에도 적용할 수 있는데 이 문법을 사용하면 따로 read, update 했던 두 개의 쿼리를 하나의 쿼리로 합칠 수 있다.
reporter = Reporters.objects.filter(name='Tintin')
reporter.update(stories_filed=F('stories_filed') + 1)
# 더 압축을 하면
Reporter.objects.filter(name='Tintin').update(stories_filed=F('stories_filed') + 1)
🔋 F( ) 효과
그렇다면 F( )표현식은 왜 사용할까? 사용을 하면 얻을 수 있는 기대 효과는 무엇일까?
이 질문에 대해 공식문서에서는 다음과 같은 해답을 내놓는다.
1. 성능(속도) 개선.
데이터베이스에서 객체를 python으로 가져오고, 객체의 필드 값을 증가시키고, 다시 데이터베이스에 저장하는 과정보다 훨씬 빠를 수 있다. 특히 대량의 객체를 조회하고 수정하게 되면 F( )표현식의 가치는 더욱 클 수밖에 없다.
2. 경쟁 조건을 피할 수 있다.
앞서 설명했듯이 F( )표현식을 사용하면 동시성 문제를 해결할 수 있다.
F()의 또 다른 유용한 이점은 Python이 아닌 데이터베이스에서 필드 값을 업데이트하면 경쟁 조건을 피할 수 있다는 것입니다.
두 개의 Python 스레드가 위의 첫 번째 예에서 코드를 실행하면 다른 스레드가 데이터베이스에서 필드 값을 검색한 후 한 스레드가 필드 값을 검색, 증가 및 저장할 수 있습니다. 두 번째 스레드가 저장하는 값은 원래 값을 기반으로 합니다. 첫 번째 스레드의 작업이 손실됩니다.
데이터베이스가 필드 업데이트를 담당하는 경우 프로세스가 더 강력합니다. 인스턴스가 검색될 때의 값을 기반으로 하지 않고 save()또는 update() 실행될 때 데이터베이스의 필드 값을 기반으로만 필드를 업데이트합니다.
🔋 F( ) 주의사항
성능개선과 동시성 문제를 해결해주는 F( )표현식을 사용할 때는 한 가지 주의사항이 있다.
예를 들어 다음 코드가 실행되는 상황을 생각해보자.
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed = F('stories_filed') + 1
reporter.save()
reporter.name = 'Tintin Jr.'
reporter.save()
stories_filed 필드의 초기 값이 1일 때 코드가 실행되면 2 값을 기대할 수 있다. 하지만 결과는 3이 된다고 한다.
내가 직접 코드를 입력해보고 디버깅한 게 아니라서 정확하게 진단을 할 수는 없지만 2번째 줄에서 //reporter.stories_filed = F('stories_filed') + 1// stories_filed 값을 정의해 놓았기 때문에 마지막 save 함수가 호출될 때 해당 작업이 한번 더 실행되는 것으로 보인다.
이게 무슨 말이냐 하면..
우리는 name만 바꾸고 save를 했지만 사실 save함수는 모든 필드를 다시 SET 하는 함수이다.
(다른 분이 써주신 쿼리문을 잠깐 참고하자면)
UPDATE post
SET title = 'title1' , text = 'text1', read_count = (post.read_count + 1)
WHERE Post.id = 1
post의 read_count를 수정하게 F( )표현식을 사용해도 실제 save 될 때 쿼리를 보면 title, text, read_count 정보를 모두 수정하는 걸 볼 수 있다.
예제로 돌아와서 생각해보면
앞선 작업에서 stories_filed 필드에 F( )클래스를 활용한 구문이 저장되어 있고 나중에 name 필드를 수정하기 위해 save를 호출하면 결과적으로 name을 수정하고 stories_filed에 1 더하는 sql 구문도 한번 더 실행되는 것이다.
이걸 쿼리문으로 나타내면 (테스트하지 않고 수기로 작성한 거라 참고만 해주세요! :) )
<기대>
UPDATE reporters
SET name = 'Tintin Jr.' , stories_filed = 2
WHERE reporters.name = 'Tintin'
<현실>
UPDATE reporters
SET name = 'Tintin Jr.' , stories_filed = (reporters.stories_filed + 1)
WHERE reporters.name = 'Tintin'
stories_filed 필드는 그대로 2로 저장되어 있고 name 필드만 'Tintin Jr.'로 수정되는 결과를 바랐지만 사실은 stories_filed 필드에 1 더하는 작업을 계속해주고 있다는 사실..
이걸 해결하려면 name 필드를 수정할 때 reporter를 새로 불러오거나 그냥 filter - update를 사용해 쿼리수를 줄인 문법을 쓰자. 개인적으로 쿼리 수도 줄어들고 코드도 간편해서 선호하는 방법 :)
<reporter 덮어쓰기>
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed = F('stories_filed') + 1
reporter.save()
# stories_filed가 수정된 값으로 reporter 덮어쓰기
reporter = Reporters.objects.get(name='Tintin')
reporter.name = 'Tintin Jr.'
reporter.save()
<filter-update 사용하기>
Reporter.objects.filter(name='Tintin').update(stories_filed=F('stories_filed') + 1)
Reporter.objects.filter(name='Tintin').update(name='Tintin Jr.')
# 사실 따로 수정할 필요가 없으니,, 하나로 끝낸다면
Reporter.objects.filter(name='Tintin').update(stories_filed=F('stories_filed') + 1, name='Tintin Jr.')
'코딩 > Django' 카테고리의 다른 글
[Django] 역참조 테이블 필드로 정렬하기 (order_by) (0) | 2022.03.19 |
---|---|
Django channels 실시간 채팅 기능 (websocket) (3) | 2022.03.06 |