Django를 쓰면서 좋았던 점 몇 가지 더
원문은 Julia Evans님이 에 게재했습니다. 이 블로그 구독하기
안녕하세요! 요즘 좀 재미있는 도전을 하고 있는데요, SQL 데이터베이스를 두고 백엔드에서 HTML을 렌더링하는, 이른바 2010년 스타일로 웹사이트 만드는 법을 배우고 있습니다.
이 방식이 꼭 “쉽게” 느껴지지는 않아서 꽤 흥미로운 여정이에요. 2000년대나 2010년대에 이런 식으로 웹사이트 만드는 법을 배운 적이 없어서 배워야 할 게 많거든요.
그래서 이번 글에서는 이런 사이트를 만들 때 Go 표준 라이브러리나 Flask로 시도하다가 실패했던 때보다 훨씬 더 실현 가능하게 느껴지도록 해준 Django 기능들을 소개하고, Django를 쓰면서 겪은 몇 가지 문제점도 얘기해보려고 합니다.
왜 하필 2010년 방식으로 웹사이트를 배우려 할까?
이전까지 제가 자신 있게 쓸 수 있다고 느꼈던 웹사이트 제작 툴킷은 이런 것들이었습니다:
- 정적 사이트 생성기 (이 블로그 같은)
- JavaScript로 재미있는 기능을 넣은 정적 사이트 (이 sql playground 같은)
- Lambda나 Go를 백엔드로 쓰는 심플한 Vue.js 싱글 페이지 앱 (mess with dns 같은)
이런 아주 단순한 애플리케이션에는 프론트엔드 위주의 접근 방식이 정말 마음에 들었어요. 하지만 말 그대로 페이지가 하나가 아니라 여러 페이지가 필요한 무언가를 만들 생각을 하니, 프론트엔드 코드가 많이 들어가는 선택지들은 그다지 끌리지 않더라고요. 그래서 백엔드를 한번 시도해보기로 했습니다.
JS를 최대한 적게 쓰는 백엔드 중심 사이트를 만드는 것과, 백엔드 작업을 최대한 적게 하는 싱글 페이지 JS 웹사이트를 만드는 것은 서로 정반대처럼 보일 수도 있지만, 제게는 어떤 면에서 같은 느낌이에요. 두 경우 모두 로직을 최대한 한곳에 모아두려는 시도니까요.
이제 Django에 대한 생각들을 좀 나눠볼게요!
쿼리 빌더가 마음에 들어요
Django에서는 쿼리를 만들 때 쓰고 싶은 다양한 WHERE 조건별로 메서드를 잔뜩 넣어 “쿼리셋” 클래스를 정의할 수 있다는 걸 알게 됐습니다:
각 메서드가 무슨 역할을 하는지 정의해두고 나면, 뷰 코드에서는 이렇게 사용합니다:
Events.objects.approved()
.for_tab(tab)
.with_festivals(tab_params.festival_slugs)
.is_free(tab_params.free)
.is_outdoors(tab_params.outdoors)메서드는 이렇게 정의하고요:
class EventQuerySet(SearchableQuerySetMixin, models.QuerySet):
def approved(self):
return self.filter(approved_at__isnull=False)
def future(self):
today = timezone.localdate()
return self.filter(end__gt=self._midnight(today))
def with_tags(self, tags):
if tags:
return self.filter(tags__name__in=tags).distinct()
return self필터를 정의하는 문법 자체는 제 취향은 아니지만, 대부분은 그냥 정의해둔 메서드를 호출하는 데 시간을 쓰게 되고, 정말 읽기 쉽고 쓰기 편해서 앞으로 다른 쿼리 빌더 라이브러리들도 찾아보고 싶어졌습니다. 예전에는 “SQL을 아는데 쿼리 빌더가 왜 필요해?”라고 생각했는데, 이런 구조는 확실히 읽기에 좋더라고요.
Python으로 직접 작은 쿼리 빌더를 만든 예시도 찾았는데, 이런 걸 더 미니멀하게 쓰는 게 좋을지 고민해보려고 나중에 읽어보려 합니다.
템플릿 필터가 정말 좋아요
Django 템플릿에는 HTML을 생성할 때 아주 유용한 자잘한 삶의 질을 높여주는 Django 템플릿에서 사용할 수 있는 필터들이 많이 있습니다. 지금까지 제가 써본 것들은 다음과 같습니다:
- 일반 텍스트 URL을 링크로 바꾸거나 줄바꿈을
<br>로 바꾸는 것 ({{ event.description|urlize|linebreaksbr }}) - 날짜 포맷팅 (
{{ row.date|date:"M j" }}) json_script— Python 딕셔너리를 받아 자동으로 JSON으로 변환해 안전한 방식으로 HTML 안에<script>태그로 삽입해주는 필터
하나하나는 작은 기능들이지만, 그냥 쓸 수 있다는 것만으로도 어쩐지 큰 차이가 느껴집니다.
querystring이 멋져요
제가 가장 좋아하는 템플릿 필터는 querystring인 것 같아요. 이 사이트에서는 ?date=2026-06-01 같은 필터로 표시할 내용을 결정할 때가 있는데, querystring은 같은 쿼리 스트링에서 하나만 바꿔 링크를 만들어줍니다. 예를 들어 이전 날짜로 링크를 걸 때 이렇게 쓰고요:
<a href="{% querystring date=nav.prev_date%}">또는 outdoors 파라미터를 제거할 때는 이렇게 합니다:
<a href="{% querystring outdoors=None %}">자동 데이터베이스 마이그레이션은 여전히 훌륭해요
Django의 자동 데이터베이스 시스템은 정말 여전히 사랑스럽습니다. 모델을 그냥 수정해서 새 필드를 추가하거나 하면 Django가 자동으로 마이그레이션을 만들어주니 정말 놀라워요.
지금까지 데이터베이스 마이그레이션을 19번 했고, 앞으로도 더 늘어날 것 같아요! 문제에 대한 이해가 바뀌어감에 따라 데이터베이스를 쉽게 바꿀 수 있다는 점이 제게는 정말 큰 도움이 됩니다.
상속으로 코드를 정리하고 싶지 않아요
Django 문서에서는 뷰 코드를 정리할 때 클래스 기반 뷰와 상속을 쓰는 방법을 종종 제시합니다. 예를 들어 저는 코드가 많이 겹치는 뷰가 네 개 있는데, 부모 클래스를 하나 정의하고 다른 뷰들이 이를 상속받게 해서 상속으로 관리할 수도 있죠.
한번 써봤는데, 뷰 간에 코드를 공유하려고 상속을 쓰는 경험이 즐겁지 않더라고요. 그래서 이 글에서 주장하는 방식 비슷하게 함수 기반으로 바꿨는데, 훨씬 더 직관적이었습니다. Python에서 상속을 써서 좋았던 경험이 한 번도 없고, 앞으로도 다시 쓸 것 같지는 않아요.
다만 Django 자체가 제공하는 인터페이스를 쓰기 위해 상속을 사용하는 건 괜찮아요. 예를 들어 쿼리셋을 정의하려면 class EventQuerySet(SearchableQuerySetMixin, models.QuerySet) 같은 코드를 써야 하는데, 깊게 생각하지 않고 써도 잘 동작하는 것 같습니다.
(메타 코멘트: 저는 요즘 프로그래밍에 대한 의견을 말할 때 “THING은 제게 그다지 좋게 느껴지지 않고, 대신 OTHER THING을 선호한다”는 식으로 표현하려고 노력하고 있습니다. 제가 링크한 글은 함수 기반 뷰가 “올바른 방식”이라고 말하지만, 그게 정말 “올바른”지는 크게 중요하게 생각하지 않아요. 다만 상속에 대해 저와 비슷하게 느끼는 사람들이 있다는 걸 아는 것만으로도 위안이 됩니다)
Django 성능을 어떻게 생각해야 할지 모르겠어요
어느 시점에 LLM 스크래퍼들이 우리 사이트를 발견해서 초당 10개 정도의 요청을 보내기 시작했습니다. 일단은 차단해둔 상태라 지금은 괜찮지만, 사이트가 어느 정도까지 버틸 수 있는지 생각해보게 됐어요. 저는 Go 백엔드를 작성하는 데 익숙한데, 거기서는 성능 상황이 꽤 단순하거든요(보통 그냥 다 충분히 빠르니까요). Django 사이트는 그와는 매우 다릅니다.
가벼운 부하 테스트(ab -n 1000 -c 1)를 해보니 지금은 초당 2~3개 정도의 요청을 처리할 수 있습니다(월 약 10달러짜리 VM에서).
느린 부분을 찾아 더 빠르게 만들려고 프로파일링을 잔뜩 하면서 토끼굴에 빠지고 싶은 유혹이 생깁니다(py-spy가 그런 용도로 있고, 정말 훌륭하고 사용하기도 엄청 쉬우며, 프로파일링 자체도 재미있거든요!) 하지만 Django 사이트에서 성능 면으로 뭘 기대해야 하고 더 큰 관점에서 어떻게 생각해야 하는지는 정말 잘 모르겠습니다.
아직 답을 못 찾은 것들은 다음과 같습니다:
- 간헐적으로 트래픽이 몰리는 사이트라면 스케일 업을 할 수 있어야 할까?
- 더 많은 것을 캐시할 수 있도록 사이트를 설계해야 할까? (정말 그래야 할까? 캐시는 제대로 구현하기가 너무 귀찮은데!)
- Django 성능 문서에서는 템플릿 엔진으로 Jinja가 더 빠르다고 하는데, 템플릿 시스템을 바꾸는 걸 고민해야 할까?
- 그 문서에서는 또 “{% block %}이 {% include %}를 쓰는 것보다 빠르다”고 하는데, 차이가 얼마나 큰지, 크다면 왜 그런지 궁금하다
템플릿 캐싱이 중요할지도 몰라요
Django에 대해 배우면서 느끼는 것 중 하나는, Django가 프레임워크(™)이기 때문에 실수로 설정을 잘못하기 쉽다는 점입니다. 예를 들어, 얼마 전 사이트가 왜 느린지 고민하다가 Django 성능 문서를 읽었는데, 이런 문장을 발견했습니다:
캐시된 템플릿 로더를 활성화하면 템플릿을 렌더링할 때마다 매번 컴파일하는 과정을 피할 수 있어 성능이 대폭 향상되는 경우가 많습니다.
CPU 프로파일링을 했을 때 템플릿 렌더링에 시간이 많이 쓰이고 있다는 걸 발견했었거든요! 이게 도움이 될지도 모르겠네요!
링크를 따라가 보니 캐시된 템플릿 로더는 기본적으로 켜져 있어야 하는데, 제가 다른 작업을 하다가 실수로 꺼버렸더라고요. 이 “기본으로 켜져 있어야 할 캐시된 템플릿 로더를 꺼버린” 일은 Django 설정 파일이 아직도 꽤 헷갈리고 어렵게 느껴지는 이유를 보여주는 예시인 것 같아요. 앞으로는 설정 파일을 건드릴 때 좀 더 조심해야겠습니다.
템플릿 캐싱을 켜고 나니 이제는 CPU를 다 쓰지 않고도 초당 12개 정도의 요청을 꽤 쉽게 처리할 수 있게 된 것 같습니다. 전후를 꼼꼼하게 벤치마크해보진 않았지만 꽤 큰 차이가 난 것 같아요.
Django 성능에 대해 놀라웠던 점 중 하나는, 저는 항상 “성능 문제가 있으면 데이터베이스 쿼리를 확인해보세요! 인덱스를 추가해보세요!”라는 조언을 들어왔다는 겁니다. 하지만 제가 겪은 여러 성능 문제(이번 템플릿 캐싱 같은)는 느린 쿼리 때문이 아니었고, 그래서 지금까지는 일단 CPU 프로파일을 돌려보는 것부터 시작하는 게 더 유용했습니다. 그리고 저는 SQLite를 쓰고 있어서 느린 데이터베이스 쿼리 문제도 어차피 CPU 프로파일에 나타나니까요.
어쨌든 사이트 성능 얘기에 너무 깊게 들어가고 싶지는 않아요. 말했듯이 저는 프로파일링에 쉽게 흥미를 느끼는 편이지만, 사실 프로파일링에 대해서는 이미 많이 알고 있고 지금 제게 가장 중요한 배움은 아니거든요.
일단 여기까지!
Django를 쓰면서 좋았던 점(혹은 어려웠던 점!)에 대해서는 나중에 더 얘기할지도 모르겠어요. 요즘은 짧은 글을 써보려고 노력 중입니다.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기