[Two Scoops of Django] 7장 쿼리와 데이터베이스 레이어

guava·2021년 9월 30일
0

Two Scoops of Django

목록 보기
7/12
post-thumbnail

Two Scoops of Django 3.x를 읽고 정리한 글입니다.

  • 우리가 작성하는 대부분의 쿼리는 간단하다. 장고 ORM은 이러한 일반적인 사례에 대해 적절한 SQL을 생성해준다.
  • 여러 데이터베이스 엔진에서 작동하는 코드를 간단하게 작성할 수 있다.
  • 다른 ORM과 마찬가지로 다양한 유형의 데이터를 MySQL, PostgreSQL등 지원되는 DB에서 일관되게 사용 가능한 객체로 변환한다.
  • 그럼에도 몇가지 단점이 존재하며 이것을 이해하는것도 Django를 공부하는 것의 일부다.

7.1 단일 객체에서 get_object_or_404() 이용하기

  • 단일 객체를 가져오는 작업 등은 get()대신에 get_object_or_404를 활용하도록 하자.Model.objects.get(pk=1)get_object_or_404(Model, pk=1)
  • get_object_or_404()는 뷰에서만 사용하자. (model, form, helper function 등 view와 관련된 곳이 아니라면 사용하지 말자) 특정 데이터를 삭제했을 때 앱이 중단될 수 있다.

7.2 예외를 발생할 수 있는 쿼리 대응

  • get_object_or_404()를 이용할 때는 try-except블록으로 예외처리를 할 필요는 없다.
  • 예외를 처리해야할 경우 몇가지 팁을 나열해보겠다.

7.2.1 ObjectDoesNotExist와 DoesNotExist

  • 쿼리가 하나의 객체를 반환하지 못했을 때 발생하는 예외를 처리해준다.
  • ObjectDoesNotExist는 어떤 모델 객체에도 이용가능하지만 DoesNotExist는 특정 모델에 대해서만 동작한다.
# Example Use for ObjectDoesNotExist
from django.core.exceptions  import ObjectDoesNotExist 

from flavors.models import Flavor
from store.exceptions import OutOfStock

def list_flavor_line_item(sku): 
    try:
        return Flavor.objects.get(sku=sku, quantity__gt=0) 
    except Flavor.DoesNotExist:  # Flavor모델에 대해서만 예외처리
        msg = 'We are out of {0}'.format(sku) 
    raise OutOfStock(msg)

def list_any_line_item(model, sku): 
    try:
        return model.objects.get(sku=sku, quantity__gt=0)
    except ObjectDoesNotExist: # 어떤 모델에서 예외가 발생하더라도 잡아준다
        msg = 'We are out of {0}'.format(sku) 
        raise OutOfStock(msg)

7.2.2 여러 개의 객체가 반환되었을 때

  • 쿼리가 한개를 초과하는 객체를 반환할 수 있다면 MultipleObjectsReturned예외를 활용한다.
from flavors.models import Flavor
from store.exceptions import OutOfStock, CorruptedDatabase

def list_flavor_line_item(sku): 
    try:
        return Flavor.objects.get(sku=sku, quantity__gt=0) 
    except Flavor.DoesNotExist:
        msg = 'We are out of {}'.format(sku)
        raise OutOfStock(msg)
    except Flavor.MultipleObjectsReturned:
        msg = 'Multiple items have SKU {}. Please fix!'.format(sku) 
        raise CorruptedDatabase(msg)

7.3 쿼리를 읽기 쉽게 만들기 위해 지연 연산 이용하기

복잡한 쿼리를 짧은 코드에 너무 많은 기능을 엮어서 작성하지 말자

읽기 어려운 쿼리

# Don't do this!
from django.db.models import Q

from promos.models import Promo

