Django 4.0 릴리스와 주요 변경 사항

이 글에서는 Django 4.0에 추가된 기능과 바뀐 점을 알아보려 합니다. Django 4.0 release notes를 참고하였습니다.

Django는 3년 마다 LTS를 위해 메이저 버전을 하나씩 올리고, 8개월마다 마이너 버전을 올립니다. 현재 LTS 버전은 3.2.x입니다. 이번에 출시한 4.0은 LTS 버전은 아닙니다.

출처: Django 공식 웹 사이트 - 다운로드 페이지

파이썬 호환성

Django 4.0은 파이썬 3.8과 3.9, 3.10을 지원합니다. 파이썬 3.6과 3.7을 지원하는 마지막 버전은 Django 3.2.x입니다.

새 기능

기본 타임존은 zoneinfo

이제부터 파이썬 기본 라이브러리인 zoneinfo가 기본 타임존입니다. Django 3.2부터 시작한 pytz를 zoneinfo로 이전하는 작업의 일환입니다. pytz 지원은 중단 예정이며 Django 5.0에서 완전히 삭제됩니다.

함수형 고유키 제약

UniqueContraint() 표현식으로 함수형 고유키를 생성할 수 있습니다. 예를 들어 온라인 상점의 영문 이름을 소문자화하여 고유키로 사용하고 싶다고 해보면 다음과 같이 선언하면 됩니다.

class Shop(models.Model):
    name = models.CharField(max_length=255)
    class Meta:
        constraints = [
            UniqueConstraint(Lower('name'),
            name='unique_name')
        ]

이렇게 선언한 후, 이름이 OnlineShop인 상점이 존재할 때 다른 상점의 이름을 onlineshop으로 등록할 수 없습니다.

scrypt 암호 해시기

PBKDF2보다 안전한 scrypt 암호 해시기를 도입했습니다. 이를 사용하려면 OpenSSL 1.1 버전 이상이 필요합니다. 또한 메모리를 더 사용하기 때문에 기본값으로 지정하진 않았습니다.

Redis 캐시 백엔드 내장

Redis 캐시를 지원하는 django.core.cache.backends.redis.RedisCache 내장 백엔드를 추가했습니다. 이를 사용하려면 redis-py 3.0.0 버전 이상이 필요합니다.

템플릿 기반의 폼 렌더링

FormFormset, ErrorList에 템플릿 기반의 렌더링 기능을 추가했습니다. Form과 Formset 렌더링에 필요한 render(), get_context(), template_name 등의 메서드와 속성도 추가했습니다.

예를 들어 다음과 같은 뷰가 있다고 가정하죠.

from shop.forms import CartForm
def cart(request):
    CartFormSet = formset_factory(CartForm)
    if request.method == 'POST':
        formset = CartFormSet(request.POST)
    else:
        formset = CartFormSet()
    return render(request, 'cart.html', {'formset': formset})

그동안 cart.html에서는 이런 식으로 폼을 렌더링했습니다.

<form method="post">
    {{ formset.management_form }}
    <table>
        {% for form in formset %}
        {{ form }}
        {% endfor %}
    </table>
</form>

이제는 뷰에서 formset.template_name 속성을 선언한 후 cart.html을 다음과 같이 줄일 수 있습니다.

<form method="post">
    <table>
        {{ formset }}
    </table>
</form>

자잘한 변경

관리자 화면(django.contrib.admin)

admin/base.html 템플릿 파일에 header 블럭을 추가했습니다. 여기에는 관리자 화면의 헤더 부분(아래 화면의 빨간 테두리 부분)이 들어갑니다.

관리자 화면의 header 부분

관리자 화면의 내비게이션 바에 필터를 추가했습니다.

내비게이션 바의 필터

django.contrib.postgres

서비스 이름으로 연결하기를 지원합니다. 예를 들어 .pg_service.conf 파일을 다음과 같이 선언해두었다고 가정합니다.

[shops_service]
host=localhost
user=superadmin
dbname=shops
port=5432

데이터베이스 접속 비밀번호는 .shops_pgpass에 저장해두었고요.

localhost:5432:shops:superadmin:PASSWORD

