본문 바로가기
프로그래밍/Django

Django ORM 성능 최적화: F() 표현식과 Q() 객체 완벽 가이드

by 우주를놀라게하자 2025. 4. 15.
반응형
SMALL

Django ORM은 개발자가 SQL 쿼리를 직접 작성하지 않고도 데이터베이스를 효율적으로 다룰 수 있게 해주는 강력한 도구입니다. 그러나 대규모 애플리케이션이나 트래픽이 많은 서비스에서는 ORM을 어떻게 활용하느냐에 따라 성능 차이가 크게 날 수 있습니다.

이 글에서는 Django ORM의 핵심 기능인 F() 표현식과 Q() 객체를 깊이 있게 살펴보고, 이를 활용해 애플리케이션 성능을 최적화하는 방법을 알아보겠습니다.


Django ORM을 사용할 때 흔히 저지르는 실수 중 하나는 데이터베이스에서 객체를 가져와 Python 코드에서 처리한 후 다시 저장하는 패턴입니다. 

 

이러한 방식은 다음과 같은 문제를 일으킬 수 있습니다

 

  • Race condition: 동시에 여러 요청이 들어올 경우 데이터 일관성 저하
  • 불필요한 쿼리: 같은 로직을 반복 실행할 때 중복 쿼리 발생
  • 성능 저하: Python 런타임에서 처리 시 DB 엔진의 최적화 이점 상실
  • F() 표현식과 Q() 객체는 이러한 문제를 해결하고 ORM을 더 효율적으로 사용할 수 있게 해주는 도구입니다.

F(), Q()를 학습하기전! Django ORM에 대해서 조금 더 자세하게 설명한 글은 아래 링크를 읽으시면됩니다.

 

2025.04.14 - [프로그래밍/Django] - Django ORM 완벽 가이드: 기본부터 심화까지 Django-ORM CheatSheet!!

 

Django ORM 완벽 가이드: 기본부터 심화까지 Django-ORM CheatSheet!!

Django 웹 프레임워크를 사용하다 보면 반드시 마주치게 되는 것이 ORM(Object-Relational Mapping)입니다. Django ORM은 데이터베이스 테이블을 Python 클래스로, 레코드를 객체로, 필드를 속성으로 매핑하여 S

dentuniverse.tistory.com


F() 표현식 마스터하기

F() 표현식은 Python 메모리가 아닌 데이터베이스 수준에서 필드 값을 참조하고 연산할 수 있게 해주는 도구입니다. 

아래와 같은 코드가 있습니다. 물론 아래와 같은 방식으로해도 count의 갯수는 증가가 되어 DB에 저장이 됩니다.

    article = Article.objects.get(id=article_id)  # DB에서 조회수 10인 게시글 가져옴
    article.views_count += 1  # Python 메모리에서 11로 증가
    article.save()  # DB에 저장

 

그러나!!

 

만약 두 사용자가 거의 동시에 접속한다면?!?!

사용자 A: 조회수 "10"인 게시글 로드
사용자 B: 거의 같은 시간에 조회수 "10"인 게시글 로드
사용자 A: 조회수 "11"로 저장
사용자 B: 조회수 "11"로 저장

 

결과적으로 두 번의 조회가 있었지만 조회수는 11로 한 번만 증가했습니다. 이는 데이터 일관성을 해치는 심각한 문제입니다. 즉 Race Condition(공유(공통) 자원을 둘 이상의 스레드 혹은 프로세스가 읽거나 쓰면서 결과값이 의도와 달라질 수 있는 문제)이 발생하게 되죠!

 

교착상태를 피하기 위해 우린 아래와 같이 코드를 좀 수정할겁니다.

Article.objects.filter(id=article_id).update(views_count=F('views_count') + 1)

 

위 코드는 SQL 수준에서 다음과 같이 변환됩니다

UPDATE article SET views_count = views_count + 1 WHERE id = [article_id];

 

즉, F() 표현식은 Python 메모리가 아닌 데이터베이스 수준에서 필드 값을 참조하고 연산할 수 있게 해주는 도구입니다. 이는 다음과 같은 상황에서 특히 유용합니다

  • 필드 값 업데이트
  • 필드 간 비교
  • 여러 레코드에 동일한 연산 적용
  • Race condition 방지

F()를 응용하게 되면 다음과 같은 이점도 있습니다.

# 안티 패턴: 각 제품마다 개별 쿼리 발생 (1000개 제품 = 1000번의 쿼리)
for product in Product.objects.all():
    product.price += 100
    product.save()

# 최적화된 방식: 단일 쿼리로 모든 제품 업데이트
Product.objects.update(price=F('price') + 100)

 

첫 번째 방식은 각 제품마다 SELECT와 UPDATE 쿼리가 발생하지만, F() 표현식을 사용하면 단 하나의 UPDATE 쿼리만 실행됩니다.

 

즉, 위 시나리오로 예상해보면 1000개의 제품의 가격을 100원 인상했을때입니다.

F() 표현식 없이

  • 1000번의 SELECT + 1000번의 UPDATE
  • 데이터베이스 부하: 높음

F() 표현식 사용

  • 1번의 UPDATE
  • 데이터베이스 부하: 낮음

F() 표현식과 정렬


F() 표현식은 정렬에도 활용할 수 있습니다.

이 코드는 데이터베이스 엔진이 제공하는 NULL 처리 기능을 활용하여 별도의 필터링 없이 효율적으로 정렬합니다.

# 마지막 접속일 기준 정렬, NULL 값은 마지막에 배치
User.objects.order_by(F('last_login').desc(nulls_last=True))

복잡한 유효성 검사가 필요한 경우

