코딩/Django

[Django] F( )표현식

작은코딩 2022. 4. 26. 00:58

🧶 서두

 

프로젝트를 진행하며 동시성 문제를 해결하기 위해 F 클래스를 사용했지만 정확한 원리에 대해 알지 못해서 정리하는 시간을 가져봤다.

 


🧶 F( )표현식

Django 공식문서 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') + 코드는 일반적인 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.')