장고 중급 #3 Signals와 Middleware
장고에는 모델,뷰의 정상 흐름 바깥에서 일어나는 코드를 끼워넣는 도구가 두 종류 있습니다.
- Signals — “어떤 일이 일어났을 때” 반응하는 이벤트 시스템
- Middleware — 모든 요청/응답이 거쳐가는 파이프라인
둘 다 강력하지만 남용하면 디버깅 지옥으로 가는 도구입니다. 이번 글은 사용법과 함께 언제 쓰지 말아야 하는지도 같이 봅니다.
#1 CBV, #2 ORM 중급에서 본 도구들이 “정상 흐름 안의 도구” 였다면, 이번 글은 그 흐름을 가로지르는 도구입니다.
Signals — 이벤트 시스템 #
장고의 시그널은 발신자 (sender) → 수신자 (receiver) 패턴입니다. 한 곳에서 “이 일이 일어났다” 를 발신하면, 등록된 수신자들이 비동기처럼 (실은 동기) 호출됩니다.
빌트인 시그널 — 자주 쓰는 것들 #
| 시그널 | 발신 시점 | 자주 쓰는 경우 |
|---|---|---|
pre_save | 모델 save() 직전 | 슬러그 자동 생성, 정규화 |
post_save | save() 직후 | 캐시 무효화, 알림 |
pre_delete | delete() 직전 | 외부 자원 정리 |
post_delete | delete() 직후 | 캐시 무효화, 로그 |
m2m_changed | M2M 관계 변경 | 권한,통계 갱신 |
request_started / request_finished | 요청 시작/끝 | 글로벌 로깅 |
user_logged_in / user_logged_out | 인증 이벤트 | 마지막 로그인 시각 갱신 |
수신자 등록 — @receiver
#
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.text import slugify
from .models import Post
@receiver(post_save, sender=Post)
def post_post_save(sender, instance, created, **kwargs):
if created:
# 새로 만든 글에 대해서만
send_notification_to_subscribers(instance)@receiver(signal, sender=Model) 데코레이터로 함수를 시그널에 묶습니다. 핸들러 시그니처:
sender— 발신자 클래스 (위 예에선Post)instance— 저장된 모델 인스턴스created—True면 새로 만든 것,False면 업데이트**kwargs— 시그널마다 추가 인자가 다를 수 있어 항상**kwargs로 받음
apps.py의 ready()에서 import
#
시그널 모듈은 앱 로딩 시점에 한 번 import 돼야 등록됩니다. apps.py가 그 일을 합니다.
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
def ready(self):
from . import signals # 등록 트리거ready() 안에서 import 하는 이유는 순환 import와 앱 로딩 순서 이슈를 피하기 위해서입니다. 모델이 다 로드된 시점에 시그널이 등록됩니다.
직접 만든 시그널 #
import django.dispatch
post_published = django.dispatch.Signal()
# 발신
@receiver(post_published)
def on_published(sender, post, **kwargs):
print(f"발행: {post.title}")from .signals import post_published
class PostUpdateView(UpdateView):
def form_valid(self, form):
response = super().form_valid(form)
if self.object.published:
post_published.send(sender=self.__class__, post=self.object)
return responseSignal()로 만든 객체에 .send(sender, **kwargs)로 발신합니다.
pre_save — 슬러그 자동 생성
#
@receiver(pre_save, sender=Post)
def post_pre_save(sender, instance, **kwargs):
if not instance.slug:
instance.slug = slugify(instance.title)저장 직전에 슬러그를 채워줍니다. 다만 이런 경우는 보통 모델의 save() 오버라이드가 더 적절합니다. 다음 절에서 설명하겠습니다.
시그널의 함정 — 언제 쓰지 말 것 #
시그널은 너무 멀리 있는 부수효과를 만들기 쉽습니다.
# 어딘가에 흩어진 수신자들...
@receiver(post_save, sender=Order)
def send_email(...): ...
@receiver(post_save, sender=Order)
def update_stats(...): ...
@receiver(post_save, sender=Order)
def call_external_api(...): ...Order.objects.create(...) 한 줄을 적었는데 어디서 무엇이 일어나는지 코드 한 줄만 봐선 알 수 없습니다. 시그널이 여러 파일에 흩어져 있기 때문입니다.
대안 1 — 모델 메소드 #
class Post(models.Model):
...
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
def publish(self):
self.published = True
self.published_at = timezone.now()
self.save()
notify_subscribers(self)post.publish() 한 줄을 보면 무슨 일이 일어나는지 그 메소드만 보면 됩니다. 시그널보다 추적이 쉽습니다.
대안 2 — 매니저 / 서비스 함수 #
def publish_post(post: Post) -> None:
post.published = True
post.published_at = timezone.now()
post.save()
notify_subscribers(post)
invalidate_homepage_cache()도메인 로직을 명시적인 함수에 모읍니다. 테스트도 쉽고, 호출 경로도 분명.
시그널이 정말 어울리는 경우 #
| 경우 | 시그널 OK? | 이유 |
|---|---|---|
다른 앱이 발신하는 모델 (장고 빌트인 User 등)에 후크 | ✅ | 그 모델을 수정 못 함 |
| 외부 라이브러리 모델에 후크 | ✅ | 같은 이유 |
| 같은 앱 안의 부수효과 | ❌ | 모델 메소드/서비스가 명시적 |
| 트랜잭션 안의 순수 영속화 작업 | ✅ | 일관성을 시그널이 보장 |
원칙: 본인 코드를 자기 손으로 고칠 수 있다면 시그널보다 명시적인 호출이 답. 시그널은 “남의 모델에 후크 거는” 도구로 보세요.
트랜잭션과 시그널을 같이 쓰는 패턴 (transaction.on_commit, post_save + atomic)은 고급 #5에서 자세히 다루겠습니다.
Middleware — 요청/응답 파이프라인 #
미들웨어는 모든 요청과 응답이 거쳐가는 파이프라인 입니다. 인증, 세션, CSRF 같은 전역 관심사가 여기에 삽니다.
동작 모델 #
브라우저
↓
SecurityMiddleware
↓
SessionMiddleware
↓
CommonMiddleware
↓
CsrfViewMiddleware
↓
AuthenticationMiddleware
↓
MessageMiddleware
↓
View ← 여기서 응답 생성
↑
MessageMiddleware
↑
... (역순으로 다시 거쳐감)
↑
브라우저들어갈 때는 위에서 아래로, 나갈 때는 아래에서 위로. 양파 껍질처럼 감쌉니다.
settings.py에 등록
#
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]순서가 중요합니다. 예를 들어 AuthenticationMiddleware는 SessionMiddleware보다 뒤에 와야 합니다 (세션을 읽어 사용자를 인증하니까).
빌트인 핵심 미들웨어 #
| 미들웨어 | 하는 일 |
|---|---|
SecurityMiddleware | HTTPS 리다이렉트, HSTS, X-Content-Type-Options 등 |
SessionMiddleware | request.session 활성화 (#5) |
CommonMiddleware | URL 정규화 (슬래시 추가), 통계 헤더 |
CsrfViewMiddleware | CSRF 토큰 검증 |
AuthenticationMiddleware | request.user 활성화 (#4) |
MessageMiddleware | flash 메시지 (#5) |
XFrameOptionsMiddleware | clickjacking 방어 (X-Frame-Options) |
빌트인은 끄지 않는 게 보통 답입니다. 보안에 직결되는 것들이 많습니다.
미들웨어 작성 — 클래스 형태 #
import time
import logging
logger = logging.getLogger(__name__)
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.perf_counter()
response = self.get_response(request)
elapsed = (time.perf_counter() - start) * 1000
response["X-Render-Time"] = f"{elapsed:.1f}ms"
logger.info(f"{request.method} {request.path} {elapsed:.1f}ms")
return response구조:
__init__(self, get_response)— 앱 시작 시 한 번만 호출.get_response는 다음 미들웨어 (또는 뷰) 호출용__call__(self, request)— 요청마다 호출self.get_response(request)앞에 코드 → 요청 들어갈 때self.get_response(request)뒤에 코드 → 응답 나갈 때
MIDDLEWARE = [
...
"myapp.middleware.TimingMiddleware",
]후크 메소드 — process_view, process_exception, process_template_response
#
추가 후크가 필요하면 메소드를 더 정의합니다.
class AuditMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
return self.get_response(request)
def process_view(self, request, view_func, view_args, view_kwargs):
# 뷰 함수가 호출되기 직전. None을 반환하면 정상 진행
request._view_func_name = view_func.__name__
def process_exception(self, request, exception):
# 뷰에서 처리 안 된 예외가 났을 때
logger.error(f"미처리 예외: {exception}", exc_info=True)
# None 반환 → 다른 핸들러에 위임 / HttpResponse → 응답으로 사용
def process_template_response(self, request, response):
# TemplateResponse 일 때 렌더 직전
return response미들웨어로 흔히 푸는 경우 #
- 요청 ID 발급 — 분산 추적용
X-Request-ID헤더 부여 - 다국어/타임존 —
Accept-Language보고 활성 언어 결정 - 유지보수 모드 — 특정 경로 외엔 503 응답
- 간단한 rate limit — 캐시 + IP 기반 (큰 트래픽엔 별도 도구)
- 공통 응답 헤더 — 보안 헤더, CORS 등
미들웨어 vs 데코레이터 vs 시그널 — 언제 무엇을 #
세 도구의 쓰임을 헷갈리기 쉽습니다.
| 도구 | 쓰임 |
|---|---|
| 데코레이터 (또는 Mixin) | 특정 뷰들에만 거는 가로지르는 관심사 (예: @login_required) |
| 미들웨어 | 모든 요청에 거는 글로벌 관심사 (예: 요청 ID, 보안 헤더) |
| 시그널 | 모델,인증 같은 도메인 이벤트 후크 (자기 코드면 명시 호출이 우선) |
판단 기준:
- 특정 뷰만 → 데코레이터 / Mixin
- 모든 요청 → 미들웨어
- 모델 저장 후처리 → 같은 앱이면 메소드/서비스, 외부 모델이면 시그널
작은 실전 예 — 활성 사용자 마지막 활동 시각 #
from django.utils import timezone
class LastSeenMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if request.user.is_authenticated:
User = request.user.__class__
User.objects.filter(pk=request.user.pk).update(
last_seen_at=timezone.now()
)
return responseUser.objects.filter(...).update(...)로 사용자 객체를 다시 save 하지 않고 업데이트합니다. (#2의 패턴.) 매 요청마다 부담을 줄이려면 5 분에 한 번만 갱신 같은 throttle을 추가하세요.
비동기 미들웨어 짧게 #
장고 4.0+ 부터 미들웨어를 비동기로도 작성할 수 있습니다. 본격적인 비동기 뷰는 고급 #1에서 다루지만 미들웨어는 동기/비동기 양쪽 호환을 만들 수 있습니다.
from asgiref.sync import iscoroutinefunction
class HybridMiddleware:
sync_capable = True
async_capable = True
def __init__(self, get_response):
self.get_response = get_response
self.async_mode = iscoroutinefunction(get_response)
def __call__(self, request):
if self.async_mode:
return self.__acall__(request)
return self.get_response(request)
async def __acall__(self, request):
return await self.get_response(request)상세 패턴은 고급 #1에서.
정리 #
이번 글에서 잡은 것:
- Signals — 발신자/수신자 이벤트 시스템
- 빌트인 시그널:
pre_save,post_save,pre_delete,post_delete,m2m_changed, 인증 이벤트 @receiver(signal, sender=Model)로 등록,apps.py의ready()에서 import- 시그널 함정 — 너무 멀리 있는 부수효과, 디버깅 어려움. 자기 모델이면 메소드/서비스가 우선
- Middleware — 요청/응답 파이프라인, 양파 껍질 흐름
- 빌트인 핵심 (
Security,Session,Csrf,Authentication,Message) - 미들웨어 형태:
__init__(get_response),__call__(request), 추가로process_view/process_exception - 세 도구의 쓰임: 특정 뷰 → 데코레이터/Mixin, 모든 요청 → 미들웨어, 도메인 이벤트 → 시그널 (자기 코드면 명시 호출이 우선)
다음 글(#4 사용자/권한)에서는 기초 #7의 빌트인 인증 위에 커스텀 user model, permission, group을 쌓습니다. 프로젝트 시작 시점에 정해야 하는 결정들도 같이 다루겠습니다.