F() 표현식만으로는 조건부 유효성 검사가 어려울 수 있습니다. 이런 경우 select_for_update()와 트랜잭션을 함께 사용하세요.

 

예를 들어 아래와 같은 상황이라고 가정해보죠!!

 

"잔액이 충분할 때만 인출"과 같은 로직은 F()만으로는 처리할 수 없습니다. 

동시 접근을 하였을때, 계좌에서 값을 찾아오는 부분이 동시접근으로 인해 연산이 바뀔 수 있습니다.

def withdraw(account_id, amount):
    account = Account.objects.get(id=account_id)
    
    # 잔액 확인
    if account.balance < amount:
        raise InsufficientFunds("잔액이 부족합니다")
    
    # 안전하게 잔액 차감
    account.balance = F('balance') - amount
    account.save()
    
    return account

 

그렇기 때문에 아래와 같이 코드를 수정해줍니다.

@transaction.atomic
def withdraw(account_id, amount):
    # 락을 걸고 계좌 정보 가져오기
    account = Account.objects.select_for_update().get(id=account_id)
    
    # 잔액 확인
    if account.balance < amount:
        raise InsufficientFunds("잔액이 부족합니다")
    
    # 안전하게 잔액 차감
    account.balance = F('balance') - amount
    account.save()
    
    return account

 

🤔🤔🤔🤔 select_for_update()가 필요한 이유 🤔🤔🤔🤔


데이터베이스 행 수준 잠금

 select_for_update 메서드는 해당 레코드에 대해 "FOR UPDATE" SQL 구문을 사용하여 DB 수준의 잠금을 걸어줍니다. 다른 트랜잭션이 동시에 같은 레코드를 수정하는 것을 방지합니다.


F() 표현식의 한계 보완

 F() 표현식은 단순 업데이트에 유용하지만 "업데이트 전에 조건 확인"이 필요한 경우에는 불충분합니다. 예를 들어, "잔액이 충분할 때만 인출"과 같은 로직은 F()만으로는 처리할 수 없습니다.


트랜잭션 일관성 보장

 select_for_update()와 transaction.atomic()을 함께 사용하면, 조회부터 업데이트까지의 전체 과정이 원자적(atomic)으로 실행됩니다.


실제 작동 방식
1️⃣ 트랜잭션 시작 -> 2️⃣ 레코드 조회 시 DB에 행 잠금을 설정 (다른 트랜잭션은 대기) -> 3️⃣ 유효성 검사 및 업데이트 작업 수행 -> 4️⃣ 트랜잭션 종료와 함께 잠금 해제

 

주의할 점은 교착상태(deadlock) 발생 가능성이 있으므로 잠금 시간 최소화


Q() 객체로 복잡한 쿼리 작성하기

Q() 객체란?

Q() 객체는 ORM에서 복잡한 쿼리 조건을 표현하기 위한 도구입니다. 특히 OR 조건이나 복잡한 조건부 필터링이 필요할 때 유용합니다.

 

아래 코드를 보고 기본 사용법을 다시 익혀보죠

from django.db.models import Q

# OR 조건 | 을 사용
articles = Article.objects.filter(
    Q(title__icontains='Django') | Q(content__icontains='ORM')
)

# AND 조건 & 을 사용
articles = Article.objects.filter(
    Q(title__icontains='Django') & Q(is_published=True)
)

# NOT 조건은 ~ 을 사용
articles = Article.objects.filter(~Q(author__username='admin'))

 

Q() 객체의 진정한 강점은 복잡한 조건을 조합할 수 있다는 점입니다

# 복잡한 쿼리 조건
articles = Article.objects.filter(
    (Q(title__icontains='Django') | Q(title__icontains='Python')) &  # 제목에 'Django' 또는 'Python' 포함
    Q(created__year=2025) &  # 2025년에 작성됨
    ~Q(author__username='admin')  # 작성자가 'admin'이 아님
)

 

이렇게 복잡한 조건도 단일 SQL 쿼리로 변환되어 실행됩니다.

 

동적 쿼리 구성하기
Q() 객체는 동적 쿼리 구성에도 매우 유용합니다.

def search_articles(request):
    query = Q()
    
    # 검색어가 있으면 OR 조건 추가
    search_term = request.GET.get('search', '')
    if search_term:
        query |= Q(title__icontains=search_term)
        query |= Q(content__icontains=search_term)
    
    # 카테고리 필터가 있으면 AND 조건 추가
    category = request.GET.get('category', '')
    if category:
        query &= Q(category__name=category)
    
    # 최종 쿼리 실행
    articles = Article.objects.filter(query)
    return render(request, 'search_results.html', {'articles': articles})

주의사항 & 한계점

F() 표현식으로 업데이트된 값 즉시 사용

  • F() 표현식으로 필드를 업데이트해도 이미 메모리에 로드된 객체는 업데이트되지 않습니다.
article = Article.objects.get(id=1)  # views_count = 10
Article.objects.filter(id=1).update(views_count=F('views_count') + 1)

# 주의: article.views_count는 여전히 10입니다!
print(article.views_count)  # 10 출력

# 최신 값을 얻으려면 다시 로드해야 합니다
article.refresh_from_db()
print(article.views_count)  # 11 출력

마무리


Django ORM의 F() 표현식과 Q() 객체는 성능 최적화와 코드 품질 향상에 큰 도움이 됩니다. 이 기능들을 활용하면 아래와 같은 이점을 얻어갈 수 있어요.

  • Race condition을 방지하여 데이터 일관성 확보
  • 불필요한 쿼리를 줄여 성능 향상
  • 복잡한 쿼리 조건을 간결하게 표현
  • DB 엔진의 최적화 기능을 최대한 활용
반응형
LIST