이제 settings.pyDATABASES 부분은 다음과 같이 선언할 수 있습니다.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'OPTIONS': {
            'service': 'shops_service',
            'passfile': '.shops_pgpass',
        },
    }
}
  • 기존 데이터는 무시하고 새 데이터에만 제약 조건을 적용하고 싶을 때 사용할 수 있도록 AddConstraintNotValid 오퍼레이터를 추가했습니다.
  • 기존 데이터가 제약 조건을 위반하는지 확인할 수 있는 ValidateConstraint를 추가했습니다.
  • QuerySet.value() 메서드의 리턴 값을 배열 처럼 접근할 수 있는 ArraySubquery 표현식을 추가했습니다. 예를 들어 온라인 상점(Shop)과 주문 목록(Order)이 서로 외래키로 연결된 상태라고 가정합시다. 다음과 깉이 한 상점의 주문 목록 중 특정한 값을 배열로 가져올 수 있습니다.
from django.contrib.postgres.expressions import ArraySubquery
orders = Order.objects.filter(shop=OuterRef('pk')).values('product_name')
shop = Shop.objects.annotate(orders=ArraySubquery(orders)).first()
shop.orders  # ['Route53', 'ELB', 'EC2']

orders = Order.objects.filter(shop=OuterRef('pk')).values(json=JSONObject(product='product_name', id='id'))
shop = Shop.objects.annotate(orders=ArraySubquery(orders)).first()
shop.orders  # [{'id': 3, 'product': 'Route53'}, {'id': 4, 'product': 'ELB'}, {'id': 5, 'product': 'EC2'}]

기타

  • 관리자 화면에서 사용하는 jQuery 버전을 3.5.1에서 3.6.0으로 변경하였습니다.
  • 인증(django.contrib.auth)
    • PBKDF2 비밀번호 해시의 반복 횟수를 260,000회에서 320,000회로 늘었습니다. (Django 3.2에서는 216,000회에서 260,000회로 늘었습니다.)
    • 로그인 후 리디렉션 페이지를 바꾸려면 LoginViewnext_page 속성과 get_default_redirect_url() 메서드를 사용할 수 있습니다.
  • 캐시
    • django.core.cache.backends.base.BaseCache가 비동기 API를 지원합니다. 비동기 메서드 앞에는 a 글자가 붙습니다. 예를 들어 add() 메서드의 비동기 버전은 aadd(), get() 메서드의 비동기 버전은 aget()입니다.
  • CSRF
    • CSRF 방어시 Origin 헤더가 있으면 이를 참고합니다. 그리고 'https://*.44bits.io' 처럼 HTTP 스킴과 * 문자도 지원합니다.
  • 관리 명령어
    • run_server 명령어에 –skip-checks 옵션을 추가했습니다.
    • (PostgreSQL을 사용할 때) dbshell 명령이 비밀번호 파일을 인식합니다.
    • shell 명령어가 sys.__interactivehook__ 값에 영향을 받습니다. 이를 통해 인터랙티브 셸 간 명령 이력을 불러옵니다.
    • startappstartproject 명령어에 템플릿을 적용할 때 --exclude 옵션을 사용하여 특정 디렉터리를 제외할 수 있습니다.
  • 모델
    • 쿼리셋에 특정 개체가 포함됐는지 확인할 수 있는 QuerySet.contains(obj) 메서드를 추가했습니다.
    • Round() 데이터베이스 함수에 정밀도를 지정할 수 있는 precision 전달인자를 추가했습니다.
    • (SQLite 3.35 이상을 사용할 때) QuerySet.bulk_create() 메서드로 개체들의 기본키를 설정할 수 있습니다.
    • QuerySet.bulk_update()가 수정된 개체 수를 리턴합니다. (Django 3.2까지는 None을 리턴했습니다.)
    • 데이터베이스 함수의 결과 값이 비었을 때 리턴할 값을 Expression.empty_result_set_value로 지정할 수 있습니다.
    • Lookup 표현식을 애너테이션과 애그리게이션, 필터 안에서 사용할 수 있습니다.
    • aggregate() 결과가 비었을 때 리턴할 값을 default 전달인자로 선언할 수 있습니다.
  • 요청과 응답
  • 템플릿
    • floatformat 템플릿 필터에서 u 접미사를 사용하여 지역화를 끌 수 있습니다.
  • 테스트
    • –buffer 옵션이 병렬 테스트도 지원합니다.
    • –shuffle 옵션을 사용하여 테스트 순서를 무작위로 섞을 수 있습니다.
    • test –parallel 옵션에 auto를 지정하면 자동으로 각 프로세서마다 테스트 프로세스를 실행합니다.

