전역 오류 처리#

이 글은 Django Ninja 시리즈 튜토리얼의 22번째 글이에요.
이전 글에서는 HttpError를 다루는 방법을 배웠고, 이를 뷰 함수 내에서만 사용할 것을 권장했어요.
하지만 이것만으로는 프로젝트 API의 오류 처리가 아직 한참 불완전해요. 적어도 다음과 같은 3가지 흔한 해결 과제가 남아있어요:
- Schema 안의 검증 메서드에서
raise HttpError를 하지 않으려면 어떻게 해야 할까요? - 데이터베이스 작업 오류와 같은 다른 유형의 오류는 어떻게 처리해야 할까요?
- 각기 다른 API 오류의 응답 형식이 일관되게 보장하려면 어떻게 해야 할까요?
이 질문들은 모두 하나의 더 큰 요구 사항을 가리켜요: 우리는 포괄적인 오류 처리 메커니즘이 필요해요.
이 글에서는 바로 이 질문들에 대한 답을 제시하려고 해요. 모든 코드 변경 사항은 이 PR을 참고해 주세요.
GitHub 예제 프로젝트#
Django ValidationError로 변경하기#
검증 메서드 내에서 가장 원시적인 버전은 ValueError를 던지는 것이었던 걸 기억하시나요?
ValueError는 Django Ninja가 자동으로 잡아내어 422 응답을 제공해요. 이것은 좋은 일이지만 우리의 커스터마이징 요구 사항에는 맞지 않아요.
그래서 나중에 HttpError를 도입했어요. 이것 역시 자동으로 잡히긴 하지만 응답 형식과 내용이 더 간결하고, 오류 메시지 외에도 상태 코드를 커스터마이징할 수 있었죠.
하지만 이전 글에서 언급했듯이, 그렇게 하는 것은 비록 간단해 보일지 몰라도 사실 적절하지 않아요.
그럼 도대체 무슨 오류를 던져야 할까요?
Pydantic이나 Django Ninja가 제공하는 오류 사용 피하기#
이전 글에서 Pydantic이든 Django Ninja이든 모두 내장된 ValidationError를 가지고 있다고 언급했어요.
하지만 그것들은 주로 프레임워크 내부 사용을 위한 것이고 반환되는 오류 형식이 너무 상세하며, 초기화 방식도 까다로워요. 예를 들어 Django Ninja의 검증 오류는 다음과 같이 초기화해야 해요:
raise ValidationError(
[{'loc': ('confirm_password',),
'msg': '비밀번호와 확인 비밀번호는 동일해야 합니다.',
'type': 'value_error'}])
이것은 우리가 익숙한 「오류 메시지 문자열 하나만 넣으면 되는」 방식이 아니에요.
따라서 저는 검증 로직 안에서 이러한 오류 유형을 직접 사용하는 것을 추천하지 않아요.
Django ValidationError를 애용해 주세요#
Schema 검증 로직 내에서는 Django에 내장된 ValidationError를 사용하는 것이 더 바람직해요.
이것의 설계는 이미 개발자의 요구를 완벽하게 고려하고 있어요. 초기화 방식이 단순할 수도 있고 (단일 문자열 사용), 복잡할 수도 있으며 (list나 dict 사용), 대다수의 상황에 적합해요.
여기서는 문자열로 초기화하기만 하면 충분해요. 수정된 코드는 다음과 같아요:
from django.core.exceptions import ValidationError
class CreateUserRequest(Schema):
password: str
confirm_password: str
@model_validator(mode='after')
def check_passwords_match(self):
if self.password != self.confirm_password:
raise ValidationError('비밀번호와 확인 비밀번호는 동일해야 합니다.')
기존의 HttpError를 Django의 ValidationError로 바꿨어요.
그리고 「오류 메시지 문자열」을 초기화 방식으로 사용하여 원래 있던 첫 번째 매개변수인 「상태 코드」를 제거했어요.
Django Ninja는 이런 오류들을 자동으로 처리하지 않아요#
던지는 오류 유형을 Django의 ValidationError로 변경한 후, 한 가지 문제를 눈치채셨을 수 있어요: Django Ninja는 이런 오류들을 자동으로 잡아내지 않는다는 것이에요!
즉, 우리가 ValidationError를 던질 때, Django Ninja는 HttpError를 처리할 때처럼 자동으로 형식을 맞추어 422 오류 응답을 반환하는 대신, 바로 500 에러를 띄우게 돼요.
이 부분은 〈20편: 데이터 검증 (하) Pydantic 교차 필드 검증〉의 끝부분에서 언급했었어요.
이제 구체적인 해결 방법인 exception_handler를 소개할게요.
우리는 이렇게 던져진 오류들을 스스로 처리해야 하며, 이것이 바로 exception_handler가 활약하는 부분이에요.
전역 오류 처리기——Exception Handlers#
이러한 다양한 출처(Schema 검증 메서드에 국한되지 않음)에서 발생한 동일한 유형의 오류들을 통합 처리하기 위해, 우리는 Django Ninja가 제공하는 @api.exception_handler 데코레이터를 사용할 수 있어요.
이 데코레이터를 통해 우리는 「특정 유형의 오류」에 전용 응답 로직을 정의하고 이를 전체 API 범위 내에 적용할 수 있게 돼요.
exception_handler 정의하기#
우리는 Django의 ValidationError를 위한 전역 오류 처리기를 정의하여 어느 곳에서든 이 오류가 발생할 때 handler가 이를 잡아내서 API가 우리가 정의한 커스텀 응답 형식을 반환하도록 할 수 있어요.
프로젝트의 api.py에 다음 코드를 추가하세요:
# NinjaForum/api.py
from django.core.exceptions import ValidationError
from django.http import HttpRequest, HttpResponse
from ninja import NinjaAPI
api = NinjaAPI(...)
api.add_router(...)
api.add_router(...)
# 새로 추가된 exception handler
@api.exception_handler(exc_class=ValidationError)
def django_validation_error_handler(
request: HttpRequest, exception: ValidationError
) -> HttpResponse:
"""
Django ValidationError 예외 처리
"""
return api.create_response(
request, {'detail': exception.message}, status=400
)
우리는 Django의 ValidationError가 발생했을 때 커스텀 오류 메시지가 포함된 HTTP 400 응답을 반환하는 exception handler 함수를 정의했어요. 이렇게 하면 응답 형식의 일관성을 유지할 수 있죠.
코드는 매우 간단하지만 안에 많은 핵심 포인트가 담겨 있으니 하나씩 분석해 볼게요.
Exception Handlers 핵심 분석#
「프로젝트 구성(조직)」이라는 주제부터 이야기해 볼게요.
1. Exception Handlers 함수는 어디에 두는 것이 좋을까요?#
앞서 언급했듯이 이 오류 처리기의 영향 범위는 전역(Global)이므로 프로젝트의 어느 곳에든 둘 수 있어요.
하지만 가장 적합한 위치에 두는 것을 권장해요. 제 생각에는 주로 두 가지 선택지가 있어요:
- 오류 처리 함수가 많지 않다면, 프로젝트의
api.py안에 직접 배치할 수 있어요——우리 예제에서도 그렇게 했죠. 이는 프로젝트api.py의 전역 관리 속성에 잘 부합해요. - 오류 처리가 꽤 많다면 별도의 Python 모듈로 분리하여 관리하는 것을 추천해요.
2. 함수와 매개변수 네이밍#
제가 좋아하는 「네이밍」 부분이 또 왔네요☺️
Exception handler는 (데코레이터가 적용된) 함수이므로, 이론상으로는 「동사로 시작」하는 함수 네이밍 관례를 따라야 해요.
하지만 저는 django_validation_error_handler처럼 다소 「명사」에 치우친 네이밍을 사용했어요.
왜냐하면 이것의 본질은 전통적인 의미의 함수라기보다 처리 장치나 메커니즘에 더 가깝기 때문이에요.
물론, 이것은 여러분이 어떤 관점에서 보느냐에 달려 있어요! 「처리 행위가 있으니 여전히 동사로 시작하는 네이밍을 사용해야 한다」고 말씀하신다면 저도 전적으로 동의해요.
다음은 exception 매개변수예요. Django Ninja 문서에서는 항상 exc로 명명하는데 저는 개인적으로 이 이름이 무척 싫어요. exc는 전혀 직관적이지 않으며, 불필요한 축약이라고 생각하거든요.
조금 양보하더라도 Pydantic 검증 메서드의 v처럼 단일 문자 e를 사용하는 편이 나아요.
3. 함수 로직 분석#
Exception handler의 함수 로직은 길거나 짧을 수 있고, 간단하거나 복잡할 수 있지만 결국 다음 두 가지 일을 해요:
- 특정 오류 유형 수신하기.
- 특정 HTTP 응답 반환하기.
이 예제에서 우리는 Django의 ValidationError를 수신하고 「400 Bad Request」를 반환했어요. 게다가 오류 메시지의 내용은 발생한 오류로부터 가져오도록 우리가 직접 정의했죠.
이러한 오류 처리의 유연성은 꽤 훌륭하다고 볼 수 있어요. 만약 ValidationError가 list나 dict로 초기화되었다면 이 처리 함수는 조금 더 복잡하게 작성되어야 해요.
실력 발휘하기: 404 응답 처리를 예제로#
자주 등장하는 404를 처리하는 또 다른 exception handler를 구현해 볼게요.
「단일 게시글 정보 가져오기」 API를 예로 들게요:
@router.get(...)
def get_post(request: HttpRequest, post_id: int) -> Post:
"""
단일 게시글 정보 가져오기
"""
post = Post.objects.get(id=post_id)
return post
현재 프론트엔드가 존재하지 않는 게시글 id를 입력하면 서버는 곧바로 500 에러를 띄울 거예요:
raise self.model.DoesNotExist( post.models.Post.DoesNotExist: Post matching query does not exist.
게다가 내부 메시지까지 노출되니——이건 정말 어처구니없는 일이죠! 🤣
Django ORM에서 QuerySet의 get 메서드는 조회 결과가 없거나 여러 개의 결과가 조회될 때 모두 오류를 발생시키기 때문이에요. 그리고 우리는 이러한 오류를 잡거나 처리하지 않았으므로 서버가 바로 500 에러를 띄운 거죠.
두 가지 오류는 서로 다르고——오류 메시지도 달라야 해요. 여기서는 첫 번째 경우만 처리하도록 할게요.
결과가 없을 때 404 응답 반환하기#
이 두 글의 설명을 통해, 404 응답을 반환하는 두 가지 방법을 알게 되셨을 거예요.
첫 번째, HttpError 직접 사용하기:
try:
post = Post.objects.get(id=post_id)
except Post.DoesNotExist:
raise HttpError(404, '게시글이 존재하지 않습니다.')
이것은 우리가 이전 글에서 다뤘던 방식이며 이 또한 꽤 권장하는 방식이에요.
두 번째, exception handler 사용하기:
# NinjaForum/api.py
from django.core.exceptions import ObjectDoesNotExist
...
@api.exception_handler(exc_class=ObjectDoesNotExist)
def object_does_not_exist_handler(
request: HttpRequest, exception: ObjectDoesNotExist
) -> HttpResponse:
"""
Django ObjectDoesNotExist 예외 처리
"""
return api.create_response(
request, {'detail': '데이터를 찾을 수 없습니다.'}, status=404)
이 방식은 첫 번째 방식과 비교했을 때 각각의 장단점이 있어요:
- 장점: 뷰 함수의 내용을 변경할 필요가 없고 (코드가 더 간결해짐), 모든 API에서 발생하는
ObjectDoesNotExist오류를 잡아낼 수 있어요. (ObjectDoesNotExist는Post.DoesNotExist의 부모 클래스예요) - 단점: 「상세한」 커스텀 오류 메시지를 정의할 수 없어요——어떤 모델 객체를 조회하다가
ObjectDoesNotExist가 발생했는지 알 수 없기 때문이죠.- 물론 서로 다른 오류에 대해 각각의 exception handler를 기꺼이 정의하겠다면 가능해요!——예를 들어
Post.DoesNotExist만 잡도록 하고, 오류 메시지를 「게시글이 존재하지 않습니다.」로 작성할 수 있죠. - 하지만 그러면 여러 개의 exception handler를 정의해야 해서 조금 번거로울 거예요!
- 물론 서로 다른 오류에 대해 각각의 exception handler를 기꺼이 정의하겠다면 가능해요!——예를 들어
첫 번째 방식을 선택할지, 두 번째 방식을 선택할지는 상황에 따라 판단해야 해요.
404 응답 효과#
HttpError사용 시:
- exception handler 사용 시:
제5장 요약#
솔직히 말해 제5장은 정보량이 꽤 많았어요. 이 4편의 글을 쓰는 데 오랜 시간이 걸렸고, 중간에 「리팩터링」까지 했죠!——원래는 2편뿐이었거든요.
우리는 먼저 단일 필드 검증을 커스터마이징하는 방법과 교차 필드 검증에 대해 논의했어요. 그런 다음 API에서 발생하는 오류를 처리하는 방법을 점진적으로 학습하며——점점 더 우아하게, 점점 더 포괄적으로 다루었어요.
제1편부터 여기까지 보셨다면 정말이지 충분히 스스로 자랑스러워하셔도 돼요.
다음 단계#
이제 좀 편해졌을까요?——아쉽게도 그렇지 않아요.
우리는 API의 일반적인 고급 기능들을 소개할 거예요.
이러한 기능들은 요청과 응답을 처리하는 것에 비하면 「반드시」 필요한 것은 아니라고 할 수도 있지만, 많은 API 프로젝트에서 여전히 매우 중요해요.
다음 장에서는 이러한 고급 기능들을 하나씩 깊이 살펴보고, Django Ninja에서 이를 어떻게 구현하는지 배울 거예요. API 개발의 세계로 계속 깊이 들어가 볼까요!