def fun_function(name=None):
    """Find working ice cream promo"""
    # 너무 길게 작성된 쿼리 체인이 화면이나 페이지를 넘겨버리게 되므로 좋지 않다
    Promo.objects.active().filter(Q(name__startswith=name)|Q(description__icontain # ....

읽기 쉬운 쿼리

읽기 쉽게 작성하자

  • 지연 연산(lazy evaluation)을 이용해 좀더 깔끔하게 만들 수 있다.
  • 데이터가 필요하기 전까지 장고는 SQL을 호출하지 않는다.
  • 한 줄에 여러 메서드, 데이터베이스의 각종 기능을 엮는게 아니라 여러 줄에 나눠 쓸 수 있게 한다.
  • 이는 가독성을 향상시키며 유지보수를 쉽게 해 준다.
  • 여러 줄로 분리함으로써 코드에 주석을 좀 더 쉽게 달 수 있는 효과도 생긴다.
# Do this!
from django.db.models import Q
from promos.models import Promo

def fun_function(name=None):
    """Find working ice cream promo""" 
    results = Promo.objects.active() 
    results = results.filter(
                Q(name__startswith=name) |
                Q(description__icontains=name)
            )
    results = results.exclude(status='melted') # 주석을 달 수 있다!
    results = results.select_related('flavors')  # 줄마다 의미를 파악하지 좋다
    return results

7.4 고급 쿼리 도구 이용하기

  • 데이터를 호출한 후에 또다시 파이썬을 이용해 가공하는 것이 옳은 일인가? 하는 문제에 봉착한다.
  • 반대로 장고의 고급 쿼리 도구들을 이용해 데이터베이스를 통한 데이터 가공을 시도해볼 수 있다.
  • 이것은 성능을 향상시키고 Python기반 해결 방법보다 더 검증된 코드(지속적으로 테스트되는 Django 및 데이터베이스)를 사용하게끔 한다.

7.4.1 쿼리 표현식 (Query Expressions)

  • 데이터베이스에서 읽기를 수행할 때 쿼리 표현식을 사용하여 해당 읽기가 실행되는 동안 값이나 계산을 수행할 수 있다. 말이 더 복잡하기 때문에 코드를 통해 이해하자.
  • 다음은 평균 한 스쿱 이상을 주문한 모든 고객의 명단을 불러오는 코드다.

쿼리 표현식 미활용

# Don't do this!
from models.customers import Customer

customers = []
for customer in Customer.objects.iterator(): # 루프를 돌며 고객 레코드 하나하나에 접근한다.
    if customer.scoops_ordered > customer.store_visits: 
        customers.append(customer)

앞의 예제의 문제점

  • 데이터베이스 안의 모든 고객 레코드에 대해 하나하나 파이썬을 이용한 루프가 돌고 있다. 느리며 메모리를 많이 소모한다.
  • 코드 자체가 경합 상황(race condition)에 직면한다. READ가 아닌 UPDATE에서는 데이터 분실이 생길 수 있다.

경합 상황(race condition): 공유 자원에 대해 여러 개의 프로세스가 동시에 접근을 시도하는 상태

쿼리 표현식 활용

from django.db.models import F
from models.customers import Customer

customers = Customer.objects.filter(scoops_ordered__gt=F('store_visits'))
# Query Expression Rendered as SQL
# SELECT * from customers_customer where scoops_ordered > store_visits
  • 쿼리 표현식을 활용하면 코드의 경합 상황을 피하고 속도도 개선된다.

7.5. 로우 SQL의 사용은 피하자

  • ORM(Object-Relational Model)은 높은 생산성을 제공한다.
  • ORM은 우리가 처리하는 다양한 환경에서의 단순한 쿼리 대응에 충분하다. (생각보다 우리가 쓰는 대부분 쿼리는 단순하다)
  • ORM은 모델 업데이트 및 접근 시 유효성 검사와 보안까지 제공한다.
  • 로우 SQL은 앱의 이식성을 떨어뜨린다.
  • 다른 환경의 데이터베이스로 마이그레이션 할 경우 복잡한 문제가 대두될 수 있다.

어떤 경우에 로우 SQL을 써야할까?

  • 로우 SQL을 이용함으로써 파이썬 코드나 ORM의 코드가 월등히 간결해지고 단축될 때
  • 큰 데이터 셋에 적용되는 여러 QuerySet작업을 연결할 경우 로우 SQL이 더 효율적일 수 있다.

7.6 필요에 따라 인덱스 추가

처음에는 인덱스 없이 시작하고 필요에 따라 추가해나간다.

인덱스를 추가해야 할 때

  • 인덱스가 모든 쿼리의 10-25%정도로 자주 사용될 때
  • 실제 데이터 또는 실제와 비슷한 데이터가 존재해서 인덱싱 결과에 대한 분석이 가능할 때
  • 인덱싱을 통해 성능이 향상되는지 테스트할 수 있을 때

Chapter 26: Finding and Reducing Bottlenecks에서 인덱스 분석에 대한 내용을 참고하자

7.7 트랜잭션

  • 장고는 기본적으로 ORM이 모든 쿼리를 호출할 때마다 커밋을 한다.
  • 이로인해 데이터베이스 충돌이 발생할 수 있으며 이를 해결하기 위해 트랜잭션을 이용한다.
  • 트랜잭션이란 둘 또는 그 이상의 업데이트를 단일화된 작업으로 처리하는 기법이다. (하나의 수정작업이 실패하면 트랜잭션의 모든 업데이트가 실패 이전 상태로 복구된다)
  • 장고는 이용하기 쉬운 트랜잭션 매커니즘을 제공한다.

7.7.1 각각의 HTTP요청을 트랜잭션으로 처리하기

# settings/base.py
DATABASES = {
    'default': {
        # ...
        'ATOMIC_REQUESTS': True, 
    },
}
  • ATOMIC_REQUESTS 설정을 통해 모든 웹 요청을 트랜잭션으로 쉽게 처리할 수 있다
    • 이 설정은 뷰에서의 모든 데이터베이스 쿼리가 보호되는 안정성을 얻을 수 있다.
    • 성능 저하를 가져올 수 있다.
  • 특정 뷰에서는 설정을 제외하고싶다면 transaction.non_atomic_requests()로 데코레이팅하는 선택을 고려해야 한다.

Simple Non-Atomic View

# flavors/views.py

from django.db import transaction
from django.http import HttpResponse
from django.shortcuts import get_object_or_404 
from django.utils import timezone

from .models import Flavor

@transaction.non_atomic_requests
def posting_flavor_status(request, pk, status):
    flavor = get_object_or_404(Flavor, pk=pk)

    # 여기서 오토커밋 모드가 실행될 것이다. (장고 기본 설정)
    flavor.latest_status_change_attempt = timezone.now()
    flavor.save()

    with transaction.atomic():
        # 이 코드는 트랜잭션 안에서 실행된다.
        flavor.status = status 
        flavor.latest_status_change_success = timezone.now() 
        flavor.save()

    return HttpResponse('Hooray')
    # If the transaction fails, return the appropriate status
    return HttpResponse('Sadness', status_code=400)

7.7.2 명시적 트랜잭션 선언

  • 사이트 성능을 개선하는 방법 중 하나이다.
  • 트랜잭션에서 어떤 뷰와 비즈니스 로직이 하나로 엮여 있는지 명시
  • 개발할 때 더 많은 시간을 요구한다
목적ORM 메서드트랜잭션을 이용할 것인가?
데이터 생성.create(), .bulk_create(), .get_or_create()O
데이터 가져오기.get(), .filter(), .count(), .it- erate(), .exists(), .exclude(), .in_bulk, etc.X
데이터 수정하기.update()O
데이터 지우기.delete()O

7.7.3 django.http.StreamingHttpResponse와 트랜잭션

  • 뷰가 django.http.StreamingHttpResponse를 반환한다면 일단 응답이 시작된 이상 트랜잭션 에러를 중간에 처리하기란 불가능하다.

StreamingHttpResponse에서 트랜잭션 에러를 처리하려면? 다음 중 하나를 고려하자

  • ATOMIC_REQUESTS=False로 설정, 7.7.2의 기술을 고려
  • 뷰를 django.db.transaction.non_atomic_requests 데코레이터로 감싸본다

트랜잭션은 뷰에서만 적용된다. 스트림 응답이 SQL쿼리를 생성했다면? 오토커밋으로 동작한다.

7.7.4 MySQL에서의 트랜잭션

MySQL을 사용한다면 table type을 확인하자

  • InnoDB : 트랜잭션 지원
  • MyISAM: 트랜잭션 미지원

7.8 요약

이 장에서는 프로젝트 데이터를 쿼리하는 여러 방법을 알아보았다.

0개의 댓글