하위 호환되지 않는 기능

데이터베이스 API

  • DatabaseOperations.year_lookup_bounds_for_date_field() 메서드와 year_lookup_bounds_for_datetime_field() 메서드에 iso_year 매개변수를 선택적으로 전달할 수 있습니다. 이 매개변수는 ISO-8601 표준에 따른 주(week) 번호를 지원합니다.
  • DatabaseSchemaEditor._unique_sql() 메서드와 _create_unique_sql()의 두 번째 매개변수가 columns 대신 fields로 바뀌었습니다.

PostgreSQL 9.6 지원 중단

Oracle 12.2와 18c 지원 중단

SecurityMiddleware가 X-XSS-Protection 헤더를 설정하지 않음

마이그레이션 자동 감지 변경

마이그레이션 자동 감지기가 모델 클래스 대신 모델 스테이트를 참고합니다. 또한 ForeignKeyManyToManyField 필드에 대한 마이그레이션 파일 생성시 몇몇 속성을 특정하지 않습니다. 따라서 간혹 아무 일도 하지 않는 AlterField가 마이그레이션 파일 안에 선언되기도 합니다.

자잘한 변경

  • STATIC_URL의 기본 값이 /static/에서 static/으로 바뀌었습니다.
  • AdminSiteindex 뷰에 @never_cache 데코레이터가 기본으로 적용되지 않습니다.
  • 부분 쿼리셋이 지원하지 않는 연산에 대해 AssertionError 대신 TypeError 예외를 일으킵니다.
  • assertHTMLEqual() 테스트시 불린 속성이 아니면, 값 없이 이름만 선언한 경우와 값과 이름을 모두 선언한 경우를 같다고 간주하지 않습니다. (예를 들어 checkedchecked="checked"와 같다고 간주하지만 altalt="alt"와 같지 않습니다. Django 3.2까지는 같다고 간주했습니다.)

중단 예정인 기능

pytz 타임존

pytz 대신 zoneinfo를 권장합니다. 따라서 몇몇 시간 관련 메서드에서 사용하던 is_dst 전달인자도 중단 예정입니다.

pytz는 Django 5.0에서는 사용할 수 없습니다.

타임존 지원

Django 5.0 버전부터는USE_TZ 설정의 기본 값이 False에서 True로 바뀔 예정입니다. 사실 Django 1.4 버전부터startproject 명령어가 자동 생성하는 settings.py 파일에 USE_TZ = True가 들어 있었지만, 기본 값은 False였습니다.

지역화

USE_L10N 설정의 기본 값 역시 False에서 True로 바뀌었습니다. 아울러 USE_L10N 설정은 중단 예정입니다. Django 5.0에서는 기본으로 날짜나 숫자를 지역화합니다.

삭제된 기능

Django 3.0부터 중단 예정이었던 기능

  • django.utils.http.urlquote()urlquote_plus(), urlunquote(), urlunquote_plus()
  • django.utils.encoding.force_text()smart_text()
  • django.utils.translation.ugettext()ugettext_lazy(), ugettext_noop(), ungettext(), ungettext_lazy()
  • django.utils.http.is_safe_url()

Django 3.1부터 중단 예정이었던 기능

  • PASSWORD_RESET_TIMEOUT_DAYS 설정
  • django-admin.py (이제 django-admin만 작동합니다.)
  • HttpRequest.is_ajax()
  • 모델의 NullBooleanField 필드
  • django.conf.urls.url()
  • django.contrib.postgres.fields.JSONFielddjango.contrib.postgres.forms.JSONField
  • {% ifequal %}{% ifnotequal %} 템플릿 태그
  • DEFAULT_HASHING_ALGORITHM 설정

요약은 여기까지입니다. 개인적으론 중요하지 않아 보여서 적지 않은 내용이 여러분에겐 더 중요할 수도 있으니, Django 4.0 공식 릴리스 노트를 한 번 살펴보시길 추천합니다.

다양함을 품을 수 있는 소프트웨어 개발자가 되고 싶습니다.