Django 프로젝트에서 Middleware의 기본 구조와 동작 순서
Django의 **Middleware(미들웨어)**는 요청(request)과 응답(response)의 처리 과정에 끼어들어, 전역적으로 동작하는 플러그인 계층이라고 볼 수 있습니다. 미들웨어들은 Django 설정의 MIDDLEWARE 리스트에 정의된 순서대로 동작하며, 각각이 양파 껍질처럼 뷰(view)를 감싸는 계층으로 생각할 수 있습니다. 요청이 들어오면 MIDDLEWARE에 나열된 순서(위에서 아래)대로 각 미들웨어가 **요청 처리(request phase)**에 참여하고, 뷰가 처리된 후에는 반대로 **응답 처리(response phase)**를 거치면서 미들웨어들이 역순(아래에서 위)으로 호출됩니다. 이러한 동작 방식 때문에, 가장 위에 위치한 미들웨어가 가장 먼저 요청을 받고, 응답을 보낼 때는 가장 마지막에 처리합니다 (가장 나중에 추가된 헤더 등이 최종 응답에 반영됨).
Django 미들웨어는 함수나 클래스로 구현될 수 있으며, 기본 형태는 다음과 같습니다:
# 예시: 간단한 미들웨어 구조
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response # 다음 미들웨어 또는 뷰를 가리키는 콜러블
def __call__(self, request):
# (1) 뷰 호출 전에 실행할 코드
print("Request arrived:", request.path)
# 다음 미들웨어 또는 뷰 함수 호출
response = self.get_response(request)
# (2) 뷰 호출 후에 실행할 코드
print("Response status:", response.status_code)
return response
위 코드에서 __init__은 서버 시작 시 한 번 실행되며(get_response 콜러블을 받아 보관), __call__ 메서드가 각 요청마다 호출됩니다. __call__ 내부에서는 뷰 호출 전후에 실행할 코드를 배치할 수 있습니다. 미들웨어가 self.get_response(request)를 호출하면 다음 계층(또는 최종적으로는 뷰)이 실행되고 응답 객체가 반환됩니다. 이 응답 객체에 대해 필요한 후처리를 한 뒤 그대로 반환하면, 해당 응답이 나머지 바깥쪽 미들웨어들을 거쳐 클라이언트에게 전달됩니다.
Django의 내장 미들웨어들과 커스텀 미들웨어 모두 이러한 체인을 통해 작동합니다. 요청 단계에서는 MIDDLEWARE 리스트에 정의된 순서대로(Top-Down) 각 미들웨어의 request 처리 코드 (예: __call__ 상의 전처리나 process_request 메서드)가 실행됩니다. 응답 단계에서는 요청을 처리한 뷰가 응답을 반환한 뒤, 미들웨어들이 **역순(Bottom-Up)**으로 response 처리 코드 (예: __call__의 후처리나 process_response 메서드)를 수행합니다. 이때 하나의 미들웨어에서 요청 단계에 응답을 직접 반환하면, 안쪽(그 이후의) 미들웨어와 뷰 함수를 건너뛰고(short-circuit) 즉시 응답 단계로 넘어갑니다. 즉, 중간의 어떤 미들웨어가 응답을 만들어 리턴하면, 나머지 뷰나 다른 미들웨어는 실행되지 않고 해당 응답만 바깥쪽 미들웨어들을 거쳐 바로 반환됩니다. 이러한 구조 덕분에 미들웨어는 인증, 캐싱, 예외 처리 등의 공통 로직을 전역에서 관리할 수 있습니다.
Tip: Django는 함수형 미들웨어와 클래스형 미들웨어를 모두 지원하지만, Django 1.10+부터는 위와 같은 클래스 형태 (또는 함수형 팩토리 형태)를 사용하는 것이 표준입니다. 또한 필요에 따라 process_view, process_exception, process_template_response 등의 특별한 훅(hook) 메서드를 클래스에 정의하여 더욱 세밀하게 동작을 제어할 수도 있습니다. 예를 들어 process_view(request, view_func, *view_args, **view_kwargs)를 구현하면 뷰 함수 호출 직전에 실행되며, 특정 조건에서 미리 응답을 반환하여 뷰를 생략할 수도 있습니다.
Django REST Framework에서 Custom Middleware의 동작 방식
Django REST Framework(DRF)는 Django의 뷰를 기반으로 동작하기 때문에, 커스텀 미들웨어의 동작 방식은 기본 Django와 동일합니다. 클라이언트로부터 온 API 요청도 일반 Django 요청(HttpRequest)으로 취급되어 미들웨어 체인을 모두 통과한 후에 DRF의 뷰(예: APIView 혹은 ViewSet의 특정 액션)에 도달합니다. 마찬가지로 응답도 Django의 HttpResponse 형태로 미들웨어들을 역순으로 지나가게 됩니다.
다만 DRF는 편의를 위해 **Request**와 Response 클래스를 제공합니다. DRF의 Request는 Django의 HttpRequest를 확장한 객체로, .data 속성을 통해 JSON, XML 등 다양한 형식의 본문 데이터를 일관된 방식으로 접근할 수 있게 해줍니다. 예를 들어 request.POST는 HTML 폼 데이터에 대해서만 동작하지만, DRF의 request.data는 POST, PUT, PATCH 등 메서드의 JSON 바디까지도 파싱해줍니다. DRF의 뷰 (APIView) 내부에서는 Django의 HttpRequest 객체가 이러한 Request 객체로 **래핑(wrapping)**되어 사용됩니다. 그러나 미들웨어 단계에서는 아직 Django의 기본 HttpRequest 객체이므로, DRF의 Request 전용 속성(.data 등)은 사용할 수 없습니다.
마찬가지로, DRF의 Response 객체는 Django의 HttpResponse를 확장한 것으로, 미리 렌더링(render)되지 않은 데이터를 가지고 있다가 Content Negotiation(콘텐츠 협상)에 따라 적절한 표현(JSON/HTML 등)으로 렌더링됩니다. 중요한 것은, DRF의 Response 객체를 미들웨어에서 직접 반환하면 문제가 생긴다는 점입니다. DRF Response는 APIView 내부에서 최종적으로 Django HttpResponse로 변환되어 클라이언트에 보내지도록 설계되어 있습니다. 실제로 DRF는 뷰 함수에서 Response를 리턴하면 이를 APIView의 finalize_response 과정에서 적절한 HttpResponse로 바꾸고 헤더(Content-Type 등)를 조정한 후 반환합니다. 따라서 미들웨어에서 DRF의 Response를 그대로 반환하면 Django는 이를 제대로 처리하지 못하고 오류가 발생할 수 있습니다. 대신 미들웨어에서는 항상 Django의 표준 HttpResponse (또는 JsonResponse 등)를 반환해야 합니다. 예컨대 JSON 형태의 응답을 미들웨어에서 바로 리턴하고 싶다면 django.http.JsonResponse를 사용하는 것이 안전합니다.
또 다른 유의점은 **인증(Authentication)**과 권한(Permissions) 처리 시점입니다. Django의 세션/쿠키 기반 인증은 SessionMiddleware와 AuthenticationMiddleware가 미들웨어 단계에서 request.user 를 채워주지만, DRF에서 주로 사용하는 토큰 인증, JWT 인증 등의 경우 DRF 뷰 내부에서 인증 토큰을 해석하여 request.user를 설정합니다. 예를 들어 TokenAuthentication이나 BasicAuthentication을 사용하는 경우, 미들웨어 단계에서는 request.user가 여전히 AnonymousUser일 수 있고, APIView가 실행되는 시점에 비로소 인증 헤더를 확인하여 사용자 정보를 얻습니다. 이는 DRF의 APIView.dispatch 과정 중 Authentication 클래스들이 동작하기 때문입니다. 따라서 커스텀 미들웨어에서 request.user에 의존하는 로직을 작성할 때 주의해야 합니다. 세션 기반이 아니라 DRF의 인증 체계에만 의존하는 서비스라면, 미들웨어에서는 사용자 정보가 설정되지 않을 수 있습니다. 이런 경우 미들웨어에서 직접 헤더를 파싱하거나, 아니면 미들웨어를 사용하지 않고 DRF의 permission 클래스나 authentication 클래스로 구현하는 것이 나을 수 있습니다.
요약하면, DRF 환경에서도 미들웨어는 평소와 같은 순서로 Request/Response 흐름에 끼어들며, DRF 전용 객체들은 뷰 내부에서 처리되고 변환된다는 점만 기억하면 됩니다. DRF로 구현된 API에서도 로깅, 헤더 조작, 예외 처리 등의 공통 기능을 미들웨어로 구현하여 일관성 있게 적용할 수 있습니다.
사용 예시: API 요청 로깅, 인증 헤더 검사, 응답 헤더 삽입 등
미들웨어는 웹 전역에 적용되는 공통 기능을 넣기에 적합합니다. API 서비스 개발 시 유용한 몇 가지 미들웨어 활용 예시를 살펴보겠습니다.
- 요청/응답 로깅 미들웨어: 모든 API 요청과 그에 대한 응답 결과를 기록(log)해두면, 문제 발생 시 추적이나 모니터링에 도움이 됩니다. 미들웨어에서 요청 정보를 추출하고, 뷰 실행 후 응답까지 확보하여 한꺼번에 로깅할 수 있습니다. 예를 들어 아래는 /api/ 경로의 요청에 대해 요청 본문과 응답 본문을 JSON으로 로깅하는 미들웨어 구현입니다:위 코드에서 __call__ 메서드의 전후로 요청/응답 정보를 모아서 logger.info로 출력하고 있습니다. process_exception 훅을 구현하여 뷰에서 처리되지 않은 예외가 발생할 경우 에러 스택트레이스를 로깅하고 있습니다. 이처럼 미들웨어를 활용하면 모든 API 요청에 대한 표준화된 로깅을 한 곳에서 관리할 수 있습니다.
- import json, logging, time, socket from django.utils.deprecation import MiddlewareMixin logger = logging.getLogger(__name__) class ApiLoggingMiddleware(MiddlewareMixin): def __init__(self, get_response): self.get_response = get_response def __call__(self, request): start_time = time.monotonic() # 요청 정보 수집 log_data = { "remote_addr": request.META.get("REMOTE_ADDR"), "method": request.method, "path": request.get_full_path(), } # API 경로에 대해서 요청 본문 파싱 if request.path.startswith("/api/"): try: body_data = json.loads(request.body.decode("utf-8")) if request.body else {} except ValueError: body_data = "<non-JSON or binary data>" log_data["request_body"] = body_data # 뷰 실행 및 응답 획득 response = self.get_response(request) # 응답 정보 추가 (JSON인 경우만) if hasattr(response, "content") and response.get("Content-Type", "").startswith("application/json"): try: resp_body = json.loads(response.content.decode("utf-8")) except ValueError: resp_body = "<non-JSON or binary data>" log_data["response_body"] = resp_body log_data["status_code"] = getattr(response, "status_code", None) log_data["duration"] = time.monotonic() - start_time logger.info(f"[API] {json.dumps(log_data, ensure_ascii=False)}") return response def process_exception(self, request, exception): # 예외 발생 시 에러 로그 기록 logger.exception(f"Unhandled Exception in request: {request.path} - {exception}") # None을 리턴하여 다른 미들웨어나 기본 핸들러가 처리하게 함 return None
- 인증 헤더 검사 미들웨어: API 엔드포인트 중 특정 요청들은 커스텀 헤더나 토큰이 필요할 수 있습니다. 이를 각 뷰에서 확인하는 대신 미들웨어에서 전역으로 검사할 수 있습니다. 예를 들어, 모든 /api/private/ 경로에 대해 X-API-KEY 헤더를 요구하는 미들웨어를 작성할 수 있습니다:위 미들웨어는 요청이 사전 정의한 경로로 들어올 때 지정된 헤더 값을 검사하며, 조건을 만족하지 못하면 즉시 401 응답을 반환하여 뷰가 호출되지 않도록 합니다. 이러한 방식으로 공통 인증 요건을 중앙 관리할 수 있습니다. (DRF의 Permission이나 Authentication 클래스로도 비슷한 기능을 구현할 수 있지만, 미들웨어는 사용자 정의 헤더나 매우 전역적인 정책을 간단히 적용하는 데 유용합니다.)
- from django.http import HttpResponse class APIKeyCheckMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # 특정 경로에 대해서만 헤더 확인 if request.path.startswith("/api/private/"): api_key = request.headers.get("X-API-KEY") # Django 3.2+에서는 request.headers 사용 if api_key != "EXPECTED_SECRET_KEY": # 헤더가 없거나 값이 틀리면 401 Unauthorized 응답 반환 return HttpResponse("Invalid or missing API Key", status=401) # 정상인 경우 다음 단계 진행 response = self.get_response(request) return response
- 응답 헤더 추가 미들웨어: 응답에 공통으로 포함시킬 헤더가 있을 경우, 매 뷰마다 넣는 대신 미들웨어에서 일괄 처리할 수 있습니다. 예를 들어 모든 API 응답에 커스텀 헤더(X-Server 등)를 추가하거나, CORS 관련 헤더를 전역 설정하는 미들웨어를 작성할 수 있습니다. 아래는 모든 응답에 X-Powered-By: Django 헤더를 삽입하는 간단한 예시입니다:이처럼 response 객체를 수정함으로써 공통 헤더를 추가할 수 있습니다. (단, 이미 해당 헤더가 존재하는 경우 덮어쓰게 되므로 상황에 맞게 처리하세요.) 실제 CORS 헤더 설정의 경우 Django에서는 CorsMiddleware 등을 사용하지만, 원리를 보면 미들웨어에서 Access-Control-* 헤더들을 응답에 넣어주는 방식입니다.
- class PoweredByHeaderMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) # 응답 헤더 삽입 response["X-Powered-By"] = "Django" return response
以上の例のように, 미들웨어를 활용하면 로깅, 공통 인증/인가 검사, 응답 가공 등을 애플리케이션 전역에서 일관되게 처리할 수 있습니다. 필요에 따라 이 외에도 요청당 타이밍 측정(예: 요청 처리 시간 계산), 사용량 제한(rate limiting), Locale 처리 등의 기능들을 구현할 수 있습니다.
Custom Middleware 구현 방법과 settings.py에 등록하는 방법
커스텀 미들웨어를 구현하려면 우선 파이썬 모듈 경로상에 미들웨어 클래스를 정의하고, Django 설정의 MIDDLEWARE 리스트에 해당 경로를 추가하면 됩니다. 일반적인 구현 단계는 다음과 같습니다:
- 미들웨어 클래스 작성: 원하는 기능을 수행하는 미들웨어를 클래스 형태로 작성합니다. 앞서 언급한 대로 __init__ 및 __call__ 메서드를 구현하는 것이 기본이며, 필요에 따라 process_request, process_response, process_view, process_exception 등의 메서드를 정의할 수 있습니다. Django에서 제공하는 django.utils.deprecation.MiddlewareMixin을 상속하면 process_* 메서드를 편리하게 사용할 수 있습니다 (이 믹스인은 기존 스타일의 메서드들을 __call__ 내부에서 적절히 호출해주는 역할을 합니다).
위 클래스는 MiddlewareMixin을 상속하여 process_request와 process_response를 구현했습니다. Django는 요청 진입 시 모든 process_request를 호출하고, 뷰 처리 후 process_response를 역순으로 호출하므로, 우리는 별도 __call__을 정의하지 않고도 원하는 타이밍에 코드를 주입할 수 있습니다. (MiddlewareMixin을 사용하지 않고 __call__ 내부에서 직접 구현해도 됩니다. 어느 방법을 쓰든 동작 순서는 동일합니다.)import time from django.utils.deprecation import MiddlewareMixin class RequestTimerMiddleware(MiddlewareMixin): def process_request(self, request): # 요청 시작 시간 기록 request._start_time = time.time() def process_response(self, request, response): # 요청 시작 시간과 현재 시간으로 실행 소요 시간 계산 start_time = getattr(request, "_start_time", None) if start_time: duration = time.time() - start_time # X-Duration (ms) 헤더 추가 response["X-Duration-ms"] = f"{duration*1000:.2f}" return response
- 예를 들어, 간단한 Timer 미들웨어를 구현해보겠습니다. 이 미들웨어는 요청을 받을 때 시간 기록을 시작하고, 응답이 나갈 때까지의 소요 시간을 계산해 응답 헤더에 삽입합니다:
- settings.py에 등록: 만든 미들웨어 클래스를 Django 설정에 등록해야 실제로 동작합니다. settings.py 파일의 MIDDLEWARE 리스트에 미들웨어의 전체 경로(문자열)를 원하는 순서에 추가합니다. 예를 들어, 우리가 myproject/middleware.py에 RequestTimerMiddleware를 구현했다면:미들웨어 순서가 중요한 경우(예: 인증 관련 미들웨어는 세션이 활성화된 후에 와야 함 등) 적절한 위치에 삽입해야 합니다. 일반적으로 커스텀 미들웨어를 목록의 마지막 부분에 두는 경우가 많습니다. 이는 대개 다른 Django 내장 미들웨어의 결과를 활용하거나, 최종 응답을 수정하는 용도이기 때문입니다. 하지만 필요에 따라 순서를 조정할 수 있으며, Django 공식 문서의 미들웨어 순서 가이드를 참고하면 일반적인 권장 순서를 알 수 있습니다.
- MIDDLEWARE = [ # Django 기본 미들웨어들... 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', # ... (중략) # 우리가 작성한 미들웨어 추가 'myproject.middleware.RequestTimerMiddleware', ]
- 서버 재시작 및 동작 확인: 미들웨어를 추가/변경한 후에는 서버를 재시작해야 적용됩니다. 잘 등록되었다면, 의도한 대로 요청/응답에 미들웨어 로직이 적용되는지 확인합니다. 예를 들어 Timer 미들웨어를 등록했다면, 응답 헤더에 X-Duration-ms가 추가되었는지 확인해볼 수 있습니다.
Note: 미들웨어를 작성할 때 비동기 지원 여부도 고려해야 합니다. Django 3.1+ 버전부터 async view를 지원하며, 미들웨어도 sync/async 방식에 따라 동작합니다. 기본적으로 위와 같은 미들웨어는 동기(synchronous) 방식이며, 동기 view에만 적용됩니다. 만약 async view에도 적용하려면 비동기 미들웨어 지원을 확인하여 코드에 async 키워드를 붙이거나, 비동기 전용 미들웨어를 작성해야 할 수 있습니다. 초중급 수준에서는 대부분 동기 미들웨어로 충분하겠지만, 최신 Django 환경에서는 이 점도 염두에 두세요.
DRF의 APIView/GenericAPIView와 Middleware 간의 관계 (동작 시점 등)
Django REST Framework의 APIView/GenericAPIView와 미들웨어는 각각의 역할을 수행하며, 처리 순서의 관점에서 보면 미들웨어가 먼저, DRF 뷰 로직이 나중입니다. 이를 조금 더 세부적으로 살펴보겠습니다.
- 미들웨어 단계 – 클라이언트 요청이 들어오면 URL 매칭 및 뷰 결정 전에 Django의 미들웨어들이 순차적으로 실행됩니다. 이 과정에서 SessionMiddleware, AuthenticationMiddleware 등이 세션 기반 사용자 인증을 처리하고 CsrfViewMiddleware 등이 CSRF 토큰 검증을 수행합니다. 모든 활성 미들웨어의 process_request/__call__ 전처리가 끝나면 비로소 URL resolver가 URL을 해당 뷰(View 함수/클래스)에 매핑하고, 뷰를 호출하게 됩니다.
- DRF 뷰 단계 – DRF의 APIView (또는 GenericAPIView를 상속한 View 등)는 Django의 View 함수처럼 호출됩니다. APIView는 내부에서 dispatch 메서드를 재정의하여, 요청을 DRF의 Request 객체로 변환하고 (.initialize_request() 호출), 인증/권한을 체크한 뒤 (.perform_authentication(), .check_permissions() 등) 적절한 핸들러 메서드 (get(), post() 등)를 호출합니다. GenericAPIView는 APIView를 확장한 것으로, .get_queryset()이나 .get_object() 등을 추가로 활용하지만 미들웨어와의 관계에서는 특별한 차이가 없습니다. 한마디로, 미들웨어 체인을 통과한 요청은 DRF의 APIView에 도달하고, APIView 내부 로직에 따라 요청을 처리하게 됩니다.
- 이 시점에서 DRF는 개발자가 뷰에서 Response 객체를 반환하도록 유도합니다. APIView는 뷰 메서드의 반환값이 Response일 경우 이를 처리하며, 만약 Django의 HttpResponse나 JsonResponse를 반환해도 문제는 없습니다. 다만 DRF의 Response를 사용하면 자동으로 콘텐츠 협상과 렌더링이 적용되어 편리합니다.
- 응답 반환 및 미들웨어 후처리 단계 – DRF 뷰가 처리 결과로 Response 객체를 리턴하면, APIView의 dispatch 메서드는 이를 최종적으로 HttpResponse 객체로 변환하여 반환합니다. (DRF Response는 아직 렌더링되지 않은 데이터를 포함하고 있으므로, .finalize_response()를 통해 .render()가 호출되고 실제 바이트 응답이 만들어집니다.) 이제 Django는 이 최종 HttpResponse를 클라이언트로 보내기 전에 응답 단계의 미들웨어들을 역순으로 호출합니다. 각 미들웨어의 process_response(또는 __call__ 후처리 코드)이 실행되며, 필요에 따라 응답을 가로채 변경하거나 (예: 헤더 추가/수정) 로그를 남길 수 있습니다. 마지막으로 모든 미들웨어 처리가 끝나면 수정된(또는 그대로의) HttpResponse가 WSGI를 통해 클라이언트에게 전송됩니다.
이러한 관계에서 중요한 것은 미들웨어는 DRF 뷰보다 앞단에 있으므로, DRF의 여러 기능(특히 인증, 예외 처리 등)과 겹치는 작업을 할 때 타이밍을 고려해야 한다는 것입니다. 예를 들어, DRF의 Permission 클래스에서 403을 리턴할 상황이라면 미들웨어에서는 권한을 별도로 체크하지 않고 DRF에 맡기는 편이 낫습니다. 반대로, DRF 레벨에서 처리되지 않는 전역 정책(예: 요청당 로깅, 특정 헤더 요구 등)은 미들웨어에서 선제적으로 처리하는 것이 효과적입니다. 또한 DRF의 Exception Handling (예외 처리)은 APIView 내부에서 발생한 예외를 잡아 적절한 Response로 변환하는 방식으로 동작하므로, 미들웨어가 DRF 예외를 가로채지 않도록 유의해야 합니다. 이 부분은 다음 섹션에서 자세히 다루겠습니다.
기타 유용한 팁 (성능 고려, 예외 처리, DRF 예외 처리와의 충돌 방지 등)
마지막으로, Django/DRF에서 커스텀 미들웨어를 사용할 때 알아두면 좋은 팁들을 정리합니다:
- 성능 고려: 미들웨어는 모든 요청에 대해 실행되므로, 가능한 한 가볍게 유지하는 것이 좋습니다. 복잡한 DB 조회나 외부 API 호출 등의 작업을 미들웨어에서 수행하면 애플리케이션 전반의 응답 속도가 저하될 수 있습니다. 따라서 캐싱을 활용하거나, 필요한 경우 특정 URL 패턴에만 동작하도록 조건문을 사용하는 등 최적화를 고려하세요. 또한 불필요하게 많은 미들웨어를 등록하는 것도 지양해야 합니다. Django 공식 문서에서는 미들웨어로 인해 발생하는 약간의 오버헤드를 언급하며, 지원하지 않는 sync/async 상황 변환 시 성능 패널티가 있을 수 있음을 지적합니다. 즉, 자신이 작성한 미들웨어가 동기 코드인데 async 뷰에 적용된다면 Django가 이를 자동으로 감싸 주지만, 약간의 성능 손해가 있을 수 있다는 것입니다. 가급적 애플리케이션에 정말 필요한 공통 기능만 미들웨어로 만들고, 나머지는 개별 뷰 로직이나 DRF 기능(예: Throttle, Permission 등)으로 처리하는 것이 바람직합니다.
- 예외 처리 (Exception Handling): 미들웨어에 process_exception 메서드를 구현하면 뷰 실행 중 발생한 처리되지 않은 예외를 가로채서 대응할 수 있습니다. 예를 들어 모든 예외를 로깅하거나 특정 예외 타입에 대해 사용자 친화적인 메시지를 JSON으로 반환하는 글로벌 에러 처리 미들웨어를 만들 수 있습니다. 단, DRF와 함께 사용할 때는 주의점이 있습니다. DRF는 자체적으로 API 예외들을 핸들링하여 HTTP 응답으로 변환하는 시스템을 가지고 있습니다. 예를 들어, 뷰 코드에서 django.http.Http404 예외가 발생하면 DRF는 자동으로 404 응답을 생성하고, PermissionDenied 예외가 발생하면 403 응답을 반환하는 등 여러 예외를 적절한 HTTP 상태 코드로 매핑합니다. 또한 DRF의 APIException (및 그 하위 클래스들, 예: NotAuthenticated, NotFound, ValidationError 등)은 APIView 내부에서 잡혀 (handle_exception 메서드) 표준화된 오류 응답 ({"detail": "..."} 형태 등)으로 변환됩니다.참고: DRF에서 전역 예외 응답 포맷을 커스터마이징하고 싶다면, 미들웨어 대신 DRF의 Exception Handler를 사용하는 방법도 있습니다. settings.py의 REST_FRAMEWORK = {'EXCEPTION_HANDLER': '경로.함수명'} 설정을 통해 자신만의 예외 처리 함수를 등록하면, DRF는 APIView내에서 발생한 예외를 모두 해당 핸들러로 위임합니다. 이 방식을 쓰면 DRF 레벨에서 통일된 에러 형식을 직접 정할 수 있으며, 미들웨어와 충돌 걱정 없이 동작합니다. 물론 이와 별개로 미들웨어의 process_exception을 사용해 뷰 바깥에서 발생한 예외(예: URL resolver 단계의 404 등)에 대한 처리나 추가 로그를 남기는 것을 병행할 수 있습니다.
- 이러한 구조 때문에, 미들웨어의 process_exception에서는 DRF가 처리하는 예외들을 섣불리 가로채지 않는 것이 좋습니다. 만약 미들웨어에서 DRF 예외를 잡아서 별도의 응답을 리턴해 버리면, DRF가 제공하는 일관된 오류 응답 형식이 무시될 수 있습니다. 예를 들어 NotAuthenticated 예외 발생 시 DRF는 401 상태코드와 JSON 바디를 반환하지만, 미들웨어에서 이를 가로채 단순히 HttpResponse("Unauthorized", status=401)를 리턴한다면 응답 포맷 일관성이 깨집니다. 따라서 **DRF의 표준 예외는 그대로 두고(return None으로 다음 처리를 맡김), 미들웨어에서는 주로 잡히지 않은 예외(unhandled exception)**에 대해 처리하는 패턴이 권장됩니다. 앞서 예시로 든 로깅 미들웨어에서도 process_exception에서 return None으로 설정한 이유가 이것입니다. DRF가 처리하지 못한 예외(예: 코드를 잘못 작성해 발생한 TypeError 같은 것들)에 대해서만 미들웨어가 JSON 500 응답을 만들어준다든지, 로그만 남기고 기본 500 페이지를 내보내게 할 수 있습니다.
- 기타 주의 사항: 미들웨어 코드는 가급적 상태를 가지지 않도록 설계해야 합니다. __init__은 애플리케이션 시작 시 한 번만 호출되므로, 공유 자원을 연결하거나 설정을 로드하는 정도로만 활용하고 요청별 상태는 request 객체에 저장하거나 로컬 변수로 다루세요. 또한 미들웨어에서 예외를 던지는 것은 피하고, 문제 발생 시 적절한 HTTP 응답을 반환하거나 로그를 남기는 방식으로 처리하는 것이 좋습니다. 미들웨어가 던진 예외는 Django의 기본 예외 처리기(또는 다른 미들웨어의 process_exception)로 넘어가 500 오류를 발생시킬 수 있으므로, 의도적으로 500을 내보내는 게 아니라면 직접 예외 대신 HttpResponse를 리턴하는 편이 안전합니다.
정리하면, Django와 Django REST Framework에서 미들웨어는 강력한 공통 처리 수단이며, 올바르게 활용하면 코드의 중복을 줄이고 일관성을 높일 수 있습니다. 미들웨어의 구조와 DRF와의 상호작용 시나리오를 이해하고 나면, API 요청 로깅이나 글로벌 에러 처리 같은 **크로스컷팅 관심사(cross-cutting concern)**를 효과적으로 구현할 수 있습니다. 다만, 언제나 그렇듯 전역에 영향을 미치는 만큼 성능과 상호 호환을 고려하여 신중하게 적용해야 합니다. Django 공식 문서와 DRF 가이드라인을 참고하면서, 필요에 맞게 커스텀 미들웨어를 작성해보세요. 여러분의 Django API 프로젝트에 한층 견고한 기반을 마련할 수 있을 것입니다.
참고 자료: Django 공식 문서 (Middleware), Django REST Framework 공식 문서, 기타 블로그 (Better Stack 커뮤니티의 예외 처리 가이드 등), 및 Stack Overflow 사례 등을 참고하였습니다.
'백엔드' 카테고리의 다른 글
Django REST Framework: APIView, Generic View, ViewSet 차이점과 사용 예시 (0) | 2025.05.20 |
---|---|
Django에서 HttpResponse와 DRF Response 차이점 (0) | 2025.05.15 |
Python과 Django 웹 개발 시작하기: 초보자를 위한 튜토리얼 (0) | 2025.05.14 |
Flask vs FastAPI vs Django: 파이썬 웹 프레임워크 비교 (0) | 2025.05.13 |
Django(장고) 웹 프레임워크 알아보기: 기본 개념, 프로젝트 구조와 장단점 (0) | 2025.05